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
- 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. -
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(),