-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
10 changed files
with
174 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
Empty file.
98 changes: 98 additions & 0 deletions
98
src/persistence/management/commands/nuke_duplicated_characters.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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...") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
64 changes: 64 additions & 0 deletions
64
src/tests/persistence/commands/nuke_duplicated_characters.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |