diff --git a/backend/api/migrations/0019_multi_bis.py b/backend/api/migrations/0019_multi_bis.py new file mode 100644 index 00000000..78eb2bbc --- /dev/null +++ b/backend/api/migrations/0019_multi_bis.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.13 on 2022-05-13 12:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_character_alias'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bislist', + options={'ordering': ['-job__role', 'job__ordering', 'name']}, + ), + migrations.AddField( + model_name='bislist', + name='name', + field=models.CharField(default='', max_length=64), + ), + migrations.AlterUniqueTogether( + name='bislist', + unique_together=set(), + ), + ] diff --git a/backend/api/models/bis_list.py b/backend/api/models/bis_list.py index 87b39bf4..2f5994ff 100644 --- a/backend/api/models/bis_list.py +++ b/backend/api/models/bis_list.py @@ -2,8 +2,6 @@ The main class of the system; Links together characters, jobs and gear into a single list - -Currently is one gear list per character per job for ease, may change later """ from typing import List from django.db import models @@ -38,11 +36,14 @@ class BISList(models.Model): current_right_ring = models.ForeignKey('Gear', on_delete=models.CASCADE, related_name='current_right_ring_set') external_link = models.URLField(null=True) job = models.ForeignKey('Job', on_delete=models.CASCADE) + name = models.CharField(max_length=64, default='') owner = models.ForeignKey('Character', on_delete=models.CASCADE, related_name='bis_lists') class Meta: - unique_together = ['job', 'owner'] - ordering = ['-job__role', 'job__ordering'] + ordering = ['-job__role', 'job__ordering', 'name'] + + def __str__(self) -> str: + return self.display_name def accessory_augments_required(self, gear_name: str) -> int: """ @@ -82,6 +83,24 @@ def _check_augments(self, gear_name: str, slots: List[str]) -> int: needed += 1 return needed + def sync(self, to_sync: 'BISList'): + """ + Given another list, sync the current gear from it to this one and save this one + """ + self.current_mainhand = to_sync.current_mainhand + self.current_offhand = to_sync.current_offhand + self.current_head = to_sync.current_head + self.current_body = to_sync.current_body + self.current_hands = to_sync.current_hands + self.current_legs = to_sync.current_legs + self.current_feet = to_sync.current_feet + self.current_earrings = to_sync.current_earrings + self.current_necklace = to_sync.current_necklace + self.current_bracelet = to_sync.current_bracelet + self.current_left_ring = to_sync.current_left_ring + self.current_right_ring = to_sync.current_right_ring + self.save() + @property def item_level(self): """ @@ -102,6 +121,16 @@ def item_level(self): self.current_right_ring.item_level, ]) / 12 + @property + def display_name(self) -> str: + """ + Same as Character, use the list name if one exists otherwise use the job name + """ + if self.name != '': + return self.name + else: + return self.job.display_name + @staticmethod def needs_accessory_augments(gear_name: str) -> models.QuerySet: """ diff --git a/backend/api/models/character.py b/backend/api/models/character.py index 44604072..fa142f9f 100644 --- a/backend/api/models/character.py +++ b/backend/api/models/character.py @@ -26,7 +26,7 @@ class Character(models.Model): world = models.CharField(max_length=60) def __str__(self) -> str: - return f'{self.name} @ {self.world}' + return self.display_name @staticmethod def generate_token() -> str: diff --git a/backend/api/notifier.py b/backend/api/notifier.py index aa507ce3..dae628fb 100644 --- a/backend/api/notifier.py +++ b/backend/api/notifier.py @@ -35,7 +35,7 @@ def _create_notif(user: User, text: str, link: str, type: str): def loot_tracker_update(bis: models.BISList, team: models.Team): char = bis.owner - text = f'"{char}"\'s {bis.job.id} BIS List was updated via "{team.name}"\'s Loot Tracker!' + text = f'"{char}"\'s BIS List "{bis}" was updated via "{team.name}"\'s Loot Tracker!' link = f'/characters/{char.id}/bis_list/{bis.id}/' user = char.user _create_notif(user, text, link, 'loot_tracker_update') diff --git a/backend/api/serializers/bis_list.py b/backend/api/serializers/bis_list.py index 455310c1..d3f98eea 100644 --- a/backend/api/serializers/bis_list.py +++ b/backend/api/serializers/bis_list.py @@ -26,11 +26,11 @@ def _inner(value: int) -> int: try: obj = Gear.objects.get(pk=value) except Gear.DoesNotExist: - raise serializers.ValidationError('Please ensure your value corresponds with a valid type of Gear.') + raise serializers.ValidationError('Please select a valid type of Gear.') # Ensure valid Gear for the slot if not getattr(obj, f'has_{gear_type}', False): - raise serializers.ValidationError('The chosen category of Gear does not have an item for this slot.') + raise serializers.ValidationError('The chosen type of Gear is invalid for this equipment slot.') return value return _inner @@ -64,6 +64,7 @@ class BISListSerializer(serializers.ModelSerializer): current_right_ring = GearSerializer() item_level = serializers.IntegerField() job = JobSerializer() + display_name = serializers.CharField() class Meta: exclude = ['owner'] @@ -99,6 +100,7 @@ class BISListModifySerializer(serializers.ModelSerializer): current_offhand_id = serializers.IntegerField() current_right_ring_id = serializers.IntegerField(validators=[_validate_gear_type('accessories')]) external_link = serializers.URLField(required=False, allow_null=True, allow_blank=True) + name = serializers.CharField(max_length=64, allow_blank=True) class Meta: model = BISList @@ -129,7 +131,9 @@ class Meta: 'current_offhand_id', 'current_right_ring_id', 'external_link', + 'name', ) + extra_kwargs = {'bis_head_id': {'error_messages': {'invalid': 'Please select a valid type of Gear.'}}} def validate_job_id(self, job_id: str) -> str: """ @@ -141,16 +145,6 @@ def validate_job_id(self, job_id: str) -> str: except Job.DoesNotExist: raise serializers.ValidationError('Please select a valid Job.') - # Ensure that this is the only BISList for the Job - test = BISList.objects.filter(job_id=job_id) - if self.instance is not None: - test = test.filter(owner=self.instance.owner).exclude(pk=self.instance.pk) - else: - test = test.filter(owner=self.context['owner']) - - if test.exists(): - raise serializers.ValidationError('Currently SavageAim only supports one BISList per job.') - # Check the flag if job_id == 'PLD': self.offhand_is_mainhand = False diff --git a/backend/api/serializers/loot.py b/backend/api/serializers/loot.py index 26582814..a0602c1f 100644 --- a/backend/api/serializers/loot.py +++ b/backend/api/serializers/loot.py @@ -92,7 +92,7 @@ def validate_obtained(self, obtained): Ensure we're not recording loot for the future """ if obtained > datetime.today().date(): - raise serializers.ValidationError('Cannot record loot for a date in the future.') + raise serializers.ValidationError('Cannot record Loot for a date in the future.') return obtained diff --git a/backend/api/tasks.py b/backend/api/tasks.py index 8b22a6b8..6014baa1 100644 --- a/backend/api/tasks.py +++ b/backend/api/tasks.py @@ -25,7 +25,7 @@ ) -async def xivapi_lookup(pk: str, token: str, log) -> Optional[str]: +def xivapi_lookup(pk: str, token: str, log) -> Optional[str]: """ Actually check XIVAPI for the specified token being present in the specified character's bio """ @@ -69,9 +69,9 @@ def verify_character(pk: int): return # Call the xivapi function in a sync context - logger.debug('calling async function') - err = async_to_sync(xivapi_lookup)(obj.lodestone_id, obj.token, logger) - logger.debug('finished async function') + logger.debug('calling lookup function') + err = xivapi_lookup(obj.lodestone_id, obj.token, logger) + logger.debug('finished lookup function') if err is not None: notifier.verify_fail(obj, err) diff --git a/backend/api/tests/test_bis_list.py b/backend/api/tests/test_bis_list.py index ba185dc1..81d781e7 100644 --- a/backend/api/tests/test_bis_list.py +++ b/backend/api/tests/test_bis_list.py @@ -73,6 +73,7 @@ def test_create(self): 'current_right_ring_id': self.gear_id_map['Moonward'], 'current_left_ring_id': self.gear_id_map['Moonward'], 'external_link': '', + 'name': 'Hello :)', } response = self.client.post(url, data) @@ -101,7 +102,7 @@ def test_create_400(self): - Gear pk doesn't exist - Gear category is incorrect - Job ID doesn't exist - - Job already has a BIS List + - Name too long - Data missing - External link isn't a url """ @@ -113,7 +114,7 @@ def test_create_400(self): content = response.json() for field in content: self.assertEqual(content[field], ['This field is required.']) - self.assertEqual(len(content), 25) + self.assertEqual(len(content), 26) # All gear errors will be run at once since there's only one actual function to test data = { @@ -124,59 +125,126 @@ def test_create_400(self): 'bis_earrings_id': self.gear_id_map['Divine Light'], 'current_mainhand_id': self.gear_id_map['The Last'], 'external_link': 'abcde', + 'name': 'abcde' * 100, } response = self.client.post(url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) # Since we checked all the "this field is required" errors above, just check the ones we send now content = response.json() - invalid_gear = 'The chosen category of Gear does not have an item for this slot.' + invalid_gear = 'The chosen type of Gear is invalid for this equipment slot.' self.assertEqual(content['job_id'], ['Please select a valid Job.']) self.assertEqual(content['bis_mainhand_id'], ['A valid integer is required.']) - self.assertEqual(content['bis_body_id'], ['Please ensure your value corresponds with a valid type of Gear.']) + self.assertEqual(content['bis_body_id'], ['Please select a valid type of Gear.']) self.assertEqual(content['bis_head_id'], [invalid_gear]) self.assertEqual(content['bis_earrings_id'], [invalid_gear]) self.assertEqual(content['current_mainhand_id'], [invalid_gear]) self.assertEqual(content['external_link'], ['Enter a valid URL.']) + self.assertEqual(content['name'], ['Ensure this field has no more than 64 characters.']) - # Create a BIS List for a job, then send a request to make one for the same job - bis_gear = Gear.objects.first() - curr_gear = Gear.objects.last() - BISList.objects.create( - bis_body=bis_gear, - bis_bracelet=bis_gear, - bis_earrings=bis_gear, - bis_feet=bis_gear, - bis_hands=bis_gear, - bis_head=bis_gear, - bis_left_ring=bis_gear, - bis_legs=bis_gear, - bis_mainhand=bis_gear, - bis_necklace=bis_gear, - bis_offhand=bis_gear, - bis_right_ring=bis_gear, - current_body=curr_gear, - current_bracelet=curr_gear, - current_earrings=curr_gear, - current_feet=curr_gear, - current_hands=curr_gear, - current_head=curr_gear, - current_left_ring=curr_gear, - current_legs=curr_gear, - current_mainhand=curr_gear, - current_necklace=curr_gear, - current_offhand=curr_gear, - current_right_ring=curr_gear, - job_id='DRG', + def test_create_with_sync(self): + """ + Test creating a list and also syncing the gear to another existing list at the same time + """ + self.client.force_authenticate(self.char.user) + + # Create existing BIS Lists; one to sync and one that shouldn't because it's the wrong job + sync_bis = BISList.objects.create( + bis_body_id=self.gear_id_map['Augmented Radiant Host'], + bis_bracelet_id=self.gear_id_map['Augmented Radiant Host'], + bis_earrings_id=self.gear_id_map['Augmented Radiant Host'], + bis_feet_id=self.gear_id_map['Augmented Radiant Host'], + bis_hands_id=self.gear_id_map['Augmented Radiant Host'], + bis_head_id=self.gear_id_map['Augmented Radiant Host'], + bis_left_ring_id=self.gear_id_map['Augmented Radiant Host'], + bis_legs_id=self.gear_id_map['Augmented Radiant Host'], + bis_mainhand_id=self.gear_id_map['Augmented Radiant Host'], + bis_necklace_id=self.gear_id_map['Augmented Radiant Host'], + bis_offhand_id=self.gear_id_map['Augmented Radiant Host'], + bis_right_ring_id=self.gear_id_map['Augmented Radiant Host'], + current_body_id=self.gear_id_map['Moonward'], + current_bracelet_id=self.gear_id_map['Moonward'], + current_earrings_id=self.gear_id_map['Moonward'], + current_feet_id=self.gear_id_map['Moonward'], + current_hands_id=self.gear_id_map['Moonward'], + current_head_id=self.gear_id_map['Moonward'], + current_left_ring_id=self.gear_id_map['Moonward'], + current_legs_id=self.gear_id_map['Moonward'], + current_mainhand_id=self.gear_id_map['Moonward'], + current_necklace_id=self.gear_id_map['Moonward'], + current_offhand_id=self.gear_id_map['Moonward'], + current_right_ring_id=self.gear_id_map['Moonward'], + job_id='PLD', owner=self.char, + external_link='https://etro.gg/', + ) + non_sync_bis = BISList.objects.create( + bis_body_id=self.gear_id_map['Augmented Radiant Host'], + bis_bracelet_id=self.gear_id_map['Augmented Radiant Host'], + bis_earrings_id=self.gear_id_map['Augmented Radiant Host'], + bis_feet_id=self.gear_id_map['Augmented Radiant Host'], + bis_hands_id=self.gear_id_map['Augmented Radiant Host'], + bis_head_id=self.gear_id_map['Augmented Radiant Host'], + bis_left_ring_id=self.gear_id_map['Augmented Radiant Host'], + bis_legs_id=self.gear_id_map['Augmented Radiant Host'], + bis_mainhand_id=self.gear_id_map['Augmented Radiant Host'], + bis_necklace_id=self.gear_id_map['Augmented Radiant Host'], + bis_offhand_id=self.gear_id_map['Augmented Radiant Host'], + bis_right_ring_id=self.gear_id_map['Augmented Radiant Host'], + current_body_id=self.gear_id_map['Moonward'], + current_bracelet_id=self.gear_id_map['Moonward'], + current_earrings_id=self.gear_id_map['Moonward'], + current_feet_id=self.gear_id_map['Moonward'], + current_hands_id=self.gear_id_map['Moonward'], + current_head_id=self.gear_id_map['Moonward'], + current_left_ring_id=self.gear_id_map['Moonward'], + current_legs_id=self.gear_id_map['Moonward'], + current_mainhand_id=self.gear_id_map['Moonward'], + current_necklace_id=self.gear_id_map['Moonward'], + current_offhand_id=self.gear_id_map['Moonward'], + current_right_ring_id=self.gear_id_map['Moonward'], + job_id='DRK', + owner=self.char, + external_link='https://etro.gg/', ) - data = {'job_id': 'DRG'} - response = self.client.post(url, data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) - # Since we checked all the "this field is required" errors above, just check the ones we send now - content = response.json() - self.assertEqual(content['job_id'], ['Currently SavageAim only supports one BISList per job.']) + data = { + 'job_id': 'PLD', + 'bis_mainhand_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_offhand_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_head_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_body_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_hands_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_legs_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_feet_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_earrings_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_necklace_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_bracelet_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_right_ring_id': self.gear_id_map['Augmented Radiant Host'], + 'bis_left_ring_id': self.gear_id_map['Augmented Radiant Host'], + 'current_mainhand_id': self.gear_id_map['Radiant Host'], + 'current_offhand_id': self.gear_id_map['Radiant Host'], + 'current_head_id': self.gear_id_map['Radiant Host'], + 'current_body_id': self.gear_id_map['Radiant Host'], + 'current_hands_id': self.gear_id_map['Radiant Host'], + 'current_legs_id': self.gear_id_map['Radiant Host'], + 'current_feet_id': self.gear_id_map['Radiant Host'], + 'current_earrings_id': self.gear_id_map['Radiant Host'], + 'current_necklace_id': self.gear_id_map['Radiant Host'], + 'current_bracelet_id': self.gear_id_map['Radiant Host'], + 'current_right_ring_id': self.gear_id_map['Radiant Host'], + 'current_left_ring_id': self.gear_id_map['Radiant Host'], + 'external_link': '', + 'name': 'Hello :)', + } + + url = reverse('api:bis_collection', kwargs={'character_id': self.char.pk}) + response = self.client.post(f'{url}?sync={sync_bis.pk}&sync={non_sync_bis.pk}', data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.content) + sync_bis.refresh_from_db() + non_sync_bis.refresh_from_db() + 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_404(self): """ @@ -315,6 +383,7 @@ def test_update(self): 'current_right_ring_id': self.gear_id_map['Moonward'], 'current_left_ring_id': self.gear_id_map['Moonward'], 'external_link': None, + 'name': 'Update c:', } response = self.client.put(url, data) @@ -332,7 +401,7 @@ def test_update_400(self): - Gear pk doesn't exist - Gear category is incorrect - Job ID doesn't exist - - Job already has a BIS List + - Name is too long - Data missing """ url = reverse('api:bis_resource', kwargs={'character_id': self.char.pk, 'pk': self.bis.pk}) @@ -343,7 +412,7 @@ def test_update_400(self): content = response.json() for field in content: self.assertEqual(content[field], ['This field is required.']) - self.assertEqual(len(content), 25) + self.assertEqual(len(content), 26) # All gear errors will be run at once since there's only one actual function to test data = { @@ -353,58 +422,21 @@ def test_update_400(self): 'bis_head_id': self.gear_id_map['Eternal Dark'], 'bis_earrings_id': self.gear_id_map['Divine Light'], 'current_mainhand_id': self.gear_id_map['The Last'], + 'name': 'abcde' * 64, } response = self.client.put(url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) # Since we checked all the "this field is required" errors above, just check the ones we send now content = response.json() - invalid_gear = 'The chosen category of Gear does not have an item for this slot.' + invalid_gear = 'The chosen type of Gear is invalid for this equipment slot.' self.assertEqual(content['job_id'], ['Please select a valid Job.']) self.assertEqual(content['bis_mainhand_id'], ['A valid integer is required.']) - self.assertEqual(content['bis_body_id'], ['Please ensure your value corresponds with a valid type of Gear.']) + self.assertEqual(content['bis_body_id'], ['Please select a valid type of Gear.']) self.assertEqual(content['bis_head_id'], [invalid_gear]) self.assertEqual(content['bis_earrings_id'], [invalid_gear]) self.assertEqual(content['current_mainhand_id'], [invalid_gear]) - - # Create a BIS List for a job, then send a request to make one for the same job - bis_gear = Gear.objects.first() - curr_gear = Gear.objects.last() - BISList.objects.create( - bis_body=bis_gear, - bis_bracelet=bis_gear, - bis_earrings=bis_gear, - bis_feet=bis_gear, - bis_hands=bis_gear, - bis_head=bis_gear, - bis_left_ring=bis_gear, - bis_legs=bis_gear, - bis_mainhand=bis_gear, - bis_necklace=bis_gear, - bis_offhand=bis_gear, - bis_right_ring=bis_gear, - current_body=curr_gear, - current_bracelet=curr_gear, - current_earrings=curr_gear, - current_feet=curr_gear, - current_hands=curr_gear, - current_head=curr_gear, - current_left_ring=curr_gear, - current_legs=curr_gear, - current_mainhand=curr_gear, - current_necklace=curr_gear, - current_offhand=curr_gear, - current_right_ring=curr_gear, - job_id='RPR', - owner=self.char, - ) - - data = {'job_id': 'RPR'} - response = self.client.put(url, data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) - # Since we checked all the "this field is required" errors above, just check the ones we send now - content = response.json() - self.assertEqual(content['job_id'], ['Currently SavageAim only supports one BISList per job.']) + self.assertEqual(content['name'], ['Ensure this field has no more than 64 characters.']) def test_404(self): """ diff --git a/backend/api/tests/test_etro.py b/backend/api/tests/test_etro.py new file mode 100644 index 00000000..42020560 --- /dev/null +++ b/backend/api/tests/test_etro.py @@ -0,0 +1,61 @@ +from io import StringIO +from django.core.management import call_command +from django.urls import reverse +from rest_framework import status +from api.models import Gear +from .test_base import SavageAimTestCase + + +class EtroImport(SavageAimTestCase): + """ + Test the Etro Import view to ensure it works as we expect + + Currently using https://etro.gg/gearset/48e6d8c0-afd8-4857-a320-70528884ac86 for testing + """ + + def setUp(self): + """ + Call the Gear seed command to prepopulate the DB + """ + call_command('gear_seed', stdout=StringIO()) + + def test_import(self): + """ + List Gears from the API, ensure same order as in DB in general + """ + url = reverse('api:etro_import', kwargs={'id': '48e6d8c0-afd8-4857-a320-70528884ac86'}) + user = self._get_user() + self.client.force_authenticate(user) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Build an expected data packet + expected = { + 'job_id': 'DNC', + 'mainhand': Gear.objects.get(name='Ultimate of the Heavens').pk, + 'offhand': Gear.objects.get(name='Ultimate of the Heavens').pk, + 'head': Gear.objects.get(name='Asphodelos', has_armour=True).pk, + 'body': Gear.objects.get(name='Asphodelos', has_armour=True).pk, + 'hands': Gear.objects.get(name='Asphodelos', has_armour=True).pk, + 'earrings': Gear.objects.get(name='Asphodelos', has_armour=True).pk, + 'left_ring': Gear.objects.get(name='Asphodelos', has_armour=True).pk, + 'legs': Gear.objects.get(name='Augmented Radiant Host').pk, + 'feet': Gear.objects.get(name='Augmented Radiant Host').pk, + 'necklace': Gear.objects.get(name='Augmented Radiant Host').pk, + 'bracelet': Gear.objects.get(name='Augmented Radiant Host').pk, + 'right_ring': Gear.objects.get(name='Augmented Radiant Host').pk, + 'min_il': 600, + 'max_il': 605, + } + self.assertDictEqual(response.json(), expected) + + def test_import_400(self): + """ + Send a request with an invalid ID, check we get a proper error + """ + url = reverse('api:etro_import', kwargs={'id': 'abcde'}) + user = self._get_user() + self.client.force_authenticate(user) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()['message'], '404 Not Found') diff --git a/backend/api/tests/test_loot.py b/backend/api/tests/test_loot.py index 8f4e22cb..2a4730fb 100644 --- a/backend/api/tests/test_loot.py +++ b/backend/api/tests/test_loot.py @@ -234,7 +234,7 @@ def setUp(self): 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { @@ -242,7 +242,7 @@ def setUp(self): 'character_name': self.team_lead.alias, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', }, ], @@ -253,16 +253,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, + 'bis_list_name': self.mt_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'whitemage', + 'job_icon_name': 'WHM', 'job_role': 'heal', }, { 'bis_list_id': self.mt_alt_bis2.id, + 'bis_list_name': self.mt_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'dancer', + 'job_icon_name': 'DNC', 'job_role': 'dps', }, ], @@ -273,16 +275,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, + 'bis_list_name': self.tl_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { 'bis_list_id': self.tl_alt_bis2.id, + 'bis_list_name': self.tl_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'reaper', + 'job_icon_name': 'RPR', 'job_role': 'dps', }, ], @@ -296,7 +300,7 @@ def setUp(self): # 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', # 'current_gear_name': self.crafted.name, # 'current_gear_il': self.crafted.item_level, - # 'job_icon_name': 'paladin', + # 'job_icon_name': 'PLD', # 'job_role': 'tank', # }, # ], @@ -312,9 +316,10 @@ def setUp(self): # 'greed_lists': [ # { # 'bis_list_id': self.tl_alt_bis.id, + # 'bis_list_name': self.tl_alt_bis.display_name, # 'current_gear_name': self.crafted.name, # 'current_gear_il': self.crafted.item_level, - # 'job_icon_name': 'paladin', + # 'job_icon_name': 'PLD', # 'job_role': 'tank', # }, # ], @@ -328,7 +333,7 @@ def setUp(self): 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, ], @@ -339,9 +344,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis2.id, + 'bis_list_name': self.mt_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'dancer', + 'job_icon_name': 'DNC', 'job_role': 'dps', }, ], @@ -352,16 +358,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, + 'bis_list_name': self.tl_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { 'bis_list_id': self.tl_alt_bis2.id, + 'bis_list_name': self.tl_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'reaper', + 'job_icon_name': 'RPR', 'job_role': 'dps', }, ], @@ -375,7 +383,7 @@ def setUp(self): 'character_name': self.team_lead.alias, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', }, ], @@ -386,9 +394,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, + 'bis_list_name': self.mt_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'whitemage', + 'job_icon_name': 'WHM', 'job_role': 'heal', }, ], @@ -407,7 +416,7 @@ def setUp(self): 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, ], @@ -418,9 +427,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis2.id, + 'bis_list_name': self.mt_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'dancer', + 'job_icon_name': 'DNC', 'job_role': 'dps', }, ], @@ -431,16 +441,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, + 'bis_list_name': self.tl_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { 'bis_list_id': self.tl_alt_bis2.id, + 'bis_list_name': self.tl_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'reaper', + 'job_icon_name': 'RPR', 'job_role': 'dps', }, ], @@ -454,7 +466,7 @@ def setUp(self): 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, ], @@ -465,9 +477,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis2.id, + 'bis_list_name': self.mt_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'dancer', + 'job_icon_name': 'DNC', 'job_role': 'dps', }, ], @@ -478,16 +491,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, + 'bis_list_name': self.tl_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { 'bis_list_id': self.tl_alt_bis2.id, + 'bis_list_name': self.tl_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'reaper', + 'job_icon_name': 'RPR', 'job_role': 'dps', }, ], @@ -501,7 +516,7 @@ def setUp(self): 'character_name': self.team_lead.alias, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', }, ], @@ -512,9 +527,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, + 'bis_list_name': self.mt_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'whitemage', + 'job_icon_name': 'WHM', 'job_role': 'heal', }, ], @@ -533,7 +549,7 @@ def setUp(self): 'character_name': self.team_lead.alias, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', }, ], @@ -544,9 +560,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, + 'bis_list_name': self.mt_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'whitemage', + 'job_icon_name': 'WHM', 'job_role': 'heal', }, ], @@ -565,7 +582,7 @@ def setUp(self): 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, ], @@ -576,9 +593,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis2.id, + 'bis_list_name': self.mt_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'dancer', + 'job_icon_name': 'DNC', 'job_role': 'dps', }, ], @@ -589,16 +607,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, + 'bis_list_name': self.tl_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { 'bis_list_id': self.tl_alt_bis2.id, + 'bis_list_name': self.tl_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'reaper', + 'job_icon_name': 'RPR', 'job_role': 'dps', }, ], @@ -612,7 +632,7 @@ def setUp(self): 'character_name': self.team_lead.alias, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', }, ], @@ -623,9 +643,10 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, + 'bis_list_name': self.mt_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'whitemage', + 'job_icon_name': 'WHM', 'job_role': 'heal', }, ], @@ -644,7 +665,7 @@ def setUp(self): 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { @@ -652,7 +673,7 @@ def setUp(self): 'character_name': self.team_lead.alias, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', }, ], @@ -663,16 +684,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, + 'bis_list_name': self.mt_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'whitemage', + 'job_icon_name': 'WHM', 'job_role': 'heal', }, { 'bis_list_id': self.mt_alt_bis2.id, + 'bis_list_name': self.mt_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'dancer', + 'job_icon_name': 'DNC', 'job_role': 'dps', }, ], @@ -683,16 +706,18 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, + 'bis_list_name': self.tl_alt_bis.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', }, { 'bis_list_id': self.tl_alt_bis2.id, + 'bis_list_name': self.tl_alt_bis2.display_name, 'current_gear_name': self.crafted.name, 'current_gear_il': self.crafted.item_level, - 'job_icon_name': 'reaper', + 'job_icon_name': 'RPR', 'job_role': 'dps', }, ], @@ -704,14 +729,14 @@ def setUp(self): { 'member_id': self.mt_tm.pk, 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', 'requires': 3, }, { 'member_id': self.tl_tm.pk, 'character_name': self.team_lead.alias, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', 'requires': 2, }, @@ -723,13 +748,15 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, - 'job_icon_name': 'whitemage', + 'bis_list_name': self.mt_alt_bis.display_name, + 'job_icon_name': 'WHM', 'job_role': 'heal', 'requires': 2, }, { 'bis_list_id': self.mt_alt_bis2.id, - 'job_icon_name': 'dancer', + 'bis_list_name': self.mt_alt_bis2.display_name, + 'job_icon_name': 'DNC', 'job_role': 'dps', 'requires': 3, }, @@ -741,13 +768,15 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, - 'job_icon_name': 'paladin', + 'bis_list_name': self.tl_alt_bis.display_name, + 'job_icon_name': 'PLD', 'job_role': 'tank', 'requires': 3, }, { 'bis_list_id': self.tl_alt_bis2.id, - 'job_icon_name': 'reaper', + 'bis_list_name': self.tl_alt_bis2.display_name, + 'job_icon_name': 'RPR', 'job_role': 'dps', 'requires': 3, }, @@ -760,14 +789,14 @@ def setUp(self): { 'member_id': self.mt_tm.pk, 'character_name': f'{self.main_tank.name} @ {self.main_tank.world}', - 'job_icon_name': 'paladin', + 'job_icon_name': 'PLD', 'job_role': 'tank', 'requires': 2, }, { 'member_id': self.tl_tm.pk, 'character_name': self.team_lead.alias, - 'job_icon_name': 'sage', + 'job_icon_name': 'SGE', 'job_role': 'heal', 'requires': 3, }, @@ -779,13 +808,15 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.mt_alt_bis.id, - 'job_icon_name': 'whitemage', + 'bis_list_name': self.mt_alt_bis.display_name, + 'job_icon_name': 'WHM', 'job_role': 'heal', 'requires': 3, }, { 'bis_list_id': self.mt_alt_bis2.id, - 'job_icon_name': 'dancer', + 'bis_list_name': self.mt_alt_bis2.display_name, + 'job_icon_name': 'DNC', 'job_role': 'dps', 'requires': 2, }, @@ -797,13 +828,15 @@ def setUp(self): 'greed_lists': [ { 'bis_list_id': self.tl_alt_bis.id, - 'job_icon_name': 'paladin', + 'bis_list_name': self.tl_alt_bis.display_name, + 'job_icon_name': 'PLD', 'job_role': 'tank', 'requires': 2, }, { 'bis_list_id': self.tl_alt_bis2.id, - 'job_icon_name': 'reaper', + 'bis_list_name': self.tl_alt_bis2.display_name, + 'job_icon_name': 'RPR', 'job_role': 'dps', 'requires': 2, }, @@ -1054,7 +1087,7 @@ def test_create_400(self): Greed not bool: 'Must be a valid boolean.' Obtained not sent: 'This field is required.' Obtained not valid date: 'Date has wrong format. Use one of these formats instead: YYYY-MM-DD.' - Obtained in the future: 'Cannot record loot for a date in the future.' + Obtained in the future: 'Cannot record Loot for a date in the future.' """ url = reverse('api:loot_collection', kwargs={'team_id': self.team.pk}) user = self._get_user() @@ -1089,7 +1122,7 @@ def test_create_400(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) content = response.json() self.assertEqual(content['member_id'], ['Please select a Character that is a member of the Team.']) - self.assertEqual(content['obtained'], ['Cannot record loot for a date in the future.']) + self.assertEqual(content['obtained'], ['Cannot record Loot for a date in the future.']) def test_create_with_bis(self): """ @@ -1206,8 +1239,7 @@ def test_create_with_bis_notification(self): self.assertEqual(notif.link, f'/characters/{self.team_lead.id}/bis_list/{self.tl_main_bis.id}/') self.assertEqual( notif.text, - f'"{self.team_lead}"\'s {self.tl_main_bis.job.id} BIS List was updated via ' - f'"{self.team.name}"\'s Loot Tracker!', + f'"{self.team_lead}"\'s BIS List "{self.tl_main_bis}" was updated via "{self.team.name}"\'s Loot Tracker!', ) self.assertEqual(notif.type, 'loot_tracker_update') self.assertFalse(notif.read) diff --git a/backend/api/tests/test_tasks.py b/backend/api/tests/test_tasks.py new file mode 100644 index 00000000..78579324 --- /dev/null +++ b/backend/api/tests/test_tasks.py @@ -0,0 +1,244 @@ +# stdlib +from datetime import timedelta +from typing import Dict +from unittest.mock import patch +# lib +from django.utils import timezone +# local +from api.models import Character, Notification +from api.tasks import xivapi_lookup, verify_character, cleanup +from .test_base import SavageAimTestCase + + +# Pretend to be a logger +LOGGER = type('logger', (), {'error': lambda msg: msg}) + + +# Mock response functions +def get_desktop_response(url: str, headers: Dict[str, str]): + """ + Return a faked http response object for the desktop site + """ + char_id = url.split('/')[-1] + obj = Character.objects.filter(lodestone_id=char_id).first() + body = f'
{obj.token}
' + return type('response', (), {'status_code': 200, 'content': body}) + + +def get_mobile_response(url: str, headers: Dict[str, str]): + """ + Return a faked http response object for the mobile site + """ + char_id = url.split('/')[-1] + obj = Character.objects.filter(lodestone_id=char_id).first() + body = f'
{obj.token}
' + return type('response', (), {'status_code': 200, 'content': body}) + + +def get_empty_response(url: str, headers: Dict[str, str]): + """ + Return a faked http response object for a site that doesn't contain the token we need + """ + body = '
' + return type('response', (), {'status_code': 200, 'content': body}) + + +def get_error_response(url: str, headers: Dict[str, str]): + """ + Return a faked http response object for a non 200 error + """ + return type('response', (), {'status_code': 400}) + + +class TasksTestSuite(SavageAimTestCase): + """ + Test the functions in the tasks file to make sure they work as intended + + Mock requests to return pre-determined html bodies + """ + + def tearDown(self): + Notification.objects.all().delete() + Character.objects.all().delete() + + def test_cleanup(self): + """ + - Test the cleanup task by creating a couple of different Characters and running the task + - Then ensure that the ones that should have been deleted are erased from the DB + """ + old_unver = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=False, + ) + old_ver = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=11289475, + user=self._get_user(), + name='Char 2', + world='Lich', + verified=True, + ) + # Update all created stamps to 1 week ago + Character.objects.update(created=timezone.now() - timedelta(days=7)) + # Create one last character that's new + new_unver = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=2498174123, + user=self._get_user(), + name='Char 3', + world='Lich', + verified=False, + ) + + # Run the cleanup task and ensure the DB is as it should be + cleanup() + with self.assertRaises(Character.DoesNotExist): + Character.objects.get(pk=old_unver.pk) + self.assertEqual(Character.objects.filter(pk__in=[old_ver.pk, new_unver.pk]).count(), 2) + + @patch('requests.get', side_effect=get_desktop_response) + def test_verify_character(self, mocked_get): + """ + Test a full verification call for a character is successful + + Also ensure that other copies of this character get deleted + """ + char = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=False, + token=Character.generate_token(), + ) + other_version = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._create_user(), + name='Char 2', + world='Lich', + verified=False, + token=Character.generate_token(), + ) + verify_character(char.pk) + + char.refresh_from_db() + self.assertTrue(char.verified) + self.assertEqual(Notification.objects.count(), 1) + with self.assertRaises(Character.DoesNotExist): + Character.objects.get(pk=other_version.pk) + + # Check for Notification + notif = Notification.objects.first() + message = f'The verification of {char} has succeeded!' + self.assertEqual(notif.text, message) + + @patch('requests.get', side_effect=get_error_response) + def test_verify_character_failures(self, mocked_get): + """ + Handle errors in the verification code to ensure it all works as expected + """ + # Start with an already verified character + char = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=True, + token=Character.generate_token(), + ) + verify_character(char.pk) + mocked_get.assert_not_called() + + # Unverify the character and attempt to, then check the notifications for the reason why + char.verified = False + char.save() + verify_character(char.pk) + mocked_get.assert_called_once() + + # Check for Notification + self.assertEqual(Notification.objects.count(), 1) + notif = Notification.objects.first() + error = 'Lodestone may be down.' + message = f'The verification of {char} has failed! Reason: {error}' + self.assertEqual(notif.text, message) + + @patch('requests.get', side_effect=get_desktop_response) + def test_xivapi_lookup_desktop(self, mocked_get): + """ + Check that the desktop search works for beautiful soup + """ + char = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=False, + token=Character.generate_token(), + ) + # Run the function and assert the response is None + self.assertIsNone(xivapi_lookup(char.lodestone_id, char.token, LOGGER)) + + @patch('requests.get', side_effect=get_mobile_response) + def test_xivapi_lookup_mobile(self, mocked_get): + """ + Check that the mobile search works for beautiful soup + """ + char = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=False, + token=Character.generate_token(), + ) + # Run the function and assert the response is None + self.assertIsNone(xivapi_lookup(char.lodestone_id, char.token, LOGGER)) + + @patch('requests.get', side_effect=get_empty_response) + def test_xivapi_lookup_empty(self, mocked_get): + """ + If no token can be found, we need to ensure the correct error message is returned + """ + char = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=False, + token=Character.generate_token(), + ) + # Run the function and assert the response is None + self.assertEqual( + xivapi_lookup(char.lodestone_id, char.token, LOGGER), + 'Could not find the verification code in the Lodestone profile.', + ) + + @patch('requests.get', side_effect=get_error_response) + def test_xivapi_lookup_error(self, mocked_get): + """ + If lodestone gets an error, ensure the right message is returned + """ + char = Character.objects.create( + avatar_url='https://img.savageaim.com/abcde', + lodestone_id=1234567890, + user=self._get_user(), + name='Char 1', + world='Lich', + verified=False, + token=Character.generate_token(), + ) + # Run the function and assert the response is None + self.assertEqual( + xivapi_lookup(char.lodestone_id, char.token, LOGGER), + 'Lodestone may be down.', + ) diff --git a/backend/api/urls.py b/backend/api/urls.py index 8d96ac0b..46003ef0 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -17,6 +17,9 @@ path('gear/', views.GearCollection.as_view(), name='gear_collection'), path('gear/item_levels/', views.ItemLevels.as_view(), name='item_levels'), + # Imports + path('import/etro//', views.EtroImport.as_view(), name='etro_import'), + # Job path('job/', views.JobCollection.as_view(), name='job_collection'), diff --git a/backend/api/views/__init__.py b/backend/api/views/__init__.py index 4275de4d..3cdfdddb 100644 --- a/backend/api/views/__init__.py +++ b/backend/api/views/__init__.py @@ -1,5 +1,6 @@ from .bis_list import BISListCollection, BISListDelete, BISListResource from .character import CharacterCollection, CharacterDelete, CharacterResource, CharacterVerification +from .etro import EtroImport from .gear import GearCollection, ItemLevels from .job import JobCollection from .loot import LootCollection, LootWithBIS @@ -19,6 +20,8 @@ 'CharacterResource', 'CharacterVerification', + 'EtroImport', + 'GearCollection', 'ItemLevels', diff --git a/backend/api/views/bis_list.py b/backend/api/views/bis_list.py index 64d918a0..585f9ce4 100644 --- a/backend/api/views/bis_list.py +++ b/backend/api/views/bis_list.py @@ -3,6 +3,8 @@ Can only create and update from these views, no listing since they are returned with a read character """ +# stdlib +from typing import List # lib from django.db.models.deletion import ProtectedError from rest_framework.request import Request @@ -13,7 +15,23 @@ from api.serializers import BISListSerializer, BISListModifySerializer -class BISListCollection(APIView): +class BISListBaseView(APIView): + """ + Superclass with the ability to sync BIS Lists and report the sync via websockets + """ + + def _sync_lists(self, list: BISList, sync_ids: List[int]): + sync_lists = BISList.objects.filter( + owner=list.owner, + job=list.job, + id__in=sync_ids, + ) + for sync_list in sync_lists: + sync_list.sync(list) + self._send_to_user(sync_list.owner.user, {'type': 'bis', 'char': sync_list.owner.id, 'id': sync_list.pk}) + + +class BISListCollection(BISListBaseView): """ Allows for the creation of new BIS Lists """ @@ -35,11 +53,14 @@ def post(self, request: Request, character_id: int) -> Response: # Send a WS update for BIS self._send_to_user(char.user, {'type': 'bis', 'char': char.id, 'id': serializer.instance.pk}) + # Sync lists, if any requested + self._sync_lists(serializer.instance, request.GET.getlist('sync')) + # Return the id for redirects return Response({'id': serializer.instance.pk}, status=201) -class BISListResource(APIView): +class BISListResource(BISListBaseView): """ Allows for the reading and updating of a BISList """ @@ -84,6 +105,9 @@ def put(self, request: Request, character_id: int, pk: int) -> Response: for tm in obj.teammember_set.all(): self._send_to_team(tm.team, {'type': 'team', 'id': str(tm.team.id)}) + # Sync lists, if any requested + self._sync_lists(serializer.instance, request.GET.getlist('sync')) + return Response(status=204) diff --git a/backend/api/views/etro.py b/backend/api/views/etro.py new file mode 100644 index 00000000..dcf3615e --- /dev/null +++ b/backend/api/views/etro.py @@ -0,0 +1,114 @@ +""" +Given an etro id, convert it into a format that uses Savage Aim ids +""" +# stdlib +from typing import Dict, Set +# lib +import coreapi +import jellyfish +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response +# local +from api.models import Gear + + +# Map names of slots from etro to savage aim +SLOT_MAP = { + 'weapon': 'mainhand', + 'offHand': 'offhand', + 'head': 'head', + 'body': 'body', + 'hands': 'hands', + 'legs': 'legs', + 'feet': 'feet', + 'ears': 'earrings', + 'neck': 'necklace', + 'wrists': 'bracelet', + 'fingerL': 'left_ring', + 'fingerR': 'right_ring', +} +ARMOUR_SLOTS = {'head', 'body', 'hands', 'legs', 'feet'} +ACCESSORY_SLOTS = {'earrings', 'necklace', 'bracelet', 'left_ring', 'right_ring'} + + +class EtroImport(APIView): + """ + Import an etro gearset using coreapi and levenshtein distance + """ + + @staticmethod + def _get_gear_id(gear_selection: Dict[str, str], item_name: str) -> str: + """ + Find the id of the gear piece that matches the name closest + """ + diff = float('inf') + gear_id = None + for details in gear_selection: + curr_diff = jellyfish.levenshtein_distance(details['name'], item_name) + if curr_diff < diff: + diff = curr_diff + gear_id = details['id'] + return gear_id + + def get(self, request: Request, id: str) -> Response: + """ + Return a list of Characters belonging to a certain User + """ + # Instantiate a Client instance for CoreAPI + client = coreapi.Client() + schema = client.get('https://etro.gg/api/docs/') + + # First things first, attempt to read the gearset + try: + response = client.action(schema, ['gearsets', 'read'], params={'id': id}) + except coreapi.exceptions.ErrorMessage as e: + return Response({'message': e.error.title}, status=400) + job_id = response['jobAbbrev'] + + # Loop through each slot of the etro gearset, fetch the name and item level and store it in a dict + gear_details: Dict[str, str] = {} + item_levels: Set[int] = set() + min_il = float('inf') + max_il = float('-inf') + + for etro_slot, sa_slot in SLOT_MAP.items(): + gear_id = response[etro_slot] + if gear_id is None: + continue + item_response = client.action(schema, ['equipment', 'read'], params={'id': gear_id}) + gear_details[sa_slot] = item_response['name'] + + # item level stuff + il = item_response['itemLevel'] + item_levels.add(il) + if il < min_il: + min_il = il + if il > max_il: + max_il = il + + # Get the names of all the gear with the specified Item Levels + gear_names = Gear.objects.filter(item_level__in=item_levels).values('name', 'id') + + response = { + 'job_id': job_id, + } + + # Loop through the slots one final time, and get the gear id for that slot + for slot, item_name in gear_details.items(): + if slot in ARMOUR_SLOTS: + response[slot] = self._get_gear_id(gear_names.filter(has_armour=True), item_name) + elif slot in ACCESSORY_SLOTS: + response[slot] = self._get_gear_id(gear_names.filter(has_accessories=True), item_name) + else: + response[slot] = self._get_gear_id(gear_names.filter(has_weapon=True), item_name) + + # Check for offhand + if job_id != 'PLD': + response['offhand'] = response['mainhand'] + + # Also add item level status + response['min_il'] = min_il + response['max_il'] = max_il + + return Response(response) diff --git a/backend/api/views/loot.py b/backend/api/views/loot.py index 4a0b2962..3c3b7c93 100644 --- a/backend/api/views/loot.py +++ b/backend/api/views/loot.py @@ -65,7 +65,7 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: 'character_name': tm.character.display_name, 'current_gear_name': current_gear.name, 'current_gear_il': current_gear.item_level, - 'job_icon_name': tm.bis_list.job.name, + 'job_icon_name': tm.bis_list.job.id, 'job_role': tm.bis_list.job.role, }) @@ -87,10 +87,11 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: for greed_list in greed_lists: current_gear = getattr(greed_list, f'current_{slot}') data['greed_lists'].append({ + 'bis_list_name': greed_list.display_name, 'bis_list_id': greed_list.id, 'current_gear_name': current_gear.name, 'current_gear_il': current_gear.item_level, - 'job_icon_name': greed_list.job.name, + 'job_icon_name': greed_list.job.id, 'job_role': greed_list.job.role, }) gear[slot]['greed'].append(data) @@ -109,7 +110,7 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: # 'character_name': tm.character.display_name, # 'current_gear_name': current_gear.name, # 'current_gear_il': current_gear.item_level, - # 'job_icon_name': tm.bis_list.job.name, + # 'job_icon_name': tm.bis_list.job.id, # 'job_role': tm.bis_list.job.role, # }) @@ -132,10 +133,11 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: # for greed_list in greed_lists: # current_gear = getattr(greed_list, 'current_offhand') # data['greed_lists'].append({ + # 'bis_list_name': greed_list.display_name, # 'bis_list_id': greed_list.id, # 'current_gear_name': current_gear.name, # 'current_gear_il': current_gear.item_level, - # 'job_icon_name': greed_list.job.name, + # 'job_icon_name': greed_list.job.id, # 'job_role': greed_list.job.role, # }) # gear[slot]['greed'].append(data) @@ -159,7 +161,7 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: 'character_name': tm.character.display_name, 'current_gear_name': current_gear.name, 'current_gear_il': current_gear.item_level, - 'job_icon_name': tm.bis_list.job.name, + 'job_icon_name': tm.bis_list.job.id, 'job_role': tm.bis_list.job.role, }) @@ -188,10 +190,11 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: current_gear = greed_list.current_left_ring data['greed_lists'].append({ + 'bis_list_name': greed_list.display_name, 'bis_list_id': greed_list.id, 'current_gear_name': current_gear.name, 'current_gear_il': current_gear.item_level, - 'job_icon_name': greed_list.job.name, + 'job_icon_name': greed_list.job.id, 'job_role': greed_list.job.role, }) gear[slot]['greed'].append(data) @@ -207,7 +210,7 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: gear[slot]['need'].append({ 'member_id': tm.id, 'character_name': tm.character.display_name, - 'job_icon_name': tm.bis_list.job.name, + 'job_icon_name': tm.bis_list.job.id, 'job_role': tm.bis_list.job.role, 'requires': needs, }) @@ -223,8 +226,9 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: } for greed_list in greed_lists: data['greed_lists'].append({ + 'bis_list_name': greed_list.display_name, 'bis_list_id': greed_list.id, - 'job_icon_name': greed_list.job.name, + 'job_icon_name': greed_list.job.id, 'job_role': greed_list.job.role, 'requires': greed_list.accessory_augments_required(obj.tier.tome_gear_name), }) @@ -240,7 +244,7 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: gear[slot]['need'].append({ 'member_id': tm.id, 'character_name': tm.character.display_name, - 'job_icon_name': tm.bis_list.job.name, + 'job_icon_name': tm.bis_list.job.id, 'job_role': tm.bis_list.job.role, 'requires': needs, }) @@ -256,8 +260,9 @@ def _get_gear_data(self, obj: Team) -> Dict[str, List[Dict[str, str]]]: } for greed_list in greed_lists: data['greed_lists'].append({ + 'bis_list_name': greed_list.display_name, 'bis_list_id': greed_list.id, - 'job_icon_name': greed_list.job.name, + 'job_icon_name': greed_list.job.id, 'job_role': greed_list.job.role, 'requires': greed_list.armour_augments_required(obj.tier.tome_gear_name), }) diff --git a/backend/backend/settings_live.py b/backend/backend/settings_live.py index ad9f189a..b11ccfdf 100644 --- a/backend/backend/settings_live.py +++ b/backend/backend/settings_live.py @@ -178,7 +178,7 @@ def sampler(context): # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. send_default_pii=True, - release='savageaim@20220511', + release='savageaim@20220516', ) # Channels diff --git a/backend/requirements.txt b/backend/requirements.txt index 01e46777..9d90e2c0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,32 +1,53 @@ +aioredis==1.3.1 amqp==5.0.9 asgiref==3.4.1 +async-timeout==4.0.2 +attrs==21.4.0 +autobahn==22.4.2 +Automat==20.2.0 beautifulsoup4==4.10.0 billiard==3.6.4.0 celery==5.2.3 certifi==2021.10.8 cffi==1.15.0 -channels -channels_redis +channels==3.0.4 +channels-redis==3.4.0 charset-normalizer==2.0.9 click==8.0.3 click-didyoumean==0.3.0 click-plugins==1.1.1 click-repl==0.2.0 +constantly==15.1.0 +coreapi==2.3.3 +coreschema==0.0.4 cryptography==36.0.1 +daphne==3.0.2 defusedxml==0.7.1 Deprecated==1.2.13 Django==3.2.13 django-allauth==0.47.0 django-rest-framework==0.1.0 djangorestframework==3.13.1 +gunicorn==20.1.0 +hiredis==2.0.0 +hyperlink==21.0.0 idna==3.3 +incremental==21.3.0 +itypes==1.2.0 +jellyfish==0.9.0 +Jinja2==3.1.2 kombu==5.2.3 +MarkupSafe==2.1.1 +msgpack==1.0.3 oauthlib==3.1.1 packaging==21.3 prompt-toolkit==3.0.24 psycopg2-binary==2.9.3 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 pycparser==2.21 PyJWT==2.3.0 +pyOpenSSL==22.0.0 pyparsing==3.0.6 python3-openid==3.2.0 pytz==2021.3 @@ -34,10 +55,16 @@ redis==4.1.0 requests==2.26.0 requests-oauthlib==1.3.0 sentry-sdk==1.5.3 +service-identity==21.1.0 six==1.16.0 soupsieve==2.3.1 sqlparse==0.4.2 +Twisted==22.4.0 +txaio==22.2.1 +typing_extensions==4.2.0 +uritemplate==4.1.1 urllib3==1.26.7 vine==5.0.0 wcwidth==0.2.5 wrapt==1.13.3 +zope.interface==5.4.0 diff --git a/frontend/.env b/frontend/.env index eee56f1a..78476dd4 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1 @@ -VUE_APP_VERSION="20220511" +VUE_APP_VERSION="20220516" diff --git a/frontend/public/job_icons/astrologian.png b/frontend/public/job_icons/AST.png similarity index 100% rename from frontend/public/job_icons/astrologian.png rename to frontend/public/job_icons/AST.png diff --git a/frontend/public/job_icons/blackmage.png b/frontend/public/job_icons/BLM.png similarity index 100% rename from frontend/public/job_icons/blackmage.png rename to frontend/public/job_icons/BLM.png diff --git a/frontend/public/job_icons/bard.png b/frontend/public/job_icons/BRD.png similarity index 100% rename from frontend/public/job_icons/bard.png rename to frontend/public/job_icons/BRD.png diff --git a/frontend/public/job_icons/dancer.png b/frontend/public/job_icons/DNC.png similarity index 100% rename from frontend/public/job_icons/dancer.png rename to frontend/public/job_icons/DNC.png diff --git a/frontend/public/job_icons/dragoon.png b/frontend/public/job_icons/DRG.png similarity index 100% rename from frontend/public/job_icons/dragoon.png rename to frontend/public/job_icons/DRG.png diff --git a/frontend/public/job_icons/darkknight.png b/frontend/public/job_icons/DRK.png similarity index 100% rename from frontend/public/job_icons/darkknight.png rename to frontend/public/job_icons/DRK.png diff --git a/frontend/public/job_icons/gunbreaker.png b/frontend/public/job_icons/GNB.png similarity index 100% rename from frontend/public/job_icons/gunbreaker.png rename to frontend/public/job_icons/GNB.png diff --git a/frontend/public/job_icons/machinist.png b/frontend/public/job_icons/MCH.png similarity index 100% rename from frontend/public/job_icons/machinist.png rename to frontend/public/job_icons/MCH.png diff --git a/frontend/public/job_icons/monk.png b/frontend/public/job_icons/MNK.png similarity index 100% rename from frontend/public/job_icons/monk.png rename to frontend/public/job_icons/MNK.png diff --git a/frontend/public/job_icons/ninja.png b/frontend/public/job_icons/NIN.png similarity index 100% rename from frontend/public/job_icons/ninja.png rename to frontend/public/job_icons/NIN.png diff --git a/frontend/public/job_icons/paladin.png b/frontend/public/job_icons/PLD.png similarity index 100% rename from frontend/public/job_icons/paladin.png rename to frontend/public/job_icons/PLD.png diff --git a/frontend/public/job_icons/redmage.png b/frontend/public/job_icons/RDM.png similarity index 100% rename from frontend/public/job_icons/redmage.png rename to frontend/public/job_icons/RDM.png diff --git a/frontend/public/job_icons/reaper.png b/frontend/public/job_icons/RPR.png similarity index 100% rename from frontend/public/job_icons/reaper.png rename to frontend/public/job_icons/RPR.png diff --git a/frontend/public/job_icons/samurai.png b/frontend/public/job_icons/SAM.png similarity index 100% rename from frontend/public/job_icons/samurai.png rename to frontend/public/job_icons/SAM.png diff --git a/frontend/public/job_icons/scholar.png b/frontend/public/job_icons/SCH.png similarity index 100% rename from frontend/public/job_icons/scholar.png rename to frontend/public/job_icons/SCH.png diff --git a/frontend/public/job_icons/sage.png b/frontend/public/job_icons/SGE.png similarity index 100% rename from frontend/public/job_icons/sage.png rename to frontend/public/job_icons/SGE.png diff --git a/frontend/public/job_icons/summoner.png b/frontend/public/job_icons/SMN.png similarity index 100% rename from frontend/public/job_icons/summoner.png rename to frontend/public/job_icons/SMN.png diff --git a/frontend/public/job_icons/warrior.png b/frontend/public/job_icons/WAR.png similarity index 100% rename from frontend/public/job_icons/warrior.png rename to frontend/public/job_icons/WAR.png diff --git a/frontend/public/job_icons/whitemage.png b/frontend/public/job_icons/WHM.png similarity index 100% rename from frontend/public/job_icons/whitemage.png rename to frontend/public/job_icons/WHM.png diff --git a/frontend/src/components/bis_list/actions.vue b/frontend/src/components/bis_list/actions.vue new file mode 100644 index 00000000..0dd8ae98 --- /dev/null +++ b/frontend/src/components/bis_list/actions.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/frontend/src/components/bis_list/bis.vue b/frontend/src/components/bis_list/bis.vue new file mode 100644 index 00000000..cf0ff91e --- /dev/null +++ b/frontend/src/components/bis_list/bis.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/components/bis_list/current.vue b/frontend/src/components/bis_list/current.vue new file mode 100644 index 00000000..d8393513 --- /dev/null +++ b/frontend/src/components/bis_list/current.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/components/bis_list/desktop_form.vue b/frontend/src/components/bis_list/desktop_form.vue new file mode 100644 index 00000000..28843de4 --- /dev/null +++ b/frontend/src/components/bis_list/desktop_form.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/frontend/src/components/bis_list/details.vue b/frontend/src/components/bis_list/details.vue new file mode 100644 index 00000000..f17ab2e9 --- /dev/null +++ b/frontend/src/components/bis_list/details.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/src/components/bis_list/filters.vue b/frontend/src/components/bis_list/filters.vue new file mode 100644 index 00000000..29b4688a --- /dev/null +++ b/frontend/src/components/bis_list/filters.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/src/components/bis_list/form.vue b/frontend/src/components/bis_list/form.vue new file mode 100644 index 00000000..190cfc57 --- /dev/null +++ b/frontend/src/components/bis_list/form.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/frontend/src/components/bis_list/mobile_form.vue b/frontend/src/components/bis_list/mobile_form.vue new file mode 100644 index 00000000..2787166b --- /dev/null +++ b/frontend/src/components/bis_list/mobile_form.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/frontend/src/components/bis_list_form.vue b/frontend/src/components/bis_list_form.vue deleted file mode 100644 index 3eba70b3..00000000 --- a/frontend/src/components/bis_list_form.vue +++ /dev/null @@ -1,500 +0,0 @@ - - - - - diff --git a/frontend/src/components/loot/greed_character_entry.vue b/frontend/src/components/loot/greed_character_entry.vue new file mode 100644 index 00000000..4c583b1b --- /dev/null +++ b/frontend/src/components/loot/greed_character_entry.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/frontend/src/components/loot/greed_raid_item_box.vue b/frontend/src/components/loot/greed_raid_item_box.vue deleted file mode 100644 index 120a8f8c..00000000 --- a/frontend/src/components/loot/greed_raid_item_box.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - - - diff --git a/frontend/src/components/loot/greed_raid_modal.vue b/frontend/src/components/loot/greed_raid_modal.vue new file mode 100644 index 00000000..5e54ae1a --- /dev/null +++ b/frontend/src/components/loot/greed_raid_modal.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/frontend/src/components/loot/greed_tome_item_box.vue b/frontend/src/components/loot/greed_tome_item_box.vue deleted file mode 100644 index 07a5af41..00000000 --- a/frontend/src/components/loot/greed_tome_item_box.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - diff --git a/frontend/src/components/loot/greed_tome_modal.vue b/frontend/src/components/loot/greed_tome_modal.vue new file mode 100644 index 00000000..dbd8ad57 --- /dev/null +++ b/frontend/src/components/loot/greed_tome_modal.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/frontend/src/components/loot/history.vue b/frontend/src/components/loot/history.vue new file mode 100644 index 00000000..b9679174 --- /dev/null +++ b/frontend/src/components/loot/history.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/frontend/src/components/modals/add_bis.vue b/frontend/src/components/modals/add_bis.vue index ac0a581b..64684f50 100644 --- a/frontend/src/components/modals/add_bis.vue +++ b/frontend/src/components/modals/add_bis.vue @@ -11,15 +11,14 @@
- - +
diff --git a/frontend/src/components/modals/changelog.vue b/frontend/src/components/modals/changelog.vue index 2491a01f..54c52fc0 100644 --- a/frontend/src/components/modals/changelog.vue +++ b/frontend/src/components/modals/changelog.vue @@ -12,9 +12,33 @@

{{ version }}

-
expand_more FFXIV 6.1.1 expand_more
+
expand_more BIS Lists Expansion Update expand_more
+

The limit of one BIS List per Job has finally been removed! You can now have as many BIS Lists as you want per Job, allowing you to keep historical lists as you advance the tiers, or manage two current lists per Job for the current Tier, the options are endless!

+

You can also give each list a name, to help differentiate different BIS Lists of the same Job. The name will default to the name of the Job.

- Added the following Gear for the release of 6.1.1; + Syncing Current Gear between BIS Lists of the same Job is also possible, in both directions. +

+

+

+ It is also now possible to import BIS Gear from Etro Gearsets by filling in the Extra URL of a BIS List. +

+

+

To help prevent over-population of pages, the Greed section of the Loot Manager page now uses popups to assign loot to a Greed BIS List instead of displaying all of the Lists for each Character directly on the page itself.

+ +
expand_more Minor Changes expand_more
+

Minor changes to the display of breadcrumbs in the BIS Create / Edit pages.

+

Job icons on Team boxes now have a tooltip with the name / alias of the associated Character.

+

Minor design improvements in the Join a Team page.

+

The forms in the Loot History section of the Loot Manager page have been moved to the top of their related sections to prevent large amounts of unnecessary scrolling as History tables fill up.

+ +
expand_more Database Additions expand_more
+

+ Added the following Gear for the release of 6.11;

diff --git a/frontend/src/components/modals/confirmations/delete_bis.vue b/frontend/src/components/modals/confirmations/delete_bis.vue index 9db44cb0..dcfd70cc 100644 --- a/frontend/src/components/modals/confirmations/delete_bis.vue +++ b/frontend/src/components/modals/confirmations/delete_bis.vue @@ -15,7 +15,7 @@
- {{ bis.job.display_name }} + {{ bis.display_name }}
@@ -31,7 +31,7 @@
- +
@@ -129,7 +129,7 @@ export default class DeleteBIS extends Vue { if (response.ok) { // Attempt to parse the json, get the id, and then redirect this.$emit('close') - this.$notify({ text: `${this.bis.job.id} BIS deleted successfully!`, type: 'is-success' }) + this.$notify({ text: `${this.bis.display_name} deleted successfully!`, type: 'is-success' }) } else { this.$notify({ text: `Unexpected response ${response.status} when attempting to delete BIS List.`, type: 'is-danger' }) diff --git a/frontend/src/components/modals/confirmations/kick_from_team.vue b/frontend/src/components/modals/confirmations/kick_from_team.vue index 05d3d89e..473f74f7 100644 --- a/frontend/src/components/modals/confirmations/kick_from_team.vue +++ b/frontend/src/components/modals/confirmations/kick_from_team.vue @@ -30,7 +30,7 @@
- +
diff --git a/frontend/src/components/modals/confirmations/leave_team.vue b/frontend/src/components/modals/confirmations/leave_team.vue index d71e4b59..a60abaef 100644 --- a/frontend/src/components/modals/confirmations/leave_team.vue +++ b/frontend/src/components/modals/confirmations/leave_team.vue @@ -31,7 +31,7 @@
- +
diff --git a/frontend/src/components/modals/load_current_gear.vue b/frontend/src/components/modals/load_current_gear.vue new file mode 100644 index 00000000..b8e33cf7 --- /dev/null +++ b/frontend/src/components/modals/load_current_gear.vue @@ -0,0 +1,59 @@ + + + diff --git a/frontend/src/components/modals/sync_current_gear.vue b/frontend/src/components/modals/sync_current_gear.vue new file mode 100644 index 00000000..6bfbcec2 --- /dev/null +++ b/frontend/src/components/modals/sync_current_gear.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/team_bio.vue b/frontend/src/components/team_bio.vue index da85c2c3..d2b0513c 100644 --- a/frontend/src/components/team_bio.vue +++ b/frontend/src/components/team_bio.vue @@ -16,7 +16,11 @@
-
+
+ + + +
diff --git a/frontend/src/components/team_member_card.vue b/frontend/src/components/team_member_card.vue index 3d50c1af..85e3f9f7 100644 --- a/frontend/src/components/team_member_card.vue +++ b/frontend/src/components/team_member_card.vue @@ -19,7 +19,7 @@ - + diff --git a/frontend/src/components/team_member_form.vue b/frontend/src/components/team_member_form.vue index 0aa4d805..461dccc8 100644 --- a/frontend/src/components/team_member_form.vue +++ b/frontend/src/components/team_member_form.vue @@ -30,7 +30,7 @@
@@ -88,7 +88,7 @@ export default class TeamMemberForm extends SavageAimMixin { get bisIcon(): string { const list = this.bisLists.find((bl: BISList) => bl.id === parseInt(this.bisListId, 10)) if (list != null) { - return list.job.name + return list.job.id } return '' } diff --git a/frontend/src/dataclasses/bis_list_modify.ts b/frontend/src/dataclasses/bis_list_modify.ts index f98b6211..1d01f375 100644 --- a/frontend/src/dataclasses/bis_list_modify.ts +++ b/frontend/src/dataclasses/bis_list_modify.ts @@ -1,10 +1,11 @@ // Slight variation on bis_list that is used for sending create / update requests // Done as a class instead of an interface to allow for functions and such import BISList from '@/interfaces/bis_list' +import { ImportResponse } from '@/interfaces/imports' export default class BISListModify { public id = -1 - public job_id = 'na' + public job_id = 'PLD' public bis_body_id = -1 public bis_bracelet_id = -1 @@ -31,6 +32,8 @@ export default class BISListModify { public current_offhand_id = -1 public current_right_ring_id = -1 public external_link: string | null = null + public name = '' + public display_name = '' static buildEditVersion(responseList: BISList): BISListModify { // Create an instance of this dataclass from an object with the BISList interface @@ -65,6 +68,40 @@ export default class BISListModify { newList.current_right_ring_id = responseList.current_right_ring.id newList.external_link = responseList.external_link + newList.name = responseList.name + newList.display_name = responseList.display_name + return newList } + + importBIS(data: ImportResponse): void { + this.job_id = data.job_id + this.bis_mainhand_id = data.mainhand + this.bis_offhand_id = data.offhand + this.bis_head_id = data.head + this.bis_body_id = data.body + this.bis_hands_id = data.hands + this.bis_legs_id = data.legs + this.bis_feet_id = data.feet + this.bis_earrings_id = data.earrings + this.bis_necklace_id = data.necklace + this.bis_bracelet_id = data.bracelet + this.bis_left_ring_id = data.left_ring + this.bis_right_ring_id = data.right_ring + } + + importCurrent(data: BISList): void { + this.current_body_id = data.current_body.id + this.current_bracelet_id = data.current_bracelet.id + this.current_earrings_id = data.current_earrings.id + this.current_feet_id = data.current_feet.id + this.current_hands_id = data.current_hands.id + this.current_head_id = data.current_head.id + this.current_left_ring_id = data.current_left_ring.id + this.current_legs_id = data.current_legs.id + this.current_mainhand_id = data.current_mainhand.id + this.current_necklace_id = data.current_necklace.id + this.current_offhand_id = data.current_offhand.id + this.current_right_ring_id = data.current_right_ring.id + } } diff --git a/frontend/src/interfaces/bis_list.ts b/frontend/src/interfaces/bis_list.ts index f55c4306..b96c4757 100644 --- a/frontend/src/interfaces/bis_list.ts +++ b/frontend/src/interfaces/bis_list.ts @@ -27,8 +27,10 @@ export default interface BISList { current_necklace: Gear current_offhand: Gear current_right_ring: Gear + display_name: string external_link: string | null id: number item_level: number job: Job + name: string } diff --git a/frontend/src/interfaces/imports.ts b/frontend/src/interfaces/imports.ts new file mode 100644 index 00000000..3bae611e --- /dev/null +++ b/frontend/src/interfaces/imports.ts @@ -0,0 +1,23 @@ +export interface ImportError { + message: string +} + +export interface ImportResponse { + job_id: string + job_name: string + mainhand: number + offhand: number + head: number + body: number + hands: number + legs: number + feet: number + earrings: number + necklace: number + bracelet: number + left_ring: number + right_ring: number + + min_il: number + max_il: number +} diff --git a/frontend/src/interfaces/loot.ts b/frontend/src/interfaces/loot.ts index 528aad50..ef032b5f 100644 --- a/frontend/src/interfaces/loot.ts +++ b/frontend/src/interfaces/loot.ts @@ -2,6 +2,7 @@ import Team from './team' export interface GreedItem { bis_list_id: number + bis_list_name: string current_gear_name: string current_gear_il: number job_icon_name: string diff --git a/frontend/src/interfaces/responses.ts b/frontend/src/interfaces/responses.ts index 6092dde7..c6efc34d 100644 --- a/frontend/src/interfaces/responses.ts +++ b/frontend/src/interfaces/responses.ts @@ -28,6 +28,7 @@ export interface BISListErrors { current_offhand_id?: string[], current_right_ring_id?: string[], external_link?: string[], + name?: string[], } export interface BISListDeleteReadResponse { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index ea35df5d..06ddeda9 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -27,7 +27,7 @@ Sentry.init({ Vue, dsn: 'https://06f41b525a40497a848fb726f6d03244@o242258.ingest.sentry.io/6180221', logErrors: true, - release: 'savageaim@20220511', + release: 'savageaim@20220516', }) new Vue({ diff --git a/frontend/src/mixins/savage_aim_mixin.ts b/frontend/src/mixins/savage_aim_mixin.ts index e30a1e6c..6cc3e075 100644 --- a/frontend/src/mixins/savage_aim_mixin.ts +++ b/frontend/src/mixins/savage_aim_mixin.ts @@ -11,7 +11,7 @@ export default class SavageAimMixin extends Vue { } async load(): Promise { - console.log('unimplemented') + console.error('unimplemented') } get user(): User { diff --git a/frontend/src/views/character.vue b/frontend/src/views/character.vue index 2b570d7f..c8b36b5b 100644 --- a/frontend/src/views/character.vue +++ b/frontend/src/views/character.vue @@ -71,7 +71,7 @@
- {{ bis.job.display_name }} + {{ bis.display_name }}
@@ -83,7 +83,7 @@
- +
diff --git a/frontend/src/views/edit_bis.vue b/frontend/src/views/edit_bis.vue index 40b0f6bd..ff700867 100644 --- a/frontend/src/views/edit_bis.vue +++ b/frontend/src/views/edit_bis.vue @@ -6,22 +6,21 @@
- - +
diff --git a/frontend/src/views/new_bis.vue b/frontend/src/views/new_bis.vue index 5c1ac9a8..392abdc3 100644 --- a/frontend/src/views/new_bis.vue +++ b/frontend/src/views/new_bis.vue @@ -7,20 +7,19 @@ - - +
diff --git a/frontend/src/views/team/join.vue b/frontend/src/views/team/join.vue index 4b8ea6ce..7fea0bbd 100644 --- a/frontend/src/views/team/join.vue +++ b/frontend/src/views/team/join.vue @@ -14,13 +14,20 @@
-
- +
+
+
You have been invited to join;
+
+
+
+ +
+
-
Select your Character and BIS List to join!
+
Select your Character and BIS List
diff --git a/frontend/src/views/team/loot.vue b/frontend/src/views/team/loot.vue index b3fa5d94..dcb7efe9 100644 --- a/frontend/src/views/team/loot.vue +++ b/frontend/src/views/team/loot.vue @@ -84,201 +84,33 @@

Below are the people that need the chosen item for any other BIS they have, grouped by character.

Clicking the button beside anyone will add a Loot entry, and update their BIS List accordingly (where possible).

- -
@@ -287,9 +119,8 @@ @@ -576,18 +355,4 @@ export default class TeamLoot extends SavageAimMixin { .list-actions { margin-left: 1.25rem; } - -.mobile-history li:not(:last-child) { - margin-bottom: 0.75rem; - padding-bottom: 0.75rem; - border-bottom: 2px solid #17181c; -} - -.greed-box { - position: relative; -} - -.delete-cell { - width: 0; -}