Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/api/management/commands/save_all_bis_lists.py
Original file line number Diff line number Diff line change
@@ -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()
27 changes: 27 additions & 0 deletions backend/api/models/bis_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
98 changes: 98 additions & 0 deletions backend/api/tests/test_bis_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
59 changes: 59 additions & 0 deletions backend/api/tests/test_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion backend/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from .celery import app as celery_app

VERSION = '20250417'
VERSION = '20250506'
2 changes: 1 addition & 1 deletion frontend/.env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VUE_APP_VERSION="20250417"
VUE_APP_VERSION="20250506"
3 changes: 3 additions & 0 deletions frontend/src/components/loot/solver.vue
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@
</tr>
</tbody>
</table>

<br/>
<p class="has-text-warning has-text-centered" v-if="!shouldShowAssignButton">If you would like to auto-fill the Loot for a fight, try enabling the "Per Fight Loot Manager" in your <router-link to="/settings/">Settings page!</router-link></p>
</div>
</div>
</template>
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/modals/changelog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
<h2 class="has-text-primary subtitle">{{ version }}</h2>
<div class="divider"><i class="material-icons icon">expand_more</i> Quality of Life <i class="material-icons icon">expand_more</i></div>
<p>
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.
<ul><li>Thanks toonie for the suggestion!</li></ul>
BIS List updates will now automatically swap the current ring slots if, for example, the current left ring matches the bis right ring.
<ul>
<li>This should hopefully make the Loot Solver / Manager break less often.</li>
<li>This deployment has also re-saved every BIS List in the system to automatically update the lists for everyone. (I really hope this hasn't broken anything...)</li>
<li>Thanks toonie for the suggestion!</li>
</ul>
</p>

<div class="divider"><i class="material-icons icon">expand_more</i> Fixes <i class="material-icons icon">expand_more</i></div>
<p>Improved error handling in the LootSolver when the Team's Tier mismatched with the BIS Lists in it.</p>
<p>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.</p>
</div>
</div>
</template>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading