diff --git a/backend/api/management/commands/add_extra_names_to_gear.py b/backend/api/management/commands/add_extra_names_to_gear.py new file mode 100644 index 00000000..594b70de --- /dev/null +++ b/backend/api/management/commands/add_extra_names_to_gear.py @@ -0,0 +1,36 @@ +from os import scandir +# lib +import yaml +from django.conf import settings +from django.core.management.base import BaseCommand +# local +from api import models + + +class Command(BaseCommand): + help = 'Add Extra Names to Gear Objects' + + def handle(self, *args, **options): + gear_data_dir = settings.BASE_DIR / 'api/management/commands/seed_data/gear' + + # Get Gear data, double check that the Gear has the required information + with scandir(gear_data_dir) as expac_dirs: + for expac_dir in expac_dirs: + if not expac_dir.is_dir(): + continue + + with scandir(expac_dir.path) as gear_files: + for file in gear_files: + with open(file, 'r') as f: + data = yaml.safe_load(f) + for item in data: + obj = models.Gear.objects.get(name=item['name'], item_level=item['item_level']) + changed = False + if len(obj.extra_import_names) == 0 and len(item.get('extra_import_names', [])) > 0: + changed = True + obj.extra_import_names = item['extra_import_names'] + if len(obj.extra_import_classes) == 0 and len(item.get('extra_import_classes', [])) > 0: + changed = True + obj.extra_import_classes = item['extra_import_classes'] + if changed: + obj.save() diff --git a/backend/api/management/commands/seed.py b/backend/api/management/commands/seed.py index 904ed119..411af657 100644 --- a/backend/api/management/commands/seed.py +++ b/backend/api/management/commands/seed.py @@ -4,7 +4,6 @@ import yaml from django.conf import settings from django.core.management.base import BaseCommand -from django.db import IntegrityError, transaction # local from api import models @@ -22,11 +21,16 @@ def handle(self, *args, **options): self.stdout.write(self.style.HTTP_REDIRECT('Seeding Tiers')) self.import_file(f, models.Tier) - with scandir(gear_data_dir) as gear_files: - for file in gear_files: - self.stdout.write(self.style.HTTP_REDIRECT(f'Seeding Gear from {file.name}')) - with open(gear_data_dir / file.name, 'r') as f: - self.import_file(f, models.Gear) + with scandir(gear_data_dir) as expac_dirs: + for expac_dir in expac_dirs: + if not expac_dir.is_dir(): + continue + + with scandir(expac_dir.path) as gear_files: + for file in gear_files: + self.stdout.write(self.style.HTTP_REDIRECT(f'Seeding Gear from {file.name}')) + with open(file.path, 'r') as f: + self.import_file(f, models.Gear) # Lastly we import the Job data. # This is handled *slightly* differently because the 'ordering' key in this file will most likely change @@ -41,10 +45,8 @@ def import_file(self, file, model): data = yaml.safe_load(file) for item in data: self.stdout.write(f'\t{item["name"]}') - try: - with transaction.atomic(): - model.objects.create(**item) - except IntegrityError: + _, created = model.objects.get_or_create(**item) + if not created: self.stdout.write('\t\tSkipping, as it is already in the DB.') def import_jobs(self, file): diff --git a/backend/api/management/commands/seed_data/gear/6-ew/6.0.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.0.yml new file mode 100644 index 00000000..26707f3c --- /dev/null +++ b/backend/api/management/commands/seed_data/gear/6-ew/6.0.yml @@ -0,0 +1,69 @@ +- has_accessories: True + has_armour: False + has_weapon: False + item_level: 560 + name: 'The Last' +- has_accessories: False + has_armour: True + has_weapon: True + item_level: 560 + name: 'Endwalker AF' + extra_import_classes: + # Armour + - Reverence + - Pummeler's + - Ignominy + - Allegiance + - Tiamat + - Reaper's + - Anchorite's + - Saotome + - Hachiya + - Brioso + - Pioneer's + - Etoile + - Spaekona's + - Convoker's + - Atrophy + - Theophany + - Academic's + - Astronomia + - Didact's + extra_import_names: + # Weapons + - Lightbringer + - Gigantaxe + - Chaosbringer + - Hyperion + - Abel's Lance + - Burning Fists + - Murasame + - Mutsunokami + - Perseus's Bow + - Ataktos + - Terpsichore + - Asura's Rod + - Abraxas + - Wild Rose + - Xoanon + - Epeolatry + - Diana + - Death Sickle + - Hagneia + - Hero's Shield + +- has_accessories: True + has_armour: True + has_weapon: True + item_level: 570 + name: 'Moonward' +- has_accessories: True + has_armour: False + has_weapon: False + item_level: 580 + name: 'Eternal Dark' +- has_accessories: False + has_armour: False + has_weapon: True + item_level: 580 + name: 'Divine Light' diff --git a/backend/api/management/commands/seed_data/gear/6.01.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.01.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.01.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.01.yml diff --git a/backend/api/management/commands/seed_data/gear/6.05.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.05.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.05.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.05.yml diff --git a/backend/api/management/commands/seed_data/gear/6.1.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.1.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.1.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.1.yml diff --git a/backend/api/management/commands/seed_data/gear/6.11.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.11.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.11.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.11.yml diff --git a/backend/api/management/commands/seed_data/gear/6.2.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.2.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.2.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.2.yml diff --git a/backend/api/management/commands/seed_data/gear/6.25.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.25.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.25.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.25.yml diff --git a/backend/api/management/commands/seed_data/gear/6.3.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.3.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.3.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.3.yml diff --git a/backend/api/management/commands/seed_data/gear/6.31.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.31.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.31.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.31.yml diff --git a/backend/api/management/commands/seed_data/gear/6.35.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.35.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.35.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.35.yml diff --git a/backend/api/management/commands/seed_data/gear/6-ew/6.4.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.4.yml new file mode 100644 index 00000000..452e13ce --- /dev/null +++ b/backend/api/management/commands/seed_data/gear/6-ew/6.4.yml @@ -0,0 +1,77 @@ +- has_accessories: True + has_armour: True + has_weapon: True + item_level: 640 + name: 'Diadochos' +- has_accessories: True + has_armour: True + has_weapon: False + item_level: 640 + name: 'Anabaseios' +- has_accessories: False + has_armour: False + has_weapon: True + item_level: 645 + name: 'Voidcast' +- has_accessories: True + has_armour: True + has_weapon: True + item_level: 650 + name: 'Credendum' + extra_import_names: + - Polaris Point + - Wrathgrinder + - Blood Rapture + - Cerberus Fang + - Night's Wolf + - Avatar's Scythe + - Dragon's Beard Fists + - Higan Shigan + - Shinobi Knives + - Bluebird's Nest + - Vitória do Povo + - Twin Moons + - Ignis Malus + - Encounter in Lilies + - Lamplight Crook + - Bunny's Crescent + - Faerie Fancy + - Stargazer + - Isnae Phis + - Vessel +- has_accessories: True + has_armour: True + has_weapon: True + item_level: 660 + name: 'Augmented Credendum' + extra_import_names: + - Augmented Polaris Point + - Augmented Wrathgrinder + - Augmented Blood Rapture + - Augmented Cerberus Fang + - Augmented Night's Wolf + - Augmented Avatar's Scythe + - Augmented Dragon's Beard Fists + - Augmented Higan Shigan + - Augmented Shinobi Knives + - Augmented Bluebird's Nest + - Augmented Vitória do Povo + - Augmented Twin Moons + - Augmented Ignis Malus + - Augmented Encounter in Lilies + - Augmented Lamplight Crook + - Augmented Bunny's Crescent + - Augmented Faerie Fancy + - Augmented Stargazer + - Augmented Isnae Phis + - Augmented Vessel +- has_accessories: True + has_armour: True + has_weapon: False + item_level: 660 + name: 'Ascension' +- has_accessories: False + has_armour: False + has_weapon: True + item_level: 665 + name: 'Ascension' diff --git a/backend/api/management/commands/seed_data/gear/6.45.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.45.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.45.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.45.yml diff --git a/backend/api/management/commands/seed_data/gear/6.5.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.5.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.5.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.5.yml diff --git a/backend/api/management/commands/seed_data/gear/6.51.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.51.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.51.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.51.yml diff --git a/backend/api/management/commands/seed_data/gear/6.55.yml b/backend/api/management/commands/seed_data/gear/6-ew/6.55.yml similarity index 100% rename from backend/api/management/commands/seed_data/gear/6.55.yml rename to backend/api/management/commands/seed_data/gear/6-ew/6.55.yml diff --git a/backend/api/management/commands/seed_data/gear/6.0.yml b/backend/api/management/commands/seed_data/gear/6.0.yml deleted file mode 100644 index 1b29c61a..00000000 --- a/backend/api/management/commands/seed_data/gear/6.0.yml +++ /dev/null @@ -1,25 +0,0 @@ -- has_accessories: true - has_armour: false - has_weapon: false - item_level: 560 - name: 'The Last' -- has_accessories: false - has_armour: True - has_weapon: True - item_level: 560 - name: 'Endwalker AF' -- has_accessories: True - has_armour: True - has_weapon: True - item_level: 570 - name: 'Moonward' -- has_accessories: True - has_armour: False - has_weapon: False - item_level: 580 - name: 'Eternal Dark' -- has_accessories: False - has_armour: False - has_weapon: True - item_level: 580 - name: 'Divine Light' diff --git a/backend/api/management/commands/seed_data/gear/6.4.yml b/backend/api/management/commands/seed_data/gear/6.4.yml deleted file mode 100644 index 79ecb515..00000000 --- a/backend/api/management/commands/seed_data/gear/6.4.yml +++ /dev/null @@ -1,35 +0,0 @@ -- has_accessories: True - has_armour: True - has_weapon: True - item_level: 640 - name: 'Diadochos' -- has_accessories: True - has_armour: True - has_weapon: False - item_level: 640 - name: 'Anabaseios' -- has_accessories: False - has_armour: False - has_weapon: True - item_level: 645 - name: 'Voidcast' -- has_accessories: True - has_armour: True - has_weapon: True - item_level: 650 - name: 'Credendum' -- has_accessories: True - has_armour: True - has_weapon: True - item_level: 660 - name: 'Augmented Credendum' -- has_accessories: True - has_armour: True - has_weapon: False - item_level: 660 - name: 'Ascension' -- has_accessories: False - has_armour: False - has_weapon: True - item_level: 665 - name: 'Ascension' diff --git a/backend/api/management/commands/seed_data/gear/7-dt/7.0.yml b/backend/api/management/commands/seed_data/gear/7-dt/7.0.yml new file mode 100644 index 00000000..cf6deffe --- /dev/null +++ b/backend/api/management/commands/seed_data/gear/7-dt/7.0.yml @@ -0,0 +1,133 @@ +- has_accessories: True + has_armour: False + has_weapon: False + item_level: 690 + name: 'Epochal' +- has_accessories: False + has_armour: True + has_weapon: True + item_level: 690 + name: 'Dawntrail Artifact Gear' + extra_import_classes: + # Armour + # Tank + # PLD + - Caballarius + # WAR + - Agoge + # DRK + - Fallen's + # GNB + - Bastion's + + # Melee + # DRG + - Heavensbound + # RPR + - Assassin's + # MNK + - Hesychast's + # SAM + - Sakonji + # NIN + - Momochi + # VPR + - Viper's + + # Phys Ranged + # BRD + - Bihu + # MCH + - Forerider's + # DNC + - Horos + + # Magic Ranged + # BLM + - Archmage's + # SMN + - Glyphic + # RDM + - Roseblood + # PCT + - Pictomancer's + + # Healer + # WHM + - Piety + # SCH + - Pedagogy + # AST + - Ephemerist's + # SGE + - Asklepian + extra_import_names: + # Weapons + # Tank + # PLD + - Clarent + - Galahad Shield + # WAR + - Ferocity + # DRK + - Maleficus + # GNB + - Chastiefol + + # Melee + # DRG + - Gae Assail + # RPR + - Vendetta + # MNK + - Suwaiyas + # SAM + - Kogarasumaru + # NIN + - Shiranui + # VPR + - Sargatanas + + # Phys Ranged + # BRD + - Gastraphetes + # MCH + - Sthalmann Special + # DNC + - Soma + + # Mage Ranged + # BLM + - Gridarvor + # SMN + - The Grand Grimoire + # RDM + - Colada + # PCT + - Angel Brush + + # Healer + # WHM + - Serenity + # SCH + - Eclecticism + # AST + - Metis + # SGE + - Metanoia + +- has_accessories: True + has_armour: True + has_weapon: True + item_level: 700 + name: 'Neo Kingdom' +- has_accessories: True + has_armour: False + has_weapon: False + item_level: 710 + name: 'Resilient' +- has_accessories: False + has_armour: False + has_weapon: True + item_level: 710 + name: 'Skyruin' diff --git a/backend/api/management/commands/seed_data/jobs.yml b/backend/api/management/commands/seed_data/jobs.yml index 7ef0c240..073ab90f 100644 --- a/backend/api/management/commands/seed_data/jobs.yml +++ b/backend/api/management/commands/seed_data/jobs.yml @@ -68,37 +68,47 @@ display_name: 'Reaper' ordering: 4 role: 'dps' +- name: 'viper' + id: 'VPR' + display_name: 'Viper' + ordering: 5 + role: 'dps' # Phys Ranged - name: 'bard' id: 'BRD' display_name: 'Bard' - ordering: 5 + ordering: 6 role: 'dps' - name: 'machinist' id: 'MCH' display_name: 'Machinist' - ordering: 6 + ordering: 7 role: 'dps' - name: 'dancer' id: 'DNC' display_name: 'Dancer' - ordering: 7 + ordering: 8 role: 'dps' # Casters - name: 'blackmage' id: 'BLM' display_name: 'Black Mage' - ordering: 8 + ordering: 9 role: 'dps' - name: 'summoner' id: 'SMN' display_name: 'Summoner' - ordering: 9 + ordering: 10 role: 'dps' - name: 'redmage' id: 'RDM' display_name: 'Red Mage' - ordering: 10 + ordering: 11 + role: 'dps' +- name: 'pictomancer' + id: 'PCT' + display_name: 'Pictomancer' + ordering: 12 role: 'dps' diff --git a/backend/api/migrations/0025_gear_extra_import_names.py b/backend/api/migrations/0025_gear_extra_import_names.py new file mode 100644 index 00000000..c40f8091 --- /dev/null +++ b/backend/api/migrations/0025_gear_extra_import_names.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.24 on 2024-06-08 14:21 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0024_team_override_solver_sort'), + ] + + operations = [ + migrations.AddField( + model_name='gear', + name='extra_import_classes', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=64), default=list, size=None), + ), + migrations.AddField( + model_name='gear', + name='extra_import_names', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), default=list, size=None), + ), + ] diff --git a/backend/api/models/gear.py b/backend/api/models/gear.py index 96318389..b4edc113 100644 --- a/backend/api/models/gear.py +++ b/backend/api/models/gear.py @@ -3,11 +3,17 @@ Will be starting with just 6.0 gear """ - +from django.contrib.postgres.fields import ArrayField from django.db import models class Gear(models.Model): + # Arrays of extra names to check / use when importing Gear that has unique names. + # extra_import_classes are checked with Levenstein distance. + extra_import_classes = ArrayField(models.CharField(max_length=64), default=list) + # extra_import_names are checked to see if the item is present in the list directly. + extra_import_names = ArrayField(models.CharField(max_length=128), default=list) + has_accessories = models.BooleanField(default=False) has_armour = models.BooleanField(default=False) has_weapon = models.BooleanField(default=False) diff --git a/backend/api/serializers/__init__.py b/backend/api/serializers/__init__.py index 8e71d18c..77085226 100644 --- a/backend/api/serializers/__init__.py +++ b/backend/api/serializers/__init__.py @@ -4,6 +4,7 @@ from .job import JobSerializer from .loot import LootSerializer, LootCreateSerializer, LootCreateWithBISSerializer from .notification import NotificationSerializer +from .plugin import PluginImportSerializer, PluginImportResponseSerializer from .settings import SettingsSerializer from .team import ( TeamSerializer, @@ -32,6 +33,9 @@ 'NotificationSerializer', + 'PluginImportSerializer', + 'PluginImportResponseSerializer', + 'SettingsSerializer', 'TeamSerializer', diff --git a/backend/api/serializers/gear.py b/backend/api/serializers/gear.py index be4a0d7a..662968c4 100644 --- a/backend/api/serializers/gear.py +++ b/backend/api/serializers/gear.py @@ -14,5 +14,5 @@ class GearSerializer(serializers.ModelSerializer): class Meta: - fields = '__all__' + exclude = ['extra_import_classes', 'extra_import_names'] model = Gear diff --git a/backend/api/serializers/plugin.py b/backend/api/serializers/plugin.py new file mode 100644 index 00000000..4414b9f4 --- /dev/null +++ b/backend/api/serializers/plugin.py @@ -0,0 +1,49 @@ +""" +Serializers for Plugin imports +""" +# lib +from rest_framework import serializers + +__all__ = [ + 'PluginImportSerializer', +] + + +class PluginImportSlotSerializer(serializers.Serializer): + name = serializers.CharField() + item_level = serializers.IntegerField() + + +class PluginImportResponseSlotSerializer(serializers.Serializer): + name = serializers.CharField() + id = serializers.IntegerField() + + +class PluginImportSerializer(serializers.Serializer): + mainhand = PluginImportSlotSerializer() + offhand = PluginImportSlotSerializer() + head = PluginImportSlotSerializer() + body = PluginImportSlotSerializer() + hands = PluginImportSlotSerializer() + legs = PluginImportSlotSerializer() + feet = PluginImportSlotSerializer() + earrings = PluginImportSlotSerializer() + necklace = PluginImportSlotSerializer() + bracelet = PluginImportSlotSerializer() + right_ring = PluginImportSlotSerializer() + left_ring = PluginImportSlotSerializer() + + +class PluginImportResponseSerializer(serializers.Serializer): + mainhand = PluginImportResponseSlotSerializer(allow_null=True) + offhand = PluginImportResponseSlotSerializer(allow_null=True) + head = PluginImportResponseSlotSerializer(allow_null=True) + body = PluginImportResponseSlotSerializer(allow_null=True) + hands = PluginImportResponseSlotSerializer(allow_null=True) + legs = PluginImportResponseSlotSerializer(allow_null=True) + feet = PluginImportResponseSlotSerializer(allow_null=True) + earrings = PluginImportResponseSlotSerializer(allow_null=True) + necklace = PluginImportResponseSlotSerializer(allow_null=True) + bracelet = PluginImportResponseSlotSerializer(allow_null=True) + right_ring = PluginImportResponseSlotSerializer(allow_null=True) + left_ring = PluginImportResponseSlotSerializer(allow_null=True) diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index 239605b3..0399cca5 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -19,6 +19,7 @@ class UserSerializer(serializers.Serializer): loot_manager_version = serializers.SerializerMethodField() notifications = serializers.SerializerMethodField() theme = serializers.SerializerMethodField() + token = serializers.CharField(source='auth_token.key', default=None) username = serializers.SerializerMethodField() def get_avatar_url(self, obj) -> str: diff --git a/backend/api/tests/test_job.py b/backend/api/tests/test_job.py index 3f36ec9d..52eed942 100644 --- a/backend/api/tests/test_job.py +++ b/backend/api/tests/test_job.py @@ -50,6 +50,7 @@ def test_solver_sort_list(self): 'NIN', 'SAM', 'RPR', + 'VPR', 'BRD', 'MCH', @@ -58,6 +59,7 @@ def test_solver_sort_list(self): 'BLM', 'SMN', 'RDM', + 'PCT', 'PLD', 'WAR', diff --git a/backend/api/tests/test_lodestone_gear_import.py b/backend/api/tests/test_lodestone_gear_import.py index fcbb8fa6..a9eaa4d2 100644 --- a/backend/api/tests/test_lodestone_gear_import.py +++ b/backend/api/tests/test_lodestone_gear_import.py @@ -28,7 +28,7 @@ def test_import(self): Test Plan; - Import Eira and ensure her gear matches what I currently had equipped """ - url = reverse('api:lodestone_gear_import', kwargs={'character_id': '22909725', 'expected_job': 'PLD'}) + url = reverse('api:lodestone_gear_import', kwargs={'character_id': '22909725', 'expected_job': 'MNK'}) user = self._get_user() self.client.force_authenticate(user) response = self.client.get(url) @@ -36,19 +36,19 @@ def test_import(self): # Build an expected data packet expected = { - 'job_id': 'PLD', + 'job_id': 'MNK', 'mainhand': Gear.objects.get(name='Voidcast').pk, 'offhand': Gear.objects.get(name='Voidcast').pk, - 'head': Gear.objects.get(name='Ascension', has_armour=True).pk, - 'body': Gear.objects.get(name='Augmented Credendum', has_armour=True).pk, + 'head': Gear.objects.get(name='Diadochos', has_armour=True).pk, + 'body': Gear.objects.get(name='Diadochos', has_armour=True).pk, 'hands': Gear.objects.get(name='Diadochos', has_armour=True).pk, - 'legs': Gear.objects.get(name='Ascension', has_armour=True).pk, - 'feet': Gear.objects.get(name='Augmented Credendum', has_armour=True).pk, - 'earrings': Gear.objects.get(name='Augmented Credendum', has_accessories=True).pk, - 'necklace': Gear.objects.get(name='Ascension', has_accessories=True).pk, - 'bracelet': Gear.objects.get(name='Diadochos', has_accessories=True).pk, - 'right_ring': Gear.objects.get(name='Credendum', has_accessories=True).pk, - 'left_ring': Gear.objects.get(name='Ascension', has_accessories=True).pk, + 'legs': Gear.objects.get(name='Diadochos', has_armour=True).pk, + 'feet': Gear.objects.get(name='Diadochos', has_armour=True).pk, + 'earrings': Gear.objects.get(name='Ascension', has_accessories=True).pk, + 'necklace': Gear.objects.get(name='Augmented Credendum', has_accessories=True).pk, + 'bracelet': Gear.objects.get(name='Ascension', has_accessories=True).pk, + 'right_ring': Gear.objects.get(name='Ascension', has_accessories=True).pk, + 'left_ring': Gear.objects.get(name='Augmented Credendum', has_accessories=True).pk, 'min_il': 640, 'max_il': 660, } @@ -79,5 +79,5 @@ def test_import_400_and_404(self): self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE) self.assertEqual( response.json()['message'], - 'Couldn\'t import Gear from Lodestone. Gear was expected to be for "SAM", but "GLA PLD" was found.', + 'Couldn\'t import Gear from Lodestone. Gear was expected to be for "SAM", but "PGL MNK" was found.', ) diff --git a/backend/api/tests/test_lodestone_scraper.py b/backend/api/tests/test_lodestone_scraper.py index 3610a385..46f758d0 100644 --- a/backend/api/tests/test_lodestone_scraper.py +++ b/backend/api/tests/test_lodestone_scraper.py @@ -39,7 +39,7 @@ def test_data_pull(self): expected = { 'avatar_url': ( 'https://img2.finalfantasyxiv.com/f/ce3cf70bc9048943a57001f987830daa_' - '7206469080400ed57a5373d0a9c55c59fc0_96x96.jpg' + '7206469080400ed57a5373d0a9c55c59fc0.jpg' ), 'name': 'Eira Erikawa', 'world': 'Lich', diff --git a/backend/api/tests/test_loot_solver.py b/backend/api/tests/test_loot_solver.py index 7af8d43b..537c89de 100644 --- a/backend/api/tests/test_loot_solver.py +++ b/backend/api/tests/test_loot_solver.py @@ -809,6 +809,7 @@ def test_solver_sort_overrides(self): 'AST': 4, 'DRK': 5, 'BLM': 3, + 'SGE': 18, } self.team.save() self.team.refresh_from_db() diff --git a/backend/api/tests/test_plugin_import.py b/backend/api/tests/test_plugin_import.py new file mode 100644 index 00000000..2e719510 --- /dev/null +++ b/backend/api/tests/test_plugin_import.py @@ -0,0 +1,90 @@ +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, Tier, Job +from .test_base import SavageAimTestCase + + +class LodestoneGearImport(SavageAimTestCase): + """ + Test the import of gear from Lodestone. + This is a volatile test suite since I'm using my main account but it's probably for the best. + """ + + def setUp(self): + """ + Call the Gear seed command to prepopulate the DB + """ + call_command('seed', stdout=StringIO()) + + def tearDown(self): + Gear.objects.all().delete() + Tier.objects.all().delete() + Job.objects.all().delete() + + def test_import(self): + """ + - Create a mapping of valid data for importing current data from the game + - Expect a correct import + """ + url = reverse('api:plugin_import') + user = self._get_user() + self.client.force_authenticate(user) + + # Generate a valid data block for importing + # Expect use of new extra_import fields on Gear + # Also include an item that isn't in the website DB, ensure it just returns None + request_data = { + 'mainhand': {'name': 'Dragon\'s Beard Fists', 'item_level': 650}, + 'offhand': {'name': 'Hero\'s Shield', 'item_level': 560}, + 'head': {'name': 'Allegiance Blinder', 'item_level': 560}, + 'body': {'name': 'Augmented Credendum Cuirass of Fending', 'item_level': 660}, + 'hands': {'name': 'Ascension Gloves of Fending', 'item_level': 660}, + 'legs': {'name': 'Brioso Bottoms', 'item_level': 560}, + 'feet': {'name': 'Diadochos Boots of Fending', 'item_level': 640}, + 'earrings': {'name': 'Ascension Earrings of Fending', 'item_level': 660}, + 'necklace': {'name': 'Ascension Necklace of Fending', 'item_level': 660}, + 'bracelet': {'name': 'Augmented Credendum of Fending', 'item_level': 660}, + 'right_ring': {'name': 'Ascension Ring of Fending', 'item_level': 660}, + 'left_ring': {'name': 'Brand New Ring', 'item_level': 30}, + } + + response = self.client.post(url, request_data) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) + + content = response.json() + self.assertEqual(content['mainhand']['name'], 'Credendum') + self.assertEqual(content['offhand']['name'], 'Endwalker AF') + self.assertEqual(content['head']['name'], 'Endwalker AF') + self.assertEqual(content['body']['name'], 'Augmented Credendum') + self.assertEqual(content['hands']['name'], 'Ascension') + self.assertEqual(content['legs']['name'], 'Endwalker AF') + self.assertEqual(content['feet']['name'], 'Diadochos') + self.assertEqual(content['earrings']['name'], 'Ascension') + self.assertEqual(content['necklace']['name'], 'Ascension') + self.assertEqual(content['bracelet']['name'], 'Augmented Credendum') + self.assertEqual(content['right_ring']['name'], 'Ascension') + self.assertIsNone(content['left_ring']) + + def test_import_400(self): + """ + Send invalid data, ensure proper errors are given + """ + url = reverse('api:plugin_import') + user = self._get_user() + self.client.force_authenticate(user) + + # Generate a valid data block for importing + # Expect use of new extra_import fields on Gear + # Also include an item that isn't in the website DB, ensure it just returns None + request_data = { + 'offhand': {'item_level': 560}, + 'head': {'name': 'Allegiance Blinder'}, + } + + response = self.client.post(url, request_data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) + self.assertEqual(response.json()['mainhand'], ['This field is required.']) + self.assertEqual(response.json()['offhand']['name'], ['This field is required.']) + self.assertEqual(response.json()['head']['item_level'], ['This field is required.']) diff --git a/backend/api/tests/test_team.py b/backend/api/tests/test_team.py index 074f73c9..5f4bf877 100644 --- a/backend/api/tests/test_team.py +++ b/backend/api/tests/test_team.py @@ -503,7 +503,7 @@ def test_update_400(self): content['team_lead'], ['Please select a non-proxy Member of the Team to be the new team lead.'], ) - self.assertEqual(content['solver_sort_overrides'], ['Please specify a position between 1 and 19 (found "0")']) + self.assertEqual(content['solver_sort_overrides'], ['Please specify a position between 1 and 21 (found "0")']) # Run the team lead test again with a valid character id that isn't on the team char = Character.objects.create( @@ -526,7 +526,7 @@ def test_update_400(self): content['team_lead'], ['Please select a non-proxy Member of the Team to be the new team lead.'], ) - self.assertEqual(content['solver_sort_overrides'], ['Please specify a position between 1 and 19 (found "100")']) + self.assertEqual(content['solver_sort_overrides'], ['Please specify a position between 1 and 21 (found "100")']) # Make the above Character a Member of the Team, but make them a proxy char.user = None @@ -569,7 +569,10 @@ def test_update_400(self): content['team_lead'], ['Please select a non-proxy Member of the Team to be the new team lead.'], ) - self.assertEqual(content['solver_sort_overrides'], ['Please specify only one Job per position! (position "1" was found multiple times)']) + self.assertEqual( + content['solver_sort_overrides'], + ['Please specify only one Job per position! (position "1" was found multiple times)'], + ) def test_delete(self): """ diff --git a/backend/api/tests/test_user.py b/backend/api/tests/test_user.py index 7cb1bb56..00620426 100644 --- a/backend/api/tests/test_user.py +++ b/backend/api/tests/test_user.py @@ -1,5 +1,6 @@ from django.urls import reverse from rest_framework import status +from rest_framework.authtoken.models import Token from .test_base import SavageAimTestCase from api.models import Settings @@ -9,6 +10,9 @@ class User(SavageAimTestCase): Test the /me/ endpoint for logged in and anonymous users """ + def tearDown(self): + Token.objects.all().delete() + def test_anonymous_user(self): """ Send a request to /me/ endpoint without logging in. @@ -95,3 +99,54 @@ def test_update_403(self): url = reverse('api:user') response = self.client.put(url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class TestUserTokenView(SavageAimTestCase): + """ + Test that creating a Token for a User works. + Test for a User creating a new Token, and one regenerating their token. + """ + + def test_create_new_token(self): + """ + Create a User without a Token. + Send request to generate a Token. + Read User data, ensure we get a Token back. + """ + read_url = reverse('api:user') + token_url = reverse('api:user_token') + user = self._get_user() + self.client.force_authenticate(user) + + with self.assertRaises(Token.DoesNotExist): + user.auth_token + + # Generate a Token + response = self.client.patch(token_url) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Read User, ensure we get a Token back + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsNotNone(response.json()['token']) + + def test_regenerate_token(self): + """ + Create a User with an existing Token. + Send request to re-generate a Token. + Read User data, ensure we get a NEW Token back. + """ + read_url = reverse('api:user') + token_url = reverse('api:user_token') + user = self._get_user() + self.client.force_authenticate(user) + token = Token.objects.create(user=user) + + # Generate a Token + response = self.client.patch(token_url) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Read User, ensure we get a Token back + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNotEqual(response.json()['token'], token.key) diff --git a/backend/api/urls.py b/backend/api/urls.py index a18b33dd..e859ac96 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -19,6 +19,7 @@ # Imports path('import/etro//', views.EtroImport.as_view(), name='etro_import'), + path('import/plugin/', views.PluginImport.as_view(), name='plugin_import'), # Job path('job/', views.JobCollection.as_view(), name='job_collection'), @@ -64,4 +65,5 @@ # UserView path('me/', views.UserView.as_view(), name='user'), + path('me/token/', views.UserTokenView.as_view(), name='user_token'), ] diff --git a/backend/api/views/__init__.py b/backend/api/views/__init__.py index dbc58f4e..e992a614 100644 --- a/backend/api/views/__init__.py +++ b/backend/api/views/__init__.py @@ -7,11 +7,12 @@ from .loot import LootCollection, LootWithBIS from .loot_solver import LootSolver from .notification import NotificationCollection, NotificationResource +from .plugin import PluginImport from .team import TeamCollection, TeamResource, TeamInvite from .team_member import TeamMemberResource, TeamMemberPermissionsResource from .team_proxy import TeamProxyCollection, TeamProxyResource, TeamProxyClaim from .tier import TierCollection -from .user import UserView +from .user import UserView, UserTokenView __all__ = [ 'BISListCollection', @@ -42,6 +43,8 @@ 'NotificationCollection', 'NotificationResource', + 'PluginImport', + 'TeamCollection', 'TeamResource', 'TeamInvite', @@ -56,4 +59,5 @@ 'TierCollection', 'UserView', + 'UserTokenView', ] diff --git a/backend/api/views/base.py b/backend/api/views/base.py index ab438155..8fc4e10d 100644 --- a/backend/api/views/base.py +++ b/backend/api/views/base.py @@ -96,13 +96,25 @@ class ImportAPIView(APIView): @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 + Find the id of the gear piece that matches the name closest. + Check the extra_import_classes for distance also + However, if item_name is present in extra_import_names, immediately return the id """ diff = float('inf') gear_id = None for details in gear_selection: + if item_name in details['extra_import_names']: + return details['id'] + curr_diff = jellyfish.levenshtein_distance(details['name'], item_name) if curr_diff < diff: diff = curr_diff gear_id = details['id'] + + for extra_class in details['extra_import_classes']: + curr_diff = jellyfish.levenshtein_distance(extra_class, item_name) + if curr_diff < diff: + diff = curr_diff + gear_id = details['id'] + return gear_id diff --git a/backend/api/views/etro.py b/backend/api/views/etro.py index 1c9a022d..3dfe0220 100644 --- a/backend/api/views/etro.py +++ b/backend/api/views/etro.py @@ -76,7 +76,10 @@ def get(self, request: Request, id: str) -> Response: gear_names['mainhand'] = relic['baseItem']['name'] # Turn the names into SA gear ids - sa_gear = Gear.objects.filter(item_level__gte=min_il, item_level__lte=max_il).values('name', 'id') + sa_gear = Gear.objects.filter( + item_level__gte=min_il, + item_level__lte=max_il, + ).values('name', 'id', 'extra_import_classes', 'extra_import_names') response = { 'name': name, 'job_id': job_id, diff --git a/backend/api/views/lodestone.py b/backend/api/views/lodestone.py index cd007ee1..8ceffccd 100644 --- a/backend/api/views/lodestone.py +++ b/backend/api/views/lodestone.py @@ -67,7 +67,7 @@ def get(self, request: Request, character_id: str, expected_job: str) -> Respons filtered_gear = Gear.objects.filter( item_level__gte=data['min_il'], item_level__lte=data['max_il'], - ).values('name', 'id') + ).values('name', 'id', 'extra_import_classes', 'extra_import_names') response = { 'job_id': expected_job, 'min_il': data['min_il'], diff --git a/backend/api/views/plugin.py b/backend/api/views/plugin.py new file mode 100644 index 00000000..763ce6a3 --- /dev/null +++ b/backend/api/views/plugin.py @@ -0,0 +1,71 @@ +""" +Given a map of slot names to item names and levels, return a map of the slots to their corresponding Gear objects +""" +# stdlib +from typing import Dict +# lib +from rest_framework.request import Request +from rest_framework.response import Response +# local +from api.models import Gear +from api.serializers import PluginImportSerializer, PluginImportResponseSerializer +from api.views.base import ImportAPIView + + +class PluginImport(ImportAPIView): + """ + Convert names and item levels from in game items to Savage Aim Gear Items. + """ + + def post(self, request: Request) -> Response: + """ + Convert names from in-game into Gear instances. + """ + # Use Serializer for validating the provided data + serializer = PluginImportSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + # Determine our max and min ils + min_il = float('inf') + max_il = float('-inf') + for data in serializer.validated_data.values(): + item_level = data['item_level'] + if item_level < min_il: + min_il = item_level + if item_level > max_il: + max_il = item_level + + gear_to_check = Gear.objects.filter( + item_level__gte=min_il, + item_level__lte=max_il, + ).values('name', 'id', 'extra_import_names', 'extra_import_classes') + + response: Dict[str, Gear] = {} + + for slot, slot_data in serializer.validated_data.items(): + item_name, item_level = slot_data['name'], slot_data['item_level'] + if slot in self.ARMOUR_SLOTS: + gear_id = self._get_gear_id( + gear_to_check.filter(has_armour=True, item_level=item_level), + item_name, + ) + elif slot in self.ACCESSORY_SLOTS: + gear_id = self._get_gear_id( + gear_to_check.filter(has_accessories=True, item_level=item_level), + item_name, + ) + else: + gear_id = self._get_gear_id( + gear_to_check.filter(has_weapon=True, item_level=item_level), + item_name, + ) + + try: + if gear_id is None: + raise ValueError() + response[slot] = gear_to_check.get(pk=gear_id) + except (ValueError, Gear.DoesNotExist): + response[slot] = None + + out_serializer = PluginImportResponseSerializer(response) + return Response(out_serializer.data) diff --git a/backend/api/views/user.py b/backend/api/views/user.py index ecc6f907..6ccab866 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -3,6 +3,7 @@ """ # lib +from rest_framework.authtoken.models import Token from rest_framework.permissions import BasePermission from rest_framework.response import Response # local @@ -59,3 +60,28 @@ def put(self, request) -> Response: self._send_to_user(request.user, {'type': 'settings'}) return Response(status=201) + + +class UserTokenView(APIView): + """ + A view for handling updates to a User's Token. + """ + + def patch(self, request) -> Response: + """ + Regenerate a User's Token + """ + try: + obj = request.user.auth_token + except Token.DoesNotExist: + obj = None + + if obj is not None: + obj.delete() + + Token.objects.create(user=request.user) + + # Send websocket packet for updates + self._send_to_user(request.user, {'type': 'settings'}) + + return Response(status=201) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 5a0acf2d..031250e2 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -33,6 +33,7 @@ INSTALLED_APPS = [ 'api', 'rest_framework', + 'rest_framework.authtoken', 'channels', 'django.contrib.admin', 'django.contrib.auth', @@ -105,6 +106,10 @@ 'rest_framework.renderers.JSONRenderer', ], 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ], } diff --git a/backend/backend/settings_live.py b/backend/backend/settings_live.py index bf26f01e..e382cc22 100644 --- a/backend/backend/settings_live.py +++ b/backend/backend/settings_live.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'api', 'rest_framework', + 'rest_framework.authtoken', 'channels', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -104,7 +105,11 @@ ], 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', - ] + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ], } CSRF_COOKIE_SECURE = True @@ -184,7 +189,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@20240531', + release='savageaim@20240629', ) # Channels diff --git a/backend/backend/test_settings.py b/backend/backend/test_settings.py index 342e8663..7791314f 100644 --- a/backend/backend/test_settings.py +++ b/backend/backend/test_settings.py @@ -33,6 +33,7 @@ INSTALLED_APPS = [ 'api', 'rest_framework', + 'rest_framework.authtoken', 'channels', 'django.contrib.admin', 'django.contrib.auth', @@ -105,6 +106,10 @@ 'rest_framework.renderers.JSONRenderer', ], 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ], } diff --git a/frontend/.env b/frontend/.env index 22ee196b..e6c873a2 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1 @@ -VUE_APP_VERSION="20240531" +VUE_APP_VERSION="20240629" diff --git a/frontend/public/job_icons/PCT.webp b/frontend/public/job_icons/PCT.webp new file mode 100644 index 00000000..224d6eb7 Binary files /dev/null and b/frontend/public/job_icons/PCT.webp differ diff --git a/frontend/public/job_icons/VPR.webp b/frontend/public/job_icons/VPR.webp new file mode 100644 index 00000000..61af777e Binary files /dev/null and b/frontend/public/job_icons/VPR.webp differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5f9d2506..3e3e543a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -78,25 +78,25 @@ export default class App extends Vue { // Do all the set up and handling of the websocket const sock = new WebSocket(`${process.env.VUE_APP_WS_URL}/ws/updates/`) - sock.onmessage = (msg: MessageEvent) => { + sock.onmessage = async (msg: MessageEvent) => { const payload = JSON.parse(msg.data) as SocketPayload switch (payload.model) { case 'bis': break case 'character': - this.$store.dispatch('fetchCharacters') + await this.$store.dispatch('fetchCharacters') break case 'loot': break case 'notification': - this.$store.dispatch('fetchNotifications') + await this.$store.dispatch('fetchNotifications') break case 'settings': - this.$store.dispatch('fetchUser') + await this.$store.dispatch('fetchUser') break case 'team': - this.$store.dispatch('fetchTeams') + await this.$store.dispatch('fetchTeams') break default: Vue.notify({ text: `Unexpected packet model "${payload.model}" received.`, type: 'is-warning' }) diff --git a/frontend/src/components/modals/changelog.vue b/frontend/src/components/modals/changelog.vue index 774ad847..bd02b9cb 100644 --- a/frontend/src/components/modals/changelog.vue +++ b/frontend/src/components/modals/changelog.vue @@ -12,65 +12,45 @@

{{ version }}

-
expand_more The Final Pre-Dawntrail (Non-Bugfix) Release expand_more
-

Good day fellow Warrior of Light, I hope that this message finds you well.

-

I've been hard at work on some community requested features / changes, and on interesting new mechanics that I hope people will take advantage of for the new expansion!

-

As a result, these patch notes are pretty large, but I encourage you to at least scroll through and read the various headings!

-

Thank you to all who've been using my little passion project through Endwalker, and may you all have a good time in Dawntrail!

- -
expand_more Streamlined New User Experience expand_more
-

- In the interest of making things easier for new Users to sign up and join their Teams, the following changes have been implemented to the system; -

+
expand_more Dawntrail Release expand_more
+

Happy first new expansion for SavageAim, and a happy 7.0 to all of you wonderful gamers out there!

+

+ Added two new Jobs! + + Viper Job Icon + + + Pictomancer Job Icon +

+

The current icons are temporary. Once XIVAPI's repo gets the fancy shaded ones, I'll update them!

-
expand_more Import from Lodestone expand_more
+
expand_more FFXIV 7.0 expand_more

- It is now possible to import your current gear from Lodestone when creating/editing BIS Lists! + Added Gear associated with Dawntrail's release;

+

Names have been kept out of the changelog to avoid spoilers, so please be careful when updating your current gear to ensure you don't spoil yourself!

-
expand_more Loot Manager - Solver expand_more
-

A new BETA feature has been added to the Loot Manager page; the Loot Solver.

-

This feature aims to "solve" loot distibution by handing out loot fairly but in a way that ensures you spend the least number of weeks on each fight as possible.

-

It is accessible in a dropdown box on the Loot Manager page, and you can see a full set of distribution for all of the required kills.

-

People using the Per-Fight Loot Manager (accessible in User Settings) can also auto-assign the week's loot into the slots, allowing for an easy save if you're following it.

-

For more details, please see the wiki page about this feature!

-

Because it is a Beta feature, there could be some algorithmic inconsistencies. If there are any issues or bugs or anything, please report them on the Discord!

+
expand_more API Keys expand_more
+

Users can now generate personal API Keys in the User Settings page.

+

You can use these keys to access your data from outside of the website, like with the soon-to-be-released Dalamud plugin! (please look forward to it)

+

Be sure to regenerate your key if you feel like there is a chance it has been compromised!

-
expand_more BIS List Actions Revamp expand_more
+
expand_more Fixes & Improvements expand_more

- In the interest of improving UX, Action buttons that only affect part of the BIS Form have been moved to the relevant sections: + Improved handling for importing gear where the name on the platform is for a collection of items that have their own names. + This should be a massive improvement for people who use the automated imports from places like Etro or the Lodestone.

-

The Actions box now only contains the Create/Save buttons, and the header of this box has been removed since it's now irrelevant to have one.

- -
expand_more Fixes & Improvements expand_more
-

Attempting to optimize queries on the Loot Manager page to improve performance!

-

Converted PNG images to WebP format to give smaller images to download!

-

Visiting a page that requires being logged in will now redirect you to the same page after you have logged in!

- -
expand_more Next Steps expand_more
-

SavageAim will be going for as long as I'm playing XIV, so this isn't some sort of closing down message!

-

As always, I will be more than happy to talk through / work on feature requests from the community! After all, SavageAim wouldn't be this good without ideas from y'all!

-

However, I would also like to try branching out.

-

I'm thinking the next thing I could work on would be some sort of plugin that would allow you to sync current gear directly from in game.

-

I have no idea how it would work or how much work it would involve but it would be something that I see myself using and that's all the motivation I need!

-

If any of you have any other ideas you'd like to see within the SavageAim ecosystem, please don't hesitate to send a message!

@@ -85,3 +65,10 @@ export default class Changelog extends Vue { } } + + diff --git a/frontend/src/components/settings/user_details.vue b/frontend/src/components/settings/user_details.vue index 579dc359..cdcb02c6 100644 --- a/frontend/src/components/settings/user_details.vue +++ b/frontend/src/components/settings/user_details.vue @@ -1,19 +1,40 @@ diff --git a/frontend/src/interfaces/user.ts b/frontend/src/interfaces/user.ts index 23a1dd90..a85f38c3 100644 --- a/frontend/src/interfaces/user.ts +++ b/frontend/src/interfaces/user.ts @@ -5,6 +5,7 @@ export default interface User { id: number | null, loot_manager_version: string, notifications: NotificationSettings, + token: string | null, theme: string, username: string, } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index e2e2ec5a..53f8b89a 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -30,7 +30,7 @@ Sentry.init({ Vue, dsn: 'https://06f41b525a40497a848fb726f6d03244@o242258.ingest.sentry.io/6180221', logErrors: true, - release: 'savageaim@20240531', + release: 'savageaim@20240629', integrations: [ new Sentry.BrowserTracing({ routingInstrumentation: Sentry.vueRouterInstrumentation(router), diff --git a/frontend/src/store.ts b/frontend/src/store.ts index fb8c3f05..2b394ff2 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -47,6 +47,7 @@ const DEFAULT_USER = { verify_fail: true, verify_success: true, }, + token: null, theme: 'beta', username: '', } diff --git a/frontend/src/views/settings.vue b/frontend/src/views/settings.vue index 61762652..98b2b4b9 100644 --- a/frontend/src/views/settings.vue +++ b/frontend/src/views/settings.vue @@ -44,6 +44,7 @@