diff --git a/backend/api/management/commands/save_all_bis_lists.py b/backend/api/management/commands/save_all_bis_lists.py new file mode 100644 index 0000000..657d8f7 --- /dev/null +++ b/backend/api/management/commands/save_all_bis_lists.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand +from api.models import BISList + + +class Command(BaseCommand): + help = 'Call .save() on all BISLists to handle the ring swapping.' + + def handle(self, *args, **options): + for obj in BISList.objects.all(): + obj.save() diff --git a/backend/api/models/bis_list.py b/backend/api/models/bis_list.py index b81b68b..2c34287 100644 --- a/backend/api/models/bis_list.py +++ b/backend/api/models/bis_list.py @@ -7,6 +7,7 @@ import auto_prefetch from django.db import models from django.db.models import Q +from django.dispatch import receiver class BISList(auto_prefetch.Model): @@ -181,3 +182,29 @@ def needs_armour_augments(gear_name: str) -> models.QuerySet: | (Q(bis_legs__name=gear_name) & ~Q(current_legs__name=gear_name)) | (Q(bis_feet__name=gear_name) & ~Q(current_feet__name=gear_name)), ) + + +@receiver(models.signals.pre_save, sender=BISList) +def bis_list_ring_swap(sender, instance: BISList, *args, **kwargs): + """ + Check if either of the current_ring slots on the to-be-saved BIS List need to be swapped. + Swapping is required if one of the bis slots matches the opposite current slot and doesn't match its own. + """ + needs_swapping = False + if ( + instance.bis_right_ring_id != instance.current_right_ring_id + and instance.bis_right_ring_id == instance.current_left_ring_id + ): + needs_swapping = True + + if ( + instance.bis_left_ring_id != instance.current_left_ring_id + and instance.bis_left_ring_id == instance.current_right_ring_id + ): + needs_swapping = True + + if not needs_swapping: + return + + # Swap the current rings to make it work + instance.current_left_ring, instance.current_right_ring = instance.current_right_ring, instance.current_left_ring diff --git a/backend/api/tests/test_bis_list.py b/backend/api/tests/test_bis_list.py index 938d4dd..b8209ba 100644 --- a/backend/api/tests/test_bis_list.py +++ b/backend/api/tests/test_bis_list.py @@ -245,6 +245,51 @@ def test_create_with_sync(self): self.assertEqual(sync_bis.current_body_id, self.gear_id_map['Radiant Host']) self.assertEqual(non_sync_bis.current_body_id, self.gear_id_map['Moonward']) + def test_create_with_ring_swap(self): + """ + Create a new BIS List for the character + """ + url = reverse('api:bis_collection', kwargs={'character_id': self.char.pk}) + self.client.force_authenticate(self.char.user) + + # Try one with PLD first + data = { + 'job_id': 'PLD', + 'bis_mainhand_id': self.gear_id_map['Augmented Historia'], + 'bis_offhand_id': self.gear_id_map['Augmented Historia'], + 'bis_head_id': self.gear_id_map['Babyface Champion'], + 'bis_body_id': self.gear_id_map['Augmented Historia'], + 'bis_hands_id': self.gear_id_map['Babyface Champion'], + 'bis_legs_id': self.gear_id_map['Babyface Champion'], + 'bis_feet_id': self.gear_id_map['Babyface Champion'], + 'bis_earrings_id': self.gear_id_map['Augmented Historia'], + 'bis_necklace_id': self.gear_id_map['Augmented Historia'], + 'bis_bracelet_id': self.gear_id_map['Augmented Historia'], + 'bis_right_ring_id': self.gear_id_map['Augmented Historia'], + 'bis_left_ring_id': self.gear_id_map['Babyface Champion'], + 'current_mainhand_id': self.gear_id_map['Ceremonial'], + 'current_offhand_id': self.gear_id_map['Ceremonial'], + 'current_head_id': self.gear_id_map['Ceremonial'], + 'current_body_id': self.gear_id_map['Ceremonial'], + 'current_hands_id': self.gear_id_map['Ceremonial'], + 'current_legs_id': self.gear_id_map['Ceremonial'], + 'current_feet_id': self.gear_id_map['Ceremonial'], + 'current_earrings_id': self.gear_id_map['Ceremonial'], + 'current_necklace_id': self.gear_id_map['Ceremonial'], + 'current_bracelet_id': self.gear_id_map['Ceremonial'], + 'current_right_ring_id': self.gear_id_map['Babyface Champion'], + 'current_left_ring_id': self.gear_id_map['Ceremonial'], + 'external_link': '', + 'name': 'Swap the Rings', + } + + response = self.client.post(url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.content) + self.assertEqual(BISList.objects.count(), 1) + obj = BISList.objects.first() + # Ensure that the request to create has swapped the rings to the correct location + self.assertEqual(obj.bis_left_ring_id, obj.current_left_ring_id) + def test_404(self): """ Test all situations where the endpoint would respond with a 404; @@ -429,6 +474,59 @@ def test_update_400(self): self.assertEqual(content['current_mainhand_id'], [invalid_gear]) self.assertEqual(content['name'], ['Ensure this field has no more than 64 characters.']) + def test_update_with_ring_swap(self): + """ + Update the existing BIS List with a PUT request + """ + url = reverse('api:bis_resource', kwargs={'character_id': self.char.pk, 'pk': self.bis.pk}) + self.client.force_authenticate(self.char.user) + + # Get modern gear for the update + tome_gear = Gear.objects.get(name='Augmented Historia').pk + raid_gear = Gear.objects.get(name='Babyface Champion', has_weapon=False).pk + crafted_gear = Gear.objects.get(name='Ceremonial').pk + + # Send an update request with the rings swapped, ensure they are bis when the request is finished + data = { + 'job_id': 'PLD', + 'bis_mainhand_id': tome_gear, + 'bis_offhand_id': tome_gear, + 'bis_head_id': raid_gear, + 'bis_body_id': tome_gear, + 'bis_hands_id': raid_gear, + 'bis_legs_id': raid_gear, + 'bis_feet_id': raid_gear, + 'bis_earrings_id': tome_gear, + 'bis_necklace_id': tome_gear, + 'bis_bracelet_id': tome_gear, + 'bis_right_ring_id': tome_gear, + 'bis_left_ring_id': raid_gear, + 'current_mainhand_id': crafted_gear, + 'current_offhand_id': crafted_gear, + 'current_head_id': crafted_gear, + 'current_body_id': crafted_gear, + 'current_hands_id': crafted_gear, + 'current_legs_id': crafted_gear, + 'current_feet_id': crafted_gear, + 'current_earrings_id': crafted_gear, + 'current_necklace_id': crafted_gear, + 'current_bracelet_id': crafted_gear, + 'current_right_ring_id': raid_gear, + 'current_left_ring_id': tome_gear, + 'external_link': None, + 'name': 'Update ring swap c:', + } + + response = self.client.put(url, data) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.content) + + # Assert the rings are correct + self.bis.refresh_from_db() + self.assertEqual(self.bis.bis_right_ring_id, tome_gear) + self.assertEqual(self.bis.bis_left_ring_id, raid_gear) + self.assertEqual(self.bis.bis_right_ring_id, self.bis.current_right_ring_id) + self.assertEqual(self.bis.bis_left_ring_id, self.bis.current_left_ring_id) + def test_404(self): """ Test all situations where the endpoint would respond with a 404; diff --git a/backend/api/tests/test_management.py b/backend/api/tests/test_management.py index 0ad5677..ef7e065 100644 --- a/backend/api/tests/test_management.py +++ b/backend/api/tests/test_management.py @@ -132,3 +132,62 @@ def test_notification_setup(self): self.assertTrue('verify_success' in settings.notifications) self.assertTrue(settings.notifications['verify_success']) self.assertFalse(settings.notifications['verify_fail']) + + def test_save_bis_list_ring_swap(self): + """ + Create a BISList with rings swapped, then run the management command + """ + call_command('seed', stdout=StringIO()) + char = models.Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Team Lead', + verified=True, + world='Lich', + ) + + # Next, create two BIS lists for each character + raid_weapon = models.Gear.objects.get(item_level=605, name='Asphodelos') + raid_gear = models.Gear.objects.get(item_level=600, has_weapon=False) + tome_gear = models.Gear.objects.get(item_level=600, has_weapon=True) + crafted = models.Gear.objects.get(name='Classical') + bis = models.BISList.objects.create( + bis_body=raid_gear, + bis_bracelet=raid_gear, + bis_earrings=raid_gear, + bis_feet=raid_gear, + bis_hands=tome_gear, + bis_head=tome_gear, + bis_legs=tome_gear, + bis_mainhand=raid_weapon, + bis_necklace=tome_gear, + bis_offhand=raid_weapon, + bis_left_ring=tome_gear, + bis_right_ring=raid_gear, + current_body=crafted, + current_bracelet=crafted, + current_earrings=crafted, + current_feet=crafted, + current_hands=crafted, + current_head=crafted, + current_legs=crafted, + current_mainhand=crafted, + current_necklace=crafted, + current_offhand=crafted, + current_left_ring=crafted, + current_right_ring=tome_gear, + job_id='SGE', + owner=char, + ) + models.BISList.objects.update( + current_right_ring_id=tome_gear.id, + current_left_ring_id=crafted.id, + ) + bis.refresh_from_db() + self.assertNotEqual(bis.current_right_ring_id, crafted.id) + self.assertNotEqual(bis.current_left_ring_id, tome_gear.id) + call_command('save_all_bis_lists') + bis.refresh_from_db() + self.assertEqual(bis.current_right_ring_id, crafted.id) + self.assertEqual(bis.current_left_ring_id, tome_gear.id) diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index 88e86fc..4b3f510 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -2,4 +2,4 @@ from .celery import app as celery_app -VERSION = '20250417' +VERSION = '20250506' diff --git a/frontend/.env b/frontend/.env index 7586fb2..a99a58f 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1 @@ -VUE_APP_VERSION="20250417" +VUE_APP_VERSION="20250506" diff --git a/frontend/src/components/loot/solver.vue b/frontend/src/components/loot/solver.vue index 33ea029..7998a82 100644 --- a/frontend/src/components/loot/solver.vue +++ b/frontend/src/components/loot/solver.vue @@ -203,6 +203,9 @@ + +
+

If you would like to auto-fill the Loot for a fight, try enabling the "Per Fight Loot Manager" in your Settings page!

diff --git a/frontend/src/components/modals/changelog.vue b/frontend/src/components/modals/changelog.vue index b4791c5..0fd67b9 100644 --- a/frontend/src/components/modals/changelog.vue +++ b/frontend/src/components/modals/changelog.vue @@ -14,12 +14,14 @@

{{ version }}

expand_more Quality of Life expand_more

- Team Leaders can now also send Lodestone Update requests via the Team Page for other Team Members, in case people are forgetting to update their BIS. -

+ BIS List updates will now automatically swap the current ring slots if, for example, the current left ring matches the bis right ring. +

- -
expand_more Fixes expand_more
-

Improved error handling in the LootSolver when the Team's Tier mismatched with the BIS Lists in it.

+

Added a message to the Loot Solver that links to the Settings page if you are not using the Per-Fight Loot Manager, just in case you want to use auto-assign but don't know how.

diff --git a/frontend/src/main.ts b/frontend/src/main.ts index c7c48d4..66612ca 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -30,7 +30,7 @@ Sentry.init({ Vue, dsn: 'https://06f41b525a40497a848fb726f6d03244@o242258.ingest.sentry.io/6180221', logErrors: true, - release: 'savageaim@20250417', + release: 'savageaim@20250506', integrations: [ Sentry.browserTracingIntegration(), Sentry.replayIntegration(),