{{ errors.name[0] }}
+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'
{{ errors.name[0] }}
+{{ errors.job_id[0] }}
+{{ errors.external_link[0] }}
+Etro links can now be imported automatically!
+{{ errors.job_id[0] }}
-{{ errors.external_link[0] }}
-This is just a link to any page you deem would be handy to have attached to the list!
-Item Level Range
-Item Level Range
-{{ errors.job_id[0] }}
-{{ errors.external_link[0] }}
-This is just a link to any page you deem would be handy to have attached to the list!
-{{ createLootErrors.obtained[0] }}
+{{ createLootErrors.member_id[0] }}
+{{ createLootErrors.greed[0] }}
+Date | +Obtained By | +Item | +Need / Greed | +Delete | +
---|---|---|---|---|
+
+
+
+ {{ createLootErrors.obtained[0] }} + |
+
+
+
+
+
+
+ {{ createLootErrors.member_id[0] }} + |
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+ {{ createLootErrors.greed[0] }} + |
+ + | +
{{ history.obtained }} |
+ {{ history.member }} |
+ {{ history.item }} |
+
+ Greed +Need + |
+ + + | +
+ | + | + | + | + + | +
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.
+ +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.
+ ++ Added the following Gear for the release of 6.11;
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).
- -{{ createLootErrors.obtained[0] }}
-{{ createLootErrors.member_id[0] }}
-{{ createLootErrors.greed[0] }}
-Date | -Obtained By | -Item | -Need / Greed | -Delete | -
---|---|---|---|---|
{{ history.obtained }} |
- {{ history.member }} |
- {{ history.item }} |
-
- Greed -Need - |
- - - | -
-
-
-
- {{ createLootErrors.obtained[0] }} - |
-
-
-
-
-
-
- {{ createLootErrors.member_id[0] }} - |
-
- |
-
-
-
-
-
-
-
-
-
-
-
- {{ createLootErrors.greed[0] }} - |
- - - | -