Skip to content

Commit bb66a17

Browse files
authored
feat: better readability for characters in admin view (#96)
* fix: corrected admin field in Account that will allow users into the admin view * feat: improved readability of characters in admin view * feat: created command tu nuke all duplicated characters
1 parent e206f08 commit bb66a17

File tree

10 files changed

+174
-6
lines changed

10 files changed

+174
-6
lines changed

src/accounts/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class AccountAdminView(admin.ModelAdmin):
3434
"is_confirmed",
3535
"is_verified",
3636
"is_active",
37-
"is_staff",
37+
"is_superuser",
3838
"legacy_id",
3939
)
4040
fieldsets = (
@@ -58,14 +58,14 @@ class AccountAdminView(admin.ModelAdmin):
5858
"is_active",
5959
"is_confirmed",
6060
"is_verified",
61-
"is_staff",
61+
"is_superuser",
6262
),
6363
},
6464
),
6565
("Legacy", {"classes": ("wide",), "fields": ("legacy_id",)}),
6666
)
6767
inlines = [AccountConfirmationInline, PasswordResetRequestInline]
68-
list_filter = ("is_staff", "is_verified", "is_confirmed", "is_active")
68+
list_filter = ("is_superuser", "is_verified", "is_confirmed", "is_active")
6969
search_fields = (
7070
"email__icontains",
7171
"username__icontains",

src/persistence/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
@admin.register(Character)
77
class CharacterAdminView(admin.ModelAdmin):
8-
pass
8+
readonly_fields = ("character_name", "last_updated")

src/persistence/management/__init__.py

Whitespace-only changes.

src/persistence/management/commands/__init__.py

Whitespace-only changes.

src/persistence/management/commands/create_test_duplicated_characters.py

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import json
2+
import logging
3+
4+
from django.core.management.base import BaseCommand
5+
from django.db import transaction
6+
7+
from accounts.models import Account
8+
from persistence.models import Character
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class Command(BaseCommand):
14+
help = "Finds all duplicated characters in the database and deletes them by comparing their JSON data."
15+
16+
def add_arguments(self, parser):
17+
parser.add_argument(
18+
"--dry-run",
19+
action="store_true",
20+
help="Dry run mode: shows what would be deleted without actually deleting anything.",
21+
)
22+
23+
def handle(self, *args, **options):
24+
dry_run = options["dry_run"]
25+
26+
if dry_run:
27+
logger.info("Running nuke duplicated characters command in dry run mode")
28+
else:
29+
logger.warning(
30+
"we are about to run the nuke duplicated characters command! This operation can not be undone."
31+
)
32+
33+
total_deleted = 0
34+
35+
accounts = Account.objects.all()
36+
for account in accounts:
37+
character_map = self.get_character_map(account)
38+
duplicates = self.get_duplicates(character_map)
39+
40+
if duplicates:
41+
total_deleted += self.process_duplicates(account, duplicates, dry_run)
42+
43+
if dry_run:
44+
logger.info("Dry run completed. No characters were deleted.")
45+
else:
46+
logger.info("Total duplicated characters deleted: %d", total_deleted)
47+
48+
@staticmethod
49+
def get_character_map(account) -> dict:
50+
"""Returns a dictionary mapping character data (as serialized JSON) to a list of characters."""
51+
characters = Character.objects.filter(account=account)
52+
character_map: dict[str, list[Character]] = {}
53+
54+
for character in characters:
55+
data_str = json.dumps(character.data, sort_keys=True)
56+
if data_str in character_map:
57+
character_map[data_str].append(character)
58+
else:
59+
character_map[data_str] = [character]
60+
61+
return character_map
62+
63+
@staticmethod
64+
def get_duplicates(character_map: dict) -> list:
65+
"""Returns a list of duplicate characters for a given character map."""
66+
return [chars[1:] for chars in character_map.values() if len(chars) > 1]
67+
68+
def process_duplicates(self, account: Account, duplicates: list, dry_run: bool) -> int:
69+
"""Processes the duplicates, logging and optionally deleting them."""
70+
total_deleted = 0
71+
72+
for chars_to_delete in duplicates:
73+
char_ids = [char.id for char in chars_to_delete]
74+
75+
if dry_run:
76+
logger.info(
77+
"[Dry run] would delete these duplicated characters for account %s: %s",
78+
account.unique_identifier,
79+
char_ids,
80+
)
81+
else:
82+
self.delete_characters(account, char_ids)
83+
84+
total_deleted += len(chars_to_delete)
85+
86+
return total_deleted
87+
88+
@staticmethod
89+
def delete_characters(account, char_ids: list):
90+
"""Deletes the characters with the specified IDs."""
91+
with transaction.atomic():
92+
Character.objects.filter(id__in=char_ids).delete()
93+
logger.warning(
94+
"Deleted the following characters for account %s: %s",
95+
account.unique_identifier,
96+
char_ids,
97+
)
98+
logger.warning("This operation cannot be undone; they are gone forever...")

src/persistence/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ class Character(models.Model):
2222
)
2323

2424
data = models.JSONField(
25-
name="data", verbose_name="Character data", help_text="Unstructured character data in JSON format."
25+
name="data",
26+
verbose_name="Character data",
27+
help_text="Unstructured character data in JSON format.",
2628
)
2729
"""The character data."""
2830

@@ -32,4 +34,8 @@ class Character(models.Model):
3234
)
3335

3436
def __str__(self):
35-
return f"{self.account.unique_identifier}'s character"
37+
return f"{self.character_name} by {self.account.unique_identifier}"
38+
39+
@property
40+
def character_name(self) -> str:
41+
return self.data.get("Name", "Unknown")

src/tests/persistence/__init__.py

Whitespace-only changes.

src/tests/persistence/commands/__init__.py

Whitespace-only changes.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import json
2+
3+
from django.core.management import call_command
4+
from django.test import TestCase
5+
6+
from accounts.models import Account
7+
from persistence.models import Character
8+
9+
10+
class DeleteDuplicateCharactersCommandTest(TestCase):
11+
def setUp(self):
12+
# Create two test accounts
13+
self.account1 = Account.objects.create(
14+
unique_identifier="testuser1", username="testuser1", email="[email protected]"
15+
)
16+
self.account2 = Account.objects.create(
17+
unique_identifier="testuser2", username="testuser2", email="[email protected]"
18+
)
19+
20+
# Define character data
21+
data_unique1 = {"Name": "Unique Character", "Age": 30}
22+
data_unique2 = {"Name": "Unique Character", "Age": 25}
23+
data_duplicate = {"Name": "Duplicate Character", "Age": 40}
24+
25+
# Add characters to account1
26+
Character.objects.create(account=self.account1, data=data_unique1)
27+
Character.objects.create(account=self.account1, data=data_unique2)
28+
Character.objects.create(account=self.account1, data=data_duplicate)
29+
Character.objects.create(account=self.account1, data=data_duplicate)
30+
31+
# Add characters to account2
32+
Character.objects.create(account=self.account2, data=data_unique1)
33+
Character.objects.create(account=self.account2, data=data_unique2)
34+
Character.objects.create(account=self.account2, data=data_duplicate)
35+
Character.objects.create(account=self.account2, data=data_duplicate)
36+
37+
def test_delete_duplicate_characters_command(self):
38+
# Verify initial character counts
39+
self.assertEqual(Character.objects.filter(account=self.account1).count(), 4)
40+
self.assertEqual(Character.objects.filter(account=self.account2).count(), 4)
41+
42+
# Run the command in dry-run mode
43+
call_command("nuke_duplicated_characters", "--dry-run")
44+
45+
# Ensure no characters were deleted in dry-run mode
46+
self.assertEqual(Character.objects.filter(account=self.account1).count(), 4)
47+
self.assertEqual(Character.objects.filter(account=self.account2).count(), 4)
48+
49+
# Run the command without dry-run to delete duplicates
50+
call_command("nuke_duplicated_characters")
51+
52+
# Verify duplicates are deleted
53+
self.assertEqual(Character.objects.filter(account=self.account1).count(), 3)
54+
self.assertEqual(Character.objects.filter(account=self.account2).count(), 3)
55+
56+
# Collect remaining character data for account1
57+
remaining_data_account1 = Character.objects.filter(account=self.account1).values_list("data", flat=True)
58+
data_strings_account1 = [json.dumps(data, sort_keys=True) for data in remaining_data_account1]
59+
self.assertEqual(len(set(data_strings_account1)), 3) # Should be 3 unique characters
60+
61+
# Collect remaining character data for account2
62+
remaining_data_account2 = Character.objects.filter(account=self.account2).values_list("data", flat=True)
63+
data_strings_account2 = [json.dumps(data, sort_keys=True) for data in remaining_data_account2]
64+
self.assertEqual(len(set(data_strings_account2)), 3) # Should be 3 unique characters

0 commit comments

Comments
 (0)