Skip to content

Commit

Permalink
feat: better readability for characters in admin view (#96)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
corp-0 authored Sep 22, 2024
1 parent e206f08 commit bb66a17
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 6 deletions.
6 changes: 3 additions & 3 deletions src/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class AccountAdminView(admin.ModelAdmin):
"is_confirmed",
"is_verified",
"is_active",
"is_staff",
"is_superuser",
"legacy_id",
)
fieldsets = (
Expand All @@ -58,14 +58,14 @@ class AccountAdminView(admin.ModelAdmin):
"is_active",
"is_confirmed",
"is_verified",
"is_staff",
"is_superuser",
),
},
),
("Legacy", {"classes": ("wide",), "fields": ("legacy_id",)}),
)
inlines = [AccountConfirmationInline, PasswordResetRequestInline]
list_filter = ("is_staff", "is_verified", "is_confirmed", "is_active")
list_filter = ("is_superuser", "is_verified", "is_confirmed", "is_active")
search_fields = (
"email__icontains",
"username__icontains",
Expand Down
2 changes: 1 addition & 1 deletion src/persistence/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

@admin.register(Character)
class CharacterAdminView(admin.ModelAdmin):
pass
readonly_fields = ("character_name", "last_updated")
Empty file.
Empty file.
Empty file.
98 changes: 98 additions & 0 deletions src/persistence/management/commands/nuke_duplicated_characters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import json
import logging

from django.core.management.base import BaseCommand
from django.db import transaction

from accounts.models import Account
from persistence.models import Character

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Finds all duplicated characters in the database and deletes them by comparing their JSON data."

def add_arguments(self, parser):
parser.add_argument(
"--dry-run",
action="store_true",
help="Dry run mode: shows what would be deleted without actually deleting anything.",
)

def handle(self, *args, **options):
dry_run = options["dry_run"]

if dry_run:
logger.info("Running nuke duplicated characters command in dry run mode")
else:
logger.warning(
"we are about to run the nuke duplicated characters command! This operation can not be undone."
)

total_deleted = 0

accounts = Account.objects.all()
for account in accounts:
character_map = self.get_character_map(account)
duplicates = self.get_duplicates(character_map)

if duplicates:
total_deleted += self.process_duplicates(account, duplicates, dry_run)

if dry_run:
logger.info("Dry run completed. No characters were deleted.")
else:
logger.info("Total duplicated characters deleted: %d", total_deleted)

@staticmethod
def get_character_map(account) -> dict:
"""Returns a dictionary mapping character data (as serialized JSON) to a list of characters."""
characters = Character.objects.filter(account=account)
character_map: dict[str, list[Character]] = {}

for character in characters:
data_str = json.dumps(character.data, sort_keys=True)
if data_str in character_map:
character_map[data_str].append(character)
else:
character_map[data_str] = [character]

return character_map

@staticmethod
def get_duplicates(character_map: dict) -> list:
"""Returns a list of duplicate characters for a given character map."""
return [chars[1:] for chars in character_map.values() if len(chars) > 1]

def process_duplicates(self, account: Account, duplicates: list, dry_run: bool) -> int:
"""Processes the duplicates, logging and optionally deleting them."""
total_deleted = 0

for chars_to_delete in duplicates:
char_ids = [char.id for char in chars_to_delete]

if dry_run:
logger.info(
"[Dry run] would delete these duplicated characters for account %s: %s",
account.unique_identifier,
char_ids,
)
else:
self.delete_characters(account, char_ids)

total_deleted += len(chars_to_delete)

return total_deleted

@staticmethod
def delete_characters(account, char_ids: list):
"""Deletes the characters with the specified IDs."""
with transaction.atomic():
Character.objects.filter(id__in=char_ids).delete()
logger.warning(
"Deleted the following characters for account %s: %s",
account.unique_identifier,
char_ids,
)
logger.warning("This operation cannot be undone; they are gone forever...")
10 changes: 8 additions & 2 deletions src/persistence/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class Character(models.Model):
)

data = models.JSONField(
name="data", verbose_name="Character data", help_text="Unstructured character data in JSON format."
name="data",
verbose_name="Character data",
help_text="Unstructured character data in JSON format.",
)
"""The character data."""

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

def __str__(self):
return f"{self.account.unique_identifier}'s character"
return f"{self.character_name} by {self.account.unique_identifier}"

@property
def character_name(self) -> str:
return self.data.get("Name", "Unknown")
Empty file.
Empty file.
64 changes: 64 additions & 0 deletions src/tests/persistence/commands/nuke_duplicated_characters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import json

from django.core.management import call_command
from django.test import TestCase

from accounts.models import Account
from persistence.models import Character


class DeleteDuplicateCharactersCommandTest(TestCase):
def setUp(self):
# Create two test accounts
self.account1 = Account.objects.create(
unique_identifier="testuser1", username="testuser1", email="[email protected]"
)
self.account2 = Account.objects.create(
unique_identifier="testuser2", username="testuser2", email="[email protected]"
)

# Define character data
data_unique1 = {"Name": "Unique Character", "Age": 30}
data_unique2 = {"Name": "Unique Character", "Age": 25}
data_duplicate = {"Name": "Duplicate Character", "Age": 40}

# Add characters to account1
Character.objects.create(account=self.account1, data=data_unique1)
Character.objects.create(account=self.account1, data=data_unique2)
Character.objects.create(account=self.account1, data=data_duplicate)
Character.objects.create(account=self.account1, data=data_duplicate)

# Add characters to account2
Character.objects.create(account=self.account2, data=data_unique1)
Character.objects.create(account=self.account2, data=data_unique2)
Character.objects.create(account=self.account2, data=data_duplicate)
Character.objects.create(account=self.account2, data=data_duplicate)

def test_delete_duplicate_characters_command(self):
# Verify initial character counts
self.assertEqual(Character.objects.filter(account=self.account1).count(), 4)
self.assertEqual(Character.objects.filter(account=self.account2).count(), 4)

# Run the command in dry-run mode
call_command("nuke_duplicated_characters", "--dry-run")

# Ensure no characters were deleted in dry-run mode
self.assertEqual(Character.objects.filter(account=self.account1).count(), 4)
self.assertEqual(Character.objects.filter(account=self.account2).count(), 4)

# Run the command without dry-run to delete duplicates
call_command("nuke_duplicated_characters")

# Verify duplicates are deleted
self.assertEqual(Character.objects.filter(account=self.account1).count(), 3)
self.assertEqual(Character.objects.filter(account=self.account2).count(), 3)

# Collect remaining character data for account1
remaining_data_account1 = Character.objects.filter(account=self.account1).values_list("data", flat=True)
data_strings_account1 = [json.dumps(data, sort_keys=True) for data in remaining_data_account1]
self.assertEqual(len(set(data_strings_account1)), 3) # Should be 3 unique characters

# Collect remaining character data for account2
remaining_data_account2 = Character.objects.filter(account=self.account2).values_list("data", flat=True)
data_strings_account2 = [json.dumps(data, sort_keys=True) for data in remaining_data_account2]
self.assertEqual(len(set(data_strings_account2)), 3) # Should be 3 unique characters

0 comments on commit bb66a17

Please sign in to comment.