diff --git a/backend/api/tests/test_loot_solver.py b/backend/api/tests/test_loot_solver.py index cb2004c..cf00623 100644 --- a/backend/api/tests/test_loot_solver.py +++ b/backend/api/tests/test_loot_solver.py @@ -1689,3 +1689,39 @@ def test_dev_setup_edgecase_bug_solution(self): self.assertEqual(len(expected), len(received), received) for i in range(len(expected)): self.assertDictEqual(expected[i], received[i], f'{i+1}/{len(received)}') + + def test_removed_pop_was_none_bug(self): + """ + Test Plan: + - Run Loot Solver with data from the latest sentry issue where it got a None.remove call + - See what happened, and fix the bug, to make sure it never happens again + """ + weeks = 0 + prio_brackets = { + 1: [1, 2], + 2: [3, 4, 5, 6], + } + floor_requirements = { + 'body': [5, 6, 3], + 'legs': [4, 1, 2], + 'tome-armour-augment': [4, 5, 6, 3], + } + + expected = [ + {'token': False, 'Body': 3, 'Legs': 4, 'Tome Armour Augment': 5}, + {'token': False, 'Body': 6, 'Legs': 1, 'Tome Armour Augment': 4}, + {'token': False, 'Body': 5, 'Legs': 2, 'Tome Armour Augment': 3}, + {'token': True, 'Body': None, 'Legs': None, 'Tome Armour Augment': 6}, + ] + + received = LootSolver._get_handout_data( + LootSolver.THIRD_FLOOR_SLOTS, + floor_requirements, + prio_brackets, + LootSolver.THIRD_FLOOR_TOKENS, + weeks, + False, + ) + self.assertEqual(len(expected), len(received), received) + for i in range(len(expected)): + self.assertDictEqual(expected[i], received[i], f'{i+1}/{len(received)}') diff --git a/backend/api/views/loot_solver.py b/backend/api/views/loot_solver.py index befb6be..a392bed 100644 --- a/backend/api/views/loot_solver.py +++ b/backend/api/views/loot_solver.py @@ -197,7 +197,7 @@ def _get_floor_data( """ # Limit floor requirements to the items that were important, then remove from this list as we update the prios below floor_requirements = { - slot: requirements.get(slot, []) + slot: deepcopy(requirements.get(slot, [])) for slot in slots } relevant_history = history.filter(item__in=slots).order_by('obtained') @@ -285,8 +285,11 @@ def _get_handout_data(slots: List[str], requirements: Requirements, prio_bracket """ handouts = [] remove_slots = slots.copy() - # Deepcopy the prio brackets dict so that sentry errors can print the upper level prio brackets for more debugging ease + + # Deepcopy passed dicts for Sentry debugging prio_brackets = deepcopy(prio_brackets) + requirements = deepcopy(requirements) + if 'augment' in slots[-1]: remove_slots = [remove_slots[-1]] while len(prio_brackets) > 0: @@ -306,11 +309,18 @@ def _get_handout_data(slots: List[str], requirements: Requirements, prio_bracket # This ensures each item is given to the person with the highest priority of getting it done = False potential_loot_members: Dict[int, List[str]] = {} + # Skip repeated single item lists + single_item_entries = set() for priority in sorted(prio_brackets, reverse=True): for member_id in prio_brackets[priority]: required = [slot for slot in requirements if member_id in requirements[slot]] - potential_loot_members[member_id] = required + if len(required) == 1: + if required[0] in single_item_entries: + continue + single_item_entries.add(required[0]) + + potential_loot_members[member_id] = required # Subtract from the set of things needed this week required_slots_for_week -= set(required) @@ -401,7 +411,7 @@ def _get_handout_data(slots: List[str], requirements: Requirements, prio_bracket prio_brackets[new_prio].append(member_id) # Now we need to remove the member_id from potentials and remove the item from the popped list in case we need to re-insert - removed = potential_loot_members.pop(member_id, None) + removed = potential_loot_members.pop(member_id, []) try: removed.remove(item) except ValueError: @@ -455,8 +465,8 @@ def _get_handout_data(slots: List[str], requirements: Requirements, prio_bracket return handouts + @staticmethod def _get_first_floor_data( - self, requirements: Requirements, history: QuerySet[Loot], id_order: List[int], @@ -466,14 +476,14 @@ def _get_first_floor_data( """ Simulate handing out the loot for a first floor clear. """ - weeks, prio_brackets, floor_requirements = self._get_floor_data( + weeks, prio_brackets, floor_requirements = LootSolver._get_floor_data( requirements, history, - self.FIRST_FLOOR_SLOTS, + LootSolver.FIRST_FLOOR_SLOTS, id_order, non_loot_gear_obtained, ) - return self._get_handout_data(self.FIRST_FLOOR_SLOTS, floor_requirements, prio_brackets, self.FIRST_FLOOR_TOKENS, weeks, greedy) + return LootSolver._get_handout_data(LootSolver.FIRST_FLOOR_SLOTS, floor_requirements, prio_brackets, LootSolver.FIRST_FLOOR_TOKENS, weeks, greedy) @staticmethod def _get_second_floor_data( @@ -495,8 +505,8 @@ def _get_second_floor_data( ) return LootSolver._get_handout_data(LootSolver.SECOND_FLOOR_SLOTS, floor_requirements, prio_brackets, LootSolver.SECOND_FLOOR_TOKENS, weeks, greedy) + @staticmethod def _get_third_floor_data( - self, requirements: Requirements, history: QuerySet[Loot], id_order: List[int], @@ -506,16 +516,17 @@ def _get_third_floor_data( """ Simulate handing out the loot for a third floor clear. """ - weeks, prio_brackets, floor_requirements = self._get_floor_data( + weeks, prio_brackets, floor_requirements = LootSolver._get_floor_data( requirements, history, - self.THIRD_FLOOR_SLOTS, + LootSolver.THIRD_FLOOR_SLOTS, id_order, non_loot_gear_obtained, ) - return self._get_handout_data(self.THIRD_FLOOR_SLOTS, floor_requirements, prio_brackets, self.THIRD_FLOOR_TOKENS, weeks, greedy) + return LootSolver._get_handout_data(LootSolver.THIRD_FLOOR_SLOTS, floor_requirements, prio_brackets, LootSolver.THIRD_FLOOR_TOKENS, weeks, greedy) - def _get_fourth_floor_data(self, history: QuerySet[Loot], team_size: int, non_loot_gear_obtained: NonLootGear) -> HandoutData: + @staticmethod + def _get_fourth_floor_data(history: QuerySet[Loot], team_size: int, non_loot_gear_obtained: NonLootGear) -> HandoutData: """ Simulate handing out the loot for a fourth floor clear. Different from how the others are handled, because we just check how many people already have bis weapon, and also how many mounts have been obtained. diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index 0a10300..c4c2608 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -2,4 +2,4 @@ from .celery import app as celery_app -VERSION = '20250108.2' +VERSION = '20250108.3' diff --git a/frontend/.env b/frontend/.env index c19ca02..00891bd 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1 @@ -VUE_APP_VERSION="20250108.2" +VUE_APP_VERSION="20250108.3" diff --git a/frontend/src/components/modals/changelog.vue b/frontend/src/components/modals/changelog.vue index 745a4f5..160b6c4 100644 --- a/frontend/src/components/modals/changelog.vue +++ b/frontend/src/components/modals/changelog.vue @@ -12,8 +12,14 @@
Improved Loot Solver algorithm to handle newfound edgecase.
+Also fixed silly bug in the code that was introduced when fixing the previous Loot Solver bug!
+ +Quick extra push today to deliver a bugfix to a long hidden bug during Team deletion. Sorry for the inconvenience but thank you for catching it!
+Thankfully it wasn't an issue for deleting anything, just caused an error at the end of the endpoint call!