From 23b6e67309c981fdc39621bc9377dafa7d15d281 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 14:39:13 +0200 Subject: [PATCH 01/28] add new footer icons --- frontend/src/components/footer.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/footer.vue b/frontend/src/components/footer.vue index f8fe6f75..7dff74b5 100644 --- a/frontend/src/components/footer.vue +++ b/frontend/src/components/footer.vue @@ -8,9 +8,11 @@ + api menu_book update code + extension Discord Logo

Savage Aim release {{ $store.state.version }}, by Eira Erikawa (Lich)

FINAL FANTASY XIV ©2010 - {{ currentYear }} SQUARE ENIX CO., LTD. All Rights Reserved.

From 7027e498a1aa0b4373c358a2271a32edc18b1df5 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 14:41:13 +0200 Subject: [PATCH 02/28] add spectacular to the requirements --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 15e6a0ea..5c0bc843 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -27,6 +27,7 @@ Deprecated==1.2.13 Django==3.2.25 django-allauth==0.47.0 djangorestframework==3.14.0 +drf-spectacular==0.27.2 gunicorn==22.0.0 hiredis==2.0.0 hyperlink==21.0.0 From 46a1189e6009efbd5ec8f8ecbd721741289907b9 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 15:32:17 +0200 Subject: [PATCH 03/28] moved version info to backend/__init__ --- backend/backend/__init__.py | 2 ++ bump.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index 171a200d..fc6feb34 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -1,3 +1,5 @@ # Make sure our celery app is run on load from .celery import app as celery_app + +VERSION = '20240629' diff --git a/bump.sh b/bump.sh index e6099a5f..67218827 100644 --- a/bump.sh +++ b/bump.sh @@ -3,7 +3,7 @@ # Function to handle doing the bump details do_bump() { # Just use sed to replace the current version with the new one in the files that need it - sed -i "s/$CURRENT_VERSION/$NEW_VERSION/" frontend/.env frontend/src/main.ts backend/backend/settings_live.py + sed -i "s/$CURRENT_VERSION/$NEW_VERSION/" frontend/.env frontend/src/main.ts backend/backend/__init__.py } From bdf5023f6e4f27591b53d08877681d9964203ab9 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 15:32:39 +0200 Subject: [PATCH 04/28] adding spectacular to settings/urls files --- backend/backend/settings.py | 13 +++++++++++++ backend/backend/settings_live.py | 17 ++++++++++++++++- backend/backend/urls.py | 3 +++ backend/backend/urls_live.py | 3 +++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 031250e2..a936cd1a 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -11,6 +11,7 @@ """ from os import environ from pathlib import Path +from . import VERSION # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -48,6 +49,9 @@ 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.discord', + + # API Schema + 'drf_spectacular', ] MIDDLEWARE = [ @@ -110,8 +114,17 @@ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ], + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } +SPECTACULAR_SETTINGS = { + 'TITLE': 'SavageAim', + 'DESCRIPTION': 'FFXIV BIS Management Website', + 'VERSION': VERSION, + 'SERVE_INCLUDE_SCHEMA': False, + 'SERVERS': [{'url': 'https://savageaim.com/backend/api', 'description': 'Main API Url'}], + 'SCHEMA_PATH_PREFIX': '/backend/api', +} # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators diff --git a/backend/backend/settings_live.py b/backend/backend/settings_live.py index e382cc22..c0503a48 100644 --- a/backend/backend/settings_live.py +++ b/backend/backend/settings_live.py @@ -13,6 +13,8 @@ from os import environ from pathlib import Path from sentry_sdk.integrations.django import DjangoIntegration +from . import VERSION + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -60,6 +62,9 @@ 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.discord', + + # API Schema + 'drf_spectacular', ] MIDDLEWARE = [ @@ -110,6 +115,16 @@ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ], + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'SavageAim', + 'DESCRIPTION': 'FFXIV BIS Management Website', + 'VERSION': VERSION, + 'SERVE_INCLUDE_SCHEMA': False, + 'SERVERS': [{'url': 'https://savageaim.com/backend/api', 'description': 'Main API Url'}], + 'SCHEMA_PATH_PREFIX': '/backend/api', } CSRF_COOKIE_SECURE = True @@ -189,7 +204,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@20240629', + release=f'savageaim@{VERSION}', ) # Channels diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 0fc93847..27cceafd 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -20,12 +20,15 @@ from django.http import HttpResponse from django.urls import path, include from django.views.generic.base import RedirectView +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView patterns = [ path('admin/', admin.site.urls), path('api/', include(('api.urls', 'api'))), path('health/', lambda _: HttpResponse()), path('logout/', LogoutView.as_view()), + path('schema.yaml', SpectacularAPIView.as_view(), name='schema'), + path('schema/', SpectacularSwaggerView.as_view(url_name='schema'), name='schema-ui'), # Auth stuff (TODO - replace this because it's sorta workaroundy) path('accounts/', include(discord_urls)), diff --git a/backend/backend/urls_live.py b/backend/backend/urls_live.py index a1271c99..76465722 100644 --- a/backend/backend/urls_live.py +++ b/backend/backend/urls_live.py @@ -19,11 +19,14 @@ from django.http import HttpResponse from django.urls import path, include from django.views.generic.base import RedirectView +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView patterns = [ path('api/', include(('api.urls', 'api'))), path('health/', lambda _: HttpResponse()), path('logout/', LogoutView.as_view()), + path('schema.yaml', SpectacularAPIView.as_view(), name='schema'), + path('schema/', SpectacularSwaggerView.as_view(url_name='schema'), name='schema-ui'), # Auth stuff (TODO - replace this because it's sorta workaroundy) path('accounts/', include(discord_urls)), From 29e6972375f11b32a059ecda5c5f179d3e5adbac Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 15:32:55 +0200 Subject: [PATCH 05/28] set up schema details for bis list views --- backend/api/views/bis_list.py | 90 ++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/backend/api/views/bis_list.py b/backend/api/views/bis_list.py index 1994f1c3..888efe68 100644 --- a/backend/api/views/bis_list.py +++ b/backend/api/views/bis_list.py @@ -7,6 +7,9 @@ from typing import List # lib from django.db.models.deletion import ProtectedError +from rest_framework import serializers +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema from rest_framework.request import Request from rest_framework.response import Response # local @@ -35,10 +38,19 @@ class BISListCollection(BISListBaseView): """ Allows for the creation of new BIS Lists """ - + queryset = BISList + serializer_class = BISListModifySerializer + + @extend_schema( + tags=['bis_list'], + responses={ + 201: inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), + 404: OpenApiResponse(description='The given Character ID did not belong to a valid Character owned by the requesting User'), + }, + ) def post(self, request: Request, character_id: int) -> Response: """ - Create a BIS List belonging to the specified character + Create a new BIS List belonging to the specified Character. """ try: char = Character.objects.get(pk=character_id, user=request.user) @@ -65,9 +77,22 @@ class BISListResource(BISListBaseView): Allows for the reading and updating of a BISList """ + @extend_schema( + tags=['bis_list'], + responses={ + 200: BISListSerializer, + 404: OpenApiResponse( + description=( + 'The given Character ID did not belong to a valid Character owned by the requesting User.' + '\nAlternatively the BISList ID does not belong to a valid Character.' + ) + ), + }, + + ) def get(self, request: Request, character_id: int, pk: int) -> Response: """ - Read a BISList + Read a specific BISList instance, belonging to a specified Character. """ try: char = Character.objects.get(pk=character_id, user=request.user) @@ -82,9 +107,22 @@ def get(self, request: Request, character_id: int, pk: int) -> Response: data = BISListSerializer(instance=obj).data return Response(data) + @extend_schema( + tags=['bis_list'], + request=BISListModifySerializer, + responses={ + 204: OpenApiResponse(description='BISList was updated successfully!'), + 404: OpenApiResponse( + description=( + 'The given Character ID did not belong to a valid Character owned by the requesting User.' + '\nAlternatively the BISList ID does not belong to a valid Character.' + ) + ), + }, + ) def put(self, request: Request, character_id: int, pk: int) -> Response: """ - Update an existing BISList + Update the details of a BISList that belongs to a specified Character. """ try: char = Character.objects.get(pk=character_id, user=request.user) @@ -120,9 +158,33 @@ class BISListDelete(APIView): Has a GET request to get information on if we can delete this BISList. """ + @extend_schema( + tags=['bis_list'], + responses={ + 200: OpenApiResponse( + response=inline_serializer( + 'DeleteReadResponse', + { + 'id': serializers.IntegerField(), + 'member': serializers.IntegerField(), + 'name': serializers.CharField(), + }, + many=True, + ), + description='A list of places where the BIS List is in use. If this list is empty, the BISList is safe to delete!', + ), + 404: OpenApiResponse( + description=( + 'The given Character ID did not belong to a valid Character owned by the requesting User.' + '\nAlternatively the BISList ID does not belong to a valid Character.' + ) + ), + }, + + ) def get(self, request: Request, character_id: int, pk: int) -> Response: """ - Check if we are able to delete a sepcified BIS List. + Check if we are able to delete a specified Character's BIS List. We can only do this if the BIS List is not in use in any Teams. If it is, return names and IDs of the Teams it's in use in so we can provide links in the frontend. @@ -148,9 +210,25 @@ def get(self, request: Request, character_id: int, pk: int) -> Response: return Response(info) + @extend_schema( + tags=['bis_list'], + request=BISListModifySerializer, + responses={ + 204: OpenApiResponse(description='BISList was deleted successfully!'), + 400: OpenApiResponse(description='BISList could not be deleted.'), + 404: OpenApiResponse( + description=( + 'The given Character ID did not belong to a valid Character owned by the requesting User.' + '\nAlternatively the BISList ID does not belong to a valid Character.' + ) + ), + }, + ) def delete(self, request: Request, character_id: int, pk: int) -> Response: """ - Delete the BISList from the DB, ensuring that we can. + Delete the BISList from the DB. + + If the BISList is not currently deleteable, this method will return a 400 error. """ try: char = Character.objects.get(pk=character_id, user=request.user) From f4e2cd7f2d499851076b97e97bf304e87bcbe55d Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 15:46:45 +0200 Subject: [PATCH 06/28] update servers list --- backend/backend/settings.py | 2 +- backend/backend/settings_live.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index a936cd1a..e3090a29 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -122,7 +122,7 @@ 'DESCRIPTION': 'FFXIV BIS Management Website', 'VERSION': VERSION, 'SERVE_INCLUDE_SCHEMA': False, - 'SERVERS': [{'url': 'https://savageaim.com/backend/api', 'description': 'Main API Url'}], + 'SERVERS': [{'url': 'https://savageaim.com/', 'description': 'Main Site'}], 'SCHEMA_PATH_PREFIX': '/backend/api', } diff --git a/backend/backend/settings_live.py b/backend/backend/settings_live.py index c0503a48..089ecce3 100644 --- a/backend/backend/settings_live.py +++ b/backend/backend/settings_live.py @@ -123,7 +123,7 @@ 'DESCRIPTION': 'FFXIV BIS Management Website', 'VERSION': VERSION, 'SERVE_INCLUDE_SCHEMA': False, - 'SERVERS': [{'url': 'https://savageaim.com/backend/api', 'description': 'Main API Url'}], + 'SERVERS': [{'url': 'https://savageaim.com/', 'description': 'Main Site'}], 'SCHEMA_PATH_PREFIX': '/backend/api', } From 4462d588d8bf1c377ff28748184c0c8b663e7a2e Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 16:02:59 +0200 Subject: [PATCH 07/28] fixed bis_list docs --- backend/api/views/bis_list.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/api/views/bis_list.py b/backend/api/views/bis_list.py index 888efe68..049acfe9 100644 --- a/backend/api/views/bis_list.py +++ b/backend/api/views/bis_list.py @@ -44,7 +44,10 @@ class BISListCollection(BISListBaseView): @extend_schema( tags=['bis_list'], responses={ - 201: inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), + 201: OpenApiResponse( + response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), + description='The ID of the created BISList', + ), 404: OpenApiResponse(description='The given Character ID did not belong to a valid Character owned by the requesting User'), }, ) @@ -88,7 +91,6 @@ class BISListResource(BISListBaseView): ) ), }, - ) def get(self, request: Request, character_id: int, pk: int) -> Response: """ @@ -163,7 +165,7 @@ class BISListDelete(APIView): responses={ 200: OpenApiResponse( response=inline_serializer( - 'DeleteReadResponse', + 'BISListDeleteReadResponse', { 'id': serializers.IntegerField(), 'member': serializers.IntegerField(), @@ -212,7 +214,6 @@ def get(self, request: Request, character_id: int, pk: int) -> Response: @extend_schema( tags=['bis_list'], - request=BISListModifySerializer, responses={ 204: OpenApiResponse(description='BISList was deleted successfully!'), 400: OpenApiResponse(description='BISList could not be deleted.'), From 9d7de89fd7bfd716218ab3a8b3a0899827bbac42 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 16:03:25 +0200 Subject: [PATCH 08/28] added documentation to character views --- backend/api/serializers/character.py | 7 ++ backend/api/views/character.py | 117 ++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/backend/api/serializers/character.py b/backend/api/serializers/character.py index d89ba413..eab15c73 100644 --- a/backend/api/serializers/character.py +++ b/backend/api/serializers/character.py @@ -5,6 +5,7 @@ from re import compile from typing import Dict, List, Union # lib +from drf_spectacular.utils import extend_schema_field, inline_serializer from rest_framework import serializers # local from api.models import Character @@ -35,6 +36,12 @@ def get_proxy(self, char: Character) -> bool: """ return char.user is None + @extend_schema_field( + inline_serializer( + 'CharacterCollectionBISListSummary', + {'id': serializers.IntegerField(), 'name': serializers.CharField()}, + ), + ) def get_bis_lists(self, char: Character) -> List[Dict[str, Union[str, int]]]: """ Return summaries of bis lists diff --git a/backend/api/views/character.py b/backend/api/views/character.py index d4990854..f5531c40 100644 --- a/backend/api/views/character.py +++ b/backend/api/views/character.py @@ -5,6 +5,9 @@ """ # lib +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -24,19 +27,35 @@ class CharacterCollection(APIView): Provides list and create methods. """ + @extend_schema( + responses={ + 200: OpenApiResponse( + response=CharacterCollectionSerializer(many=True), + description='List of all the Characters belonging to the User.', + ), + }, + ) def get(self, request: Request) -> Response: """ - Return a list of Characters belonging to a certain User + Retrieve all of the Characters belonging to the requesting User. """ # Permissions won't allow this method to be run by non-auth'd users objs = Character.objects.filter(user=request.user) data = CharacterCollectionSerializer(objs, many=True).data return Response(data) + @extend_schema( + request=CharacterCollectionSerializer, + responses={ + 201: OpenApiResponse( + response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), + description='The ID of the created Character', + ), + }, + ) def post(self, request: Request) -> Response: """ - Characters are verified via celery. - This view will create the data in the DB + Create a new, un-verified Character, that belongs to the requesting User. """ # Put the sent data into the serializer for validation serializer = CharacterCollectionSerializer(data=request.data) @@ -55,11 +74,19 @@ class CharacterResource(APIView): Handling character specific requests """ + @extend_schema( + responses={ + 200: CharacterDetailsSerializer, + 404: OpenApiResponse( + description='The given Character ID did not belong to a valid Character owned by the requesting User.', + ), + }, + ) def get(self, request: Request, pk: int) -> Response: """ - Read the data of a Character. + Read the data of a specified Character. - This view will return full data, including a list of gearsets and teams and such + This endpoint will return the full data of the Character, including associated BISLists and Teams. """ try: obj = Character.objects.get(pk=pk, user=request.user) @@ -69,9 +96,20 @@ def get(self, request: Request, pk: int) -> Response: data = CharacterDetailsSerializer(instance=obj).data return Response(data) + @extend_schema( + request=CharacterUpdateSerializer, + responses={ + 204: OpenApiResponse(description='Character was successfully updated!'), + 404: OpenApiResponse( + description='The given Character ID did not belong to a valid Character owned by the requesting User.', + ), + }, + ) def put(self, request: Request, pk: int, partial: bool = False) -> Response: """ - Update certain fields of a Character + Update the information of a Character. + + Requires sending the full object to update. """ try: obj = Character.objects.get(pk=pk, user=request.user) @@ -92,7 +130,21 @@ def put(self, request: Request, pk: int, partial: bool = False) -> Response: return Response(status=204) + @extend_schema( + request=CharacterUpdateSerializer, + responses={ + 204: OpenApiResponse(description='Character was successfully updated!'), + 404: OpenApiResponse( + description='The given Character ID did not belong to a valid Character owned by the requesting User.', + ), + }, + ) def patch(self, request: Request, pk: int) -> Response: + """ + Update the information of a Character. + + Can handle partial update requests, only changed fields need to be sent to this endpoint. + """ return self.put(request, pk, True) @@ -101,9 +153,20 @@ class CharacterVerification(APIView): A class specifically for triggering the verification process """ + @extend_schema( + request=CharacterUpdateSerializer, + responses={ + 202: OpenApiResponse(description='Character verification has been requested!'), + 404: OpenApiResponse( + description='The given Character ID did not belong to a valid, unverified, Character owned by the requesting User.', + ), + }, + ) def post(self, request: Request, pk: int) -> Response: """ - On receipt of this request, we add the given character to the queue for verification (if needed) + Trigger the verification process for a given Character. + + The process involves checking that the token associated with the Character is present on the Lodestone profile. """ try: Character.objects.get(pk=pk, user=request.user, verified=False) @@ -122,9 +185,33 @@ class CharacterDelete(APIView): Has a GET request to get what will be affected by the deletion of this Character. """ + @extend_schema( + responses={ + 200: OpenApiResponse( + response=inline_serializer( + 'CharacterDeleteReadResponse', + { + 'lead': serializers.BooleanField(), + 'members': serializers.IntegerField(), + 'name': serializers.CharField(), + }, + many=True, + ), + description='A list of Teams that will be affected by the Character\'s deletion.', + ), + 404: OpenApiResponse( + description='The given Character ID did not belong to a valid Character owned by the requesting User.', + ), + }, + + ) def get(self, request: Request, pk: int) -> Response: """ - Check through the DB for any information regarding the Character in question + Check what will happen if this Character would be deleted. + Returns a list of Teams that the Character is in, including the following fields; + - name: The name of the Team + - lead: Whether the Character is the leader of the Team. + - members: How many members are in the Team. """ try: obj = Character.objects.get(pk=pk, user=request.user) @@ -142,9 +229,21 @@ def get(self, request: Request, pk: int) -> Response: return Response(info) + @extend_schema( + responses={ + 204: OpenApiResponse(description='Character was deleted successfully!'), + 404: OpenApiResponse( + description='The given Character ID did not belong to a valid Character owned by the requesting User.', + ), + }, + ) def delete(self, request: Request, pk: int) -> Response: """ - Delete the Character from the DB, doing all the things that are stated will happen + Delete the Character from the system. + This can also trigger the following effects where appropriate; + - Disbanding Teams where the Character is the only Member + - Handing leadership of a Team to another Member if the Character is the leader + - Leaving the Team if the Character is not the leader. """ try: obj = Character.objects.get(pk=pk, user=request.user) From fc35d90c60630ab8375846f8381323ebc3f83a3c Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 16:58:16 +0200 Subject: [PATCH 09/28] documenting etro import view --- backend/api/views/etro.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/backend/api/views/etro.py b/backend/api/views/etro.py index 3dfe0220..268588e4 100644 --- a/backend/api/views/etro.py +++ b/backend/api/views/etro.py @@ -5,6 +5,9 @@ from typing import Dict # lib import coreapi +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -34,9 +37,39 @@ class EtroImport(ImportAPIView): Import an etro gearset using coreapi and levenshtein distance """ + @extend_schema( + responses={ + 200: OpenApiResponse( + response=inline_serializer( + 'EtroImportResponse', + { + 'mainhand': serializers.IntegerField(), + 'offhand': serializers.IntegerField(), + 'head': serializers.IntegerField(), + 'body': serializers.IntegerField(), + 'hands': serializers.IntegerField(), + 'legs': serializers.IntegerField(), + 'feet': serializers.IntegerField(), + 'earrings': serializers.IntegerField(), + 'necklace': serializers.IntegerField(), + 'bracelet': serializers.IntegerField(), + 'right_ring': serializers.IntegerField(), + 'left_ring': serializers.IntegerField(), + }, + ), + description='Map of slot name to the IDs of the Gear objects that *should* match the item on the slot of the Etro set.' + ), + 400: OpenApiResponse( + description='An error occurred on Etro\'s end. Error message from Etro will be included in the response', + response=inline_serializer('EtroImport400Response', {'message': serializers.CharField()}), + ), + }, + ) def get(self, request: Request, id: str) -> Response: """ - Given an Etro Gearset ID, load the equipment from it + Attempt to load the information of an Etro Gearset, and turn it into information SavageAim can use. + + Names are mapped to Gear instances using name similarity, and is not 100% guaranteed to be correct. """ # Instantiate a Client instance for CoreAPI client = coreapi.Client() From ee9d37b500af4c24767253e8f02f5884f3558978 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 17:06:49 +0200 Subject: [PATCH 10/28] added documentation to gear views --- backend/api/views/gear.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/api/views/gear.py b/backend/api/views/gear.py index 320ab178..951495c7 100644 --- a/backend/api/views/gear.py +++ b/backend/api/views/gear.py @@ -3,6 +3,8 @@ """ # lib +from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter, OpenApiResponse +from rest_framework import serializers from rest_framework.views import APIView from rest_framework.permissions import AllowAny from rest_framework.request import Request @@ -18,14 +20,20 @@ class GearCollection(APIView): """ Get a list of Gears in the system """ + queryset = Gear + serializer_class = GearSerializer(many=True) permission_classes = [AllowAny] + @extend_schema( + parameters=[ + OpenApiParameter('item_level_min', int, description='Set a minimum item level (inclusive) of Gear returned.'), + OpenApiParameter('item_level_max', int, description='Set a maximum item level (inclusive) of Gear returned.'), + ], + ) def get(self, request: Request) -> Response: """ - List the Gear items. - - We should allow filtering by item levels, both gte and lte + Retrieve a list of Gear objects from the system. """ objs = Gear.objects.all() @@ -56,9 +64,14 @@ class ItemLevels(APIView): permission_classes = [AllowAny] + @extend_schema( + responses={ + 200: inline_serializer('ItemLevels', {'min': serializers.IntegerField(), 'max': serializers.IntegerField()}), + }, + ) def get(self, request: Request) -> Response: """ - Fetch the min and max item level for the system + Retrieve the minimum and maximum Item Levels in the entire DB. """ objs = Gear.objects.values('item_level') From 78dd57131ef6fac1f927d474800392ba4f087714 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 17:12:17 +0200 Subject: [PATCH 11/28] add documentation to job views --- backend/api/views/job.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/api/views/job.py b/backend/api/views/job.py index 51a19ded..337f0db0 100644 --- a/backend/api/views/job.py +++ b/backend/api/views/job.py @@ -18,7 +18,8 @@ class JobCollection(APIView): """ Get a list of Jobs in the system """ - + queryset = Job + serializer_class = JobSerializer(many=True) permission_classes = [AllowAny] def get(self, request: Request) -> Response: @@ -35,12 +36,16 @@ class JobSolverSortCollection(APIView): """ Get a list of Jobs in the system, ordered by the default order of how jobs are sorted in the solver by default """ - + queryset = Job + serializer_class = JobSerializer(many=True) permission_classes = [AllowAny] def get(self, request: Request) -> Response: """ - Return the full list of Jobs in default ordering (Melee > Ranged > Caster > Tanks > Healer) + Return the full list of Jobs in Loot Solver default order (Melee > Ranged > Caster > Tank > Healer) + + The point of this endpoint is to allow the TeamSettings page to display the ordering of Jobs within the Loot Solver sorting. + Teams only store overrides, so this view is available to retrieve the default, and then overrides are moved around as needed. """ # Permissions won't allow this method to be run by non-auth'd users objs = Job.get_in_solver_order() From 809fc52a421475ad1523b5abd2c3f93e9b741844 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 17:21:54 +0200 Subject: [PATCH 12/28] added docs to lodestone views --- backend/api/views/lodestone.py | 48 ++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/backend/api/views/lodestone.py b/backend/api/views/lodestone.py index 8ceffccd..7f9e1265 100644 --- a/backend/api/views/lodestone.py +++ b/backend/api/views/lodestone.py @@ -9,6 +9,9 @@ """ # lib +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response @@ -23,9 +26,24 @@ class LodestoneResource(APIView): Retrieve character data for a given Character ID """ + @extend_schema( + responses={ + 200: inline_serializer( + 'LodestoneCharacterScrapeResponse', + { + 'avatar_url': serializers.URLField(), + 'name': serializers.CharField(), + 'world': serializers.CharField(), + 'dc': serializers.CharField(), + }, + ), + 400: OpenApiResponse(description='An error occurred when retrieving info from the Lodestone.'), + 404: OpenApiResponse(description='Character ID was not found on the Lodestone.'), + }, + ) def get(self, request: Request, character_id: str) -> Response: """ - Scrape the Lodestone and return the found character data + Read the given Character's Lodestone page, and scrape the required information for the system. """ scraper = LodestoneScraper.get_instance() try: @@ -43,9 +61,35 @@ class LodestoneGearImport(ImportAPIView): Given a Character ID, retrieve its current job and gear details, which the view code can turn into valid gear items """ + @extend_schema( + responses={ + 200: inline_serializer( + 'LodestoneGearImportResponse', + { + 'mainhand': serializers.IntegerField(), + 'offhand': serializers.IntegerField(), + 'head': serializers.IntegerField(), + 'body': serializers.IntegerField(), + 'hands': serializers.IntegerField(), + 'legs': serializers.IntegerField(), + 'feet': serializers.IntegerField(), + 'earrings': serializers.IntegerField(), + 'necklace': serializers.IntegerField(), + 'bracelet': serializers.IntegerField(), + 'right_ring': serializers.IntegerField(), + 'left_ring': serializers.IntegerField(), + }, + ), + 400: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), + 404: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), + 406: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), + }, + ) def get(self, request: Request, character_id: str, expected_job: str) -> Response: """ - Scrape the Lodestone and return the gear and job information + Read the given Character's Lodestone page, and scrape their currently equipped gear. + + If the gear on the site is not useable by the `expected_job`, this view will return an error. """ try: Job.objects.get(id=expected_job) From b30872e0ef21e7ad378cf0d70b9bbedb33939a0d Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 17:34:27 +0200 Subject: [PATCH 13/28] added docs to team_loot page --- backend/api/views/loot_solver.py | 49 +++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/api/views/loot_solver.py b/backend/api/views/loot_solver.py index d8cc593f..89cc2352 100644 --- a/backend/api/views/loot_solver.py +++ b/backend/api/views/loot_solver.py @@ -10,6 +10,9 @@ # lib from django.core.exceptions import ValidationError from django.db.models import QuerySet +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -404,9 +407,53 @@ def _get_fourth_floor_data(self, history: QuerySet[Loot], team_size: int, non_lo 'mounts': team_size - mounts_obtained, } + @extend_schema( + tags=['team_loot'], + responses={ + 200: inline_serializer( + 'LootSolverResponse', + { + 'first_floor': inline_serializer( + 'LootSolverFirstFloorResponse', + { + slot: serializers.ListField(child=serializers.IntegerField()) + for slot in FIRST_FLOOR_SLOTS + }, + ), + 'second_floor': inline_serializer( + 'LootSolverSecondFloorResponse', + { + slot: serializers.ListField(child=serializers.IntegerField()) + for slot in SECOND_FLOOR_SLOTS + }, + ), + 'third_floor': inline_serializer( + 'LootSolverThirdFloorResponse', + { + slot: serializers.ListField(child=serializers.IntegerField()) + for slot in THIRD_FLOOR_SLOTS + }, + ), + 'fourth_floor': inline_serializer( + 'LootSolverFourthFloorResponse', + { + 'weapons': serializers.IntegerField(), + 'mounts': serializers.IntegerField(), + }, + ), + } + ) + }, + ) def get(self, request: Request, team_id: str) -> Response: """ - Fetch the current solver information for the team + Run the Loot Solver for the specified Team. + + The Loot Solver is a system that attempts to generate an ordering for each piece of loot every week, + in order to attempt and finish gearing as fast as possible for each of the first three fights of a Tier. + + For the first three floors, each slot will have a list of TeamMember IDs in the order you should hand them out. + For the final fight, it simply returns the number of Weapons and Mounts required. """ try: obj = Team.objects.select_related( From 0ccb8482b439185198466b4df0ac48c37348ed18 Mon Sep 17 00:00:00 2001 From: freyamade Date: Thu, 11 Jul 2024 19:25:30 +0200 Subject: [PATCH 14/28] updating changelog --- frontend/src/components/modals/changelog.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/modals/changelog.vue b/frontend/src/components/modals/changelog.vue index 9a71474c..0dcc55dc 100644 --- a/frontend/src/components/modals/changelog.vue +++ b/frontend/src/components/modals/changelog.vue @@ -17,10 +17,16 @@

Currently, it must be installed as a custom plugin repo due to Dalamud Team's build systems being on hold

The code and installation instructions are located here!

If there are any questions or issues, please report them as you would anything with the website itself!

- - + +
expand_more API Schema expand_more
+

In the last release, API Keys were added to allow external access for possibly building an ecosystem of tools.

+

However, it only dawned on me recently that the API isn't obvious for anyone other than me.

+

There is now an OpenAPI schema available at https://savageaim.com/backend/schema/.

+

This set of documentation is mostly manually written so if there are any questions, feel free to ask on Discord/Github!

+
expand_more Fixes & Improvements expand_more

Improved fetching of Teams from the database to improve speed and lessen the load on the server.

+

Added "API Schema" and "Plugin" links to the footer icons.

From d0772f1b5a505547f78a14fa3ef8113e80fbe2fa Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 09:47:06 +0200 Subject: [PATCH 15/28] add concrete operation ids --- backend/api/views/bis_list.py | 5 +++++ backend/api/views/character.py | 9 ++++++++- backend/api/views/etro.py | 1 + backend/api/views/gear.py | 2 ++ backend/api/views/lodestone.py | 2 ++ backend/api/views/loot_solver.py | 1 + 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/api/views/bis_list.py b/backend/api/views/bis_list.py index 049acfe9..a0a3e5ef 100644 --- a/backend/api/views/bis_list.py +++ b/backend/api/views/bis_list.py @@ -42,6 +42,7 @@ class BISListCollection(BISListBaseView): serializer_class = BISListModifySerializer @extend_schema( + operation_id='bis_list_create', tags=['bis_list'], responses={ 201: OpenApiResponse( @@ -81,6 +82,7 @@ class BISListResource(BISListBaseView): """ @extend_schema( + operation_id='bis_list_read', tags=['bis_list'], responses={ 200: BISListSerializer, @@ -110,6 +112,7 @@ def get(self, request: Request, character_id: int, pk: int) -> Response: return Response(data) @extend_schema( + operation_id='bis_list_update', tags=['bis_list'], request=BISListModifySerializer, responses={ @@ -161,6 +164,7 @@ class BISListDelete(APIView): """ @extend_schema( + operation_id='bis_list_delete_check', tags=['bis_list'], responses={ 200: OpenApiResponse( @@ -213,6 +217,7 @@ def get(self, request: Request, character_id: int, pk: int) -> Response: return Response(info) @extend_schema( + operation_id='bis_list_delete', tags=['bis_list'], responses={ 204: OpenApiResponse(description='BISList was deleted successfully!'), diff --git a/backend/api/views/character.py b/backend/api/views/character.py index f5531c40..dd5faa9e 100644 --- a/backend/api/views/character.py +++ b/backend/api/views/character.py @@ -34,6 +34,7 @@ class CharacterCollection(APIView): description='List of all the Characters belonging to the User.', ), }, + operation_id='character_list', ) def get(self, request: Request) -> Response: """ @@ -52,6 +53,7 @@ def get(self, request: Request) -> Response: description='The ID of the created Character', ), }, + operation_id='character_create', ) def post(self, request: Request) -> Response: """ @@ -81,6 +83,7 @@ class CharacterResource(APIView): description='The given Character ID did not belong to a valid Character owned by the requesting User.', ), }, + operation_id='character_read', ) def get(self, request: Request, pk: int) -> Response: """ @@ -104,6 +107,7 @@ def get(self, request: Request, pk: int) -> Response: description='The given Character ID did not belong to a valid Character owned by the requesting User.', ), }, + operation_id='character_update', ) def put(self, request: Request, pk: int, partial: bool = False) -> Response: """ @@ -138,6 +142,7 @@ def put(self, request: Request, pk: int, partial: bool = False) -> Response: description='The given Character ID did not belong to a valid Character owned by the requesting User.', ), }, + operation_id='character_partial_update', ) def patch(self, request: Request, pk: int) -> Response: """ @@ -161,6 +166,7 @@ class CharacterVerification(APIView): description='The given Character ID did not belong to a valid, unverified, Character owned by the requesting User.', ), }, + operation_id='request_character_verification', ) def post(self, request: Request, pk: int) -> Response: """ @@ -203,7 +209,7 @@ class CharacterDelete(APIView): description='The given Character ID did not belong to a valid Character owned by the requesting User.', ), }, - + operation_id='character_delete_check', ) def get(self, request: Request, pk: int) -> Response: """ @@ -236,6 +242,7 @@ def get(self, request: Request, pk: int) -> Response: description='The given Character ID did not belong to a valid Character owned by the requesting User.', ), }, + operation_id='character_delete', ) def delete(self, request: Request, pk: int) -> Response: """ diff --git a/backend/api/views/etro.py b/backend/api/views/etro.py index 268588e4..490e3ed4 100644 --- a/backend/api/views/etro.py +++ b/backend/api/views/etro.py @@ -64,6 +64,7 @@ class EtroImport(ImportAPIView): response=inline_serializer('EtroImport400Response', {'message': serializers.CharField()}), ), }, + operation_id='import_gear_from_etro', ) def get(self, request: Request, id: str) -> Response: """ diff --git a/backend/api/views/gear.py b/backend/api/views/gear.py index 951495c7..8f65975d 100644 --- a/backend/api/views/gear.py +++ b/backend/api/views/gear.py @@ -30,6 +30,7 @@ class GearCollection(APIView): OpenApiParameter('item_level_min', int, description='Set a minimum item level (inclusive) of Gear returned.'), OpenApiParameter('item_level_max', int, description='Set a maximum item level (inclusive) of Gear returned.'), ], + operation_id='gear_list', ) def get(self, request: Request) -> Response: """ @@ -68,6 +69,7 @@ class ItemLevels(APIView): responses={ 200: inline_serializer('ItemLevels', {'min': serializers.IntegerField(), 'max': serializers.IntegerField()}), }, + operation_id='read_item_level_range', ) def get(self, request: Request) -> Response: """ diff --git a/backend/api/views/lodestone.py b/backend/api/views/lodestone.py index 7f9e1265..73bc0595 100644 --- a/backend/api/views/lodestone.py +++ b/backend/api/views/lodestone.py @@ -40,6 +40,7 @@ class LodestoneResource(APIView): 400: OpenApiResponse(description='An error occurred when retrieving info from the Lodestone.'), 404: OpenApiResponse(description='Character ID was not found on the Lodestone.'), }, + operation_id='lodestone_character_data_scrape', ) def get(self, request: Request, character_id: str) -> Response: """ @@ -84,6 +85,7 @@ class LodestoneGearImport(ImportAPIView): 404: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), 406: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), }, + operation_id='lodestone_scrape_character_current_gear', ) def get(self, request: Request, character_id: str, expected_job: str) -> Response: """ diff --git a/backend/api/views/loot_solver.py b/backend/api/views/loot_solver.py index 89cc2352..5f6f9b74 100644 --- a/backend/api/views/loot_solver.py +++ b/backend/api/views/loot_solver.py @@ -444,6 +444,7 @@ def _get_fourth_floor_data(self, history: QuerySet[Loot], team_size: int, non_lo } ) }, + operation_id='run_loot_solver', ) def get(self, request: Request, team_id: str) -> Response: """ From ed2d548c4846e57362e0066ec5abedd5d4ff8790 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 09:48:34 +0200 Subject: [PATCH 16/28] changing names to avoid duplicates --- backend/api/views/bis_list.py | 2 +- backend/api/views/character.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/api/views/bis_list.py b/backend/api/views/bis_list.py index a0a3e5ef..114ba6e0 100644 --- a/backend/api/views/bis_list.py +++ b/backend/api/views/bis_list.py @@ -46,7 +46,7 @@ class BISListCollection(BISListBaseView): tags=['bis_list'], responses={ 201: OpenApiResponse( - response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), + response=inline_serializer('BISListCreateResponse', {'id': serializers.IntegerField()}), description='The ID of the created BISList', ), 404: OpenApiResponse(description='The given Character ID did not belong to a valid Character owned by the requesting User'), diff --git a/backend/api/views/character.py b/backend/api/views/character.py index dd5faa9e..0a494901 100644 --- a/backend/api/views/character.py +++ b/backend/api/views/character.py @@ -49,7 +49,7 @@ def get(self, request: Request) -> Response: request=CharacterCollectionSerializer, responses={ 201: OpenApiResponse( - response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), + response=inline_serializer('CharacterCreateResponse', {'id': serializers.IntegerField()}), description='The ID of the created Character', ), }, From ad7cb3438c34d05778473f1c2863b75cf5067963 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 15:48:58 +0200 Subject: [PATCH 17/28] fixed inline serializer name --- backend/api/views/lodestone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/api/views/lodestone.py b/backend/api/views/lodestone.py index 73bc0595..a757ddaa 100644 --- a/backend/api/views/lodestone.py +++ b/backend/api/views/lodestone.py @@ -82,8 +82,8 @@ class LodestoneGearImport(ImportAPIView): }, ), 400: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), - 404: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), - 406: OpenApiResponse(response=inline_serializer('LodestoneImport400Response', {'message': serializers.CharField()})), + 404: OpenApiResponse(response=inline_serializer('LodestoneImport404Response', {'message': serializers.CharField()})), + 406: OpenApiResponse(response=inline_serializer('LodestoneImport406Response', {'message': serializers.CharField()})), }, operation_id='lodestone_scrape_character_current_gear', ) From 9ce4133c1989cce055e8f4f61079a879a4991e65 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 15:49:28 +0200 Subject: [PATCH 18/28] added docs to loot views moved loot/delete to a new URL to use POST instead of doing non-RESTful things --- backend/api/urls.py | 1 + backend/api/views/__init__.py | 3 +- backend/api/views/loot.py | 251 +++++++++++++++++- .../modals/confirmations/delete_loot.vue | 4 +- 4 files changed, 248 insertions(+), 11 deletions(-) diff --git a/backend/api/urls.py b/backend/api/urls.py index e859ac96..09549800 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -35,6 +35,7 @@ # Loot path('team//loot/', views.LootCollection.as_view(), name='loot_collection'), + path('team//loot/delete/', views.LootDelete.as_view(), name='loot_delete'), path('team//loot/bis/', views.LootWithBIS.as_view(), name='loot_with_bis'), # LootSolver diff --git a/backend/api/views/__init__.py b/backend/api/views/__init__.py index e992a614..34509dd9 100644 --- a/backend/api/views/__init__.py +++ b/backend/api/views/__init__.py @@ -4,7 +4,7 @@ from .gear import GearCollection, ItemLevels from .job import JobCollection, JobSolverSortCollection from .lodestone import LodestoneGearImport, LodestoneResource -from .loot import LootCollection, LootWithBIS +from .loot import LootCollection, LootDelete, LootWithBIS from .loot_solver import LootSolver from .notification import NotificationCollection, NotificationResource from .plugin import PluginImport @@ -36,6 +36,7 @@ 'LodestoneResource', 'LootCollection', + 'LootDelete', 'LootWithBIS', 'LootSolver', diff --git a/backend/api/views/loot.py b/backend/api/views/loot.py index 0ed283ce..7a670619 100644 --- a/backend/api/views/loot.py +++ b/backend/api/views/loot.py @@ -10,6 +10,9 @@ # lib from django.core.exceptions import ValidationError from django.db.models import QuerySet +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -25,6 +28,51 @@ PERMISSION_NAME = 'loot_manager' +# Define Serializers for the response info. +class GreedItemSerializer(serializers.Serializer): + bis_list_id = serializers.IntegerField() + bis_list_name = serializers.CharField() + current_gear_name = serializers.CharField() + current_gear_il = serializers.IntegerField() + job_icon_name = serializers.CharField() + job_role = serializers.CharField() + + +class TomeGreedItemSerializer(serializers.Serializer): + bis_list_id = serializers.IntegerField() + job_icon_name = serializers.CharField() + job_role = serializers.CharField() + required = serializers.IntegerField() + + +class NeedGearSerializer(serializers.Serializer): + member_id = serializers.IntegerField() + character_name = serializers.CharField() + current_gear_name = serializers.CharField() + current_gear_il = serializers.IntegerField() + job_icon_name = serializers.CharField() + job_role = serializers.CharField() + + +class TomeNeedGearSerializer(serializers.Serializer): + member_id = serializers.IntegerField() + character_name = serializers.CharField() + job_icon_name = serializers.CharField() + job_role = serializers.CharField() + required = serializers.IntegerField() + + +class GreedGearSerializer(serializers.Serializer): + member_id = serializers.IntegerField() + character_name = serializers.CharField() + greed_lists = serializers.ListField(child=GreedItemSerializer(many=True)) + + +class TomeGreedGearSerializer(serializers.Serializer): + member_id = serializers.IntegerField() + character_name = serializers.CharField() + greed_lists = serializers.ListField(child=TomeGreedItemSerializer(many=True)) + class LootCollection(APIView): """ @@ -227,9 +275,151 @@ def _get_history_loot_data(self, obj: Team, loot: QuerySet) -> Dict[str, List[Di response[slot] = slot_data return response + @extend_schema( + tags=['team_loot'], + responses={ + 200: inline_serializer( + 'LootListResponse', + { + 'history': LootSerializer(many=True), + 'received': inline_serializer( + 'LootReceived', + { + '[member_name: str]': inline_serializer( + 'LootReceivedEntry', + { + 'need': serializers.IntegerField(), + 'greed': serializers.IntegerField(), + }, + ), + }, + ), + 'gear': inline_serializer( + 'LootGearRequiredResponse', + { + 'mainhand': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'offhand': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'head': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'body': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'hands': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'legs': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'feet': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'earrings': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'necklace': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'bracelet': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'ring': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'tome-accessory-augment': inline_serializer( + 'TomeAugmentRequiredData', + { + 'need': TomeNeedGearSerializer(many=True), + 'greed': TomeGreedGearSerializer(many=True), + } + ), + 'tome-armour-augment': inline_serializer( + 'TomeAugmentRequiredData', + { + 'need': TomeNeedGearSerializer(many=True), + 'greed': TomeGreedGearSerializer(many=True), + } + ), + 'mount': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'tome-weapon-token': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + 'tome-weapon-augment': inline_serializer( + 'RaidRequiredData', + { + 'need': NeedGearSerializer(many=True), + 'greed': GreedGearSerializer(many=True), + } + ), + }, + ) + } + ), + 404: OpenApiResponse(description='The provided Team ID does not refer to a valid Team that the User has a Character in.'), + }, + operation_id='loot_list', + ) def get(self, request: Request, team_id: str) -> Response: """ - Get loot history and current need/greed status for a team + Get Loot history and current need/greed status for a team """ try: obj = Team.objects.select_related( @@ -292,10 +482,23 @@ def get(self, request: Request, team_id: str) -> Response: team_data = TeamSerializer(obj).data return Response({'team': team_data, 'loot': loot_data}) + @extend_schema( + tags=['team_loot'], + responses={ + 201: OpenApiResponse( + response=inline_serializer('LootRecordCreateResponse', {'id': serializers.IntegerField()}), + description='The ID of the created Loot record', + ), + 404: OpenApiResponse(description='The provided Team ID does not refer to a valid Team that the User has a Character in.'), + }, + request=LootCreateSerializer(), + operation_id='loot_create', + ) def post(self, request: Request, team_id: str) -> Response: """ - Attempt to create new Loot entries. - Any updates sent here will also update Character's BIS Lists + Create new Loot entry for the specified Team. + + This method does not touch the BISLists, only creates Loot records to be displayed in the Loot History. """ team = self._get_team_with_permission(request, team_id, PERMISSION_NAME) if team is None: @@ -311,10 +514,26 @@ def post(self, request: Request, team_id: str) -> Response: return Response({'id': serializer.instance.pk}, status=201) - def delete(self, request: Request, team_id: str) -> Response: + +class LootDelete(APIView): + + @extend_schema( + tags=['team_loot'], + responses={ + 204: OpenApiResponse( + description='The supplied Loot records were deleted successfully', + ), + 404: OpenApiResponse(description='The provided Team ID does not refer to a valid Team that the User has a Character in.'), + }, + request=inline_serializer('LootDeleteRequest', {'items': serializers.ListField(child=serializers.IntegerField())}), + operation_id='loot_delete', + ) + def post(self, request: Request, team_id: str) -> Response: """ - Remove Loot entries from the Team's history. - Entries to delete are specified in the request body. + Delete Loot records for a Team. + + Will delete any Loot records with valid IDs in the list provided, regardless of Tier they are from. + Any IDs either not in the system, or that don't belong to the specified Team, will be ignored silently. """ team = self._get_team_with_permission(request, team_id, PERMISSION_NAME) if team is None: @@ -335,10 +554,26 @@ class LootWithBIS(APIView): Has stricter serializer since it affects two models instead of one """ + @extend_schema( + tags=['team_loot'], + responses={ + 201: OpenApiResponse( + response=inline_serializer('LootRecordCreateWithBISResponse', {'id': serializers.IntegerField()}), + description='The ID of the created Loot record', + ), + 404: OpenApiResponse(description='The provided Team ID does not refer to a valid Team that the User has a Character in.'), + }, + request=LootCreateWithBISSerializer(), + operation_id='loot_create_with_bis_update', + ) def post(self, request: Request, team_id: str) -> Response: """ - Attempt to create new Loot entries. - Any updates sent here will also update Character's BIS Lists + Create new Loot entry for the specified Team. + + This method will also update the involved BIS List's Current gear for the slot to be the BIS item. + As a result, this method should only be called with gear that are direct drops from fights, and not obtained via tomes / upgrades. + + The `greed_bis_id` is required if the item is given not for the Character's main BIS List associated with the Team, so that the correct list can be updated. """ team = self._get_team_with_permission(request, team_id, PERMISSION_NAME) if team is None: diff --git a/frontend/src/components/modals/confirmations/delete_loot.vue b/frontend/src/components/modals/confirmations/delete_loot.vue index 36f46c0f..354456b8 100644 --- a/frontend/src/components/modals/confirmations/delete_loot.vue +++ b/frontend/src/components/modals/confirmations/delete_loot.vue @@ -54,14 +54,14 @@ export default class DeleteLoot extends Vue { team!: Team get url(): string { - return `/backend/api/team/${this.team.id}/loot/` + return `/backend/api/team/${this.team.id}/loot/delete/` } async deleteLoot(): Promise { const body = JSON.stringify({ items: this.items.map((item: Loot) => item.id) }) try { const response = await fetch(this.url, { - method: 'DELETE', + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': Vue.$cookies.get('csrftoken'), From 8198377f770f27c570c5c0bce492827aa6c0194a Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 15:56:16 +0200 Subject: [PATCH 19/28] added docs to the notification views --- backend/api/views/notification.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/api/views/notification.py b/backend/api/views/notification.py index 8e5b17ea..3b4adada 100644 --- a/backend/api/views/notification.py +++ b/backend/api/views/notification.py @@ -3,6 +3,9 @@ """ # lib +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response @@ -18,10 +21,12 @@ class NotificationCollection(APIView): Retrieve a list of Notifications Send a post request to mark all your notifications as read """ + queryset = Notification + serializer_class = NotificationSerializer def get(self, request: Request) -> Response: """ - List the Notifications + Retrieve a list of all the Notifications that were sent to the requesting User. """ # Get the filters from the query parameters unread = request.query_params.get('unread', False) @@ -39,9 +44,16 @@ def get(self, request: Request) -> Response: data = NotificationSerializer(objs, many=True).data return Response(data) + @extend_schema( + operation_id='notifications_mark_all_as_read', + request=None, + responses={ + 200: OpenApiResponse(description='All Notifications for the requesting User have been marked as read.'), + }, + ) def post(self, request: Request) -> Response: """ - Mark all your notifications as read + Mark all the requesting User's Notifications as read """ Notification.objects.filter(user=request.user).update(read=True) return Response() @@ -52,9 +64,17 @@ class NotificationResource(APIView): Mark individual notifications as read """ + @extend_schema( + operation_id='notifications_mark_as_read', + request=None, + responses={ + 200: OpenApiResponse(description='The specified Notification for the requesting User has been marked as read.'), + }, + ) def post(self, request: Request, pk: int) -> Response: """ - Mark specific notification as read + Mark a specific Notification as read for the requesting User. + If the ID is invalid, for whatever reason, this method will do nothing instead of returning an error. """ Notification.objects.filter(user=request.user, pk=pk).update(read=True) return Response() From b8416a08793fa3b8c0f2256b12e7f0122405d2de Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 16:09:09 +0200 Subject: [PATCH 20/28] add docs to plugin view --- backend/api/views/plugin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/api/views/plugin.py b/backend/api/views/plugin.py index 763ce6a3..cde1aad8 100644 --- a/backend/api/views/plugin.py +++ b/backend/api/views/plugin.py @@ -4,6 +4,9 @@ # stdlib from typing import Dict # lib +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -16,10 +19,15 @@ class PluginImport(ImportAPIView): """ Convert names and item levels from in game items to Savage Aim Gear Items. """ + serializer_class = PluginImportResponseSerializer + @extend_schema( + request=PluginImportSerializer, + ) def post(self, request: Request) -> Response: """ - Convert names from in-game into Gear instances. + Given a set of names taken from in-game, turn the in-game names into the names and IDs of Gear records in the DB. + This allows the plugin to send a valid update request using the official in-system Gear IDs. """ # Use Serializer for validating the provided data serializer = PluginImportSerializer(data=request.data) From 21dce2abc0be46d294d2b9332fcb01c5950b2d5b Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 16:11:14 +0200 Subject: [PATCH 21/28] added documentation to the tier view --- backend/api/views/tier.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/api/views/tier.py b/backend/api/views/tier.py index 902645f3..4b12806c 100644 --- a/backend/api/views/tier.py +++ b/backend/api/views/tier.py @@ -18,12 +18,13 @@ class TierCollection(APIView): """ Get a list of Tiers in the system """ - + queryset = Tier + serializer_class = TierSerializer(many=True) permission_classes = [AllowAny] def get(self, request: Request) -> Response: """ - Return a list of Characters belonging to a certain User + Return the list of Tiers tracked in the system. """ # Permissions won't allow this method to be run by non-auth'd users objs = Tier.objects.all() From 99f1309efa97d1c8bd485c3bd526b4485a233ed0 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 16:31:43 +0200 Subject: [PATCH 22/28] added docs to user views --- backend/api/serializers/user.py | 7 +++++++ backend/api/views/user.py | 30 +++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index 0399cca5..1ea0bf47 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -4,6 +4,7 @@ # stdlib from typing import Dict # lib +from drf_spectacular.utils import extend_schema_field, inline_serializer from rest_framework import serializers # local from api.models import Settings @@ -39,6 +40,12 @@ def get_loot_manager_version(self, obj) -> str: except (AttributeError, Settings.DoesNotExist): return Settings.LOOT_MANAGER_DEFAULT + @extend_schema_field( + inline_serializer( + 'UserNotifications', + {notif: serializers.BooleanField() for notif in Settings.NOTIFICATIONS}, + ), + ) def get_notifications(self, obj) -> Dict[str, bool]: """ Populate a full dictionary of notifications, filling defaults in as needed diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 6ccab866..4baf7e57 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -3,6 +3,9 @@ """ # lib +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.authtoken.models import Token from rest_framework.permissions import BasePermission from rest_framework.response import Response @@ -25,18 +28,28 @@ def has_permission(self, request, view): class UserView(APIView): """ - A simple method to get the (useful) information about the current user. - Will return 404 if the request is unauthenticated, which will prompt the UI to display a login button instead + Returns the data of the requesting User. + If there is no authenticated User, this will return a default set of information, including an `id` of `null`. """ permission_classes = [UserPermissions] + serializer_class = UserSerializer + @extend_schema(tags=['user']) def get(self, request) -> Response: data = UserSerializer(request.user).data return Response(data) + @extend_schema( + tags=['user'], + request=SettingsSerializer, + responses={ + 200: OpenApiResponse(description='Settings update was successful!'), + }, + ) def put(self, request) -> Response: """ - Update a User's serializer + Update the Settings of the logged in User. + Also allows the User to update their username. """ try: obj = request.user.settings @@ -59,7 +72,7 @@ def put(self, request) -> Response: # Send websocket packet for updates self._send_to_user(request.user, {'type': 'settings'}) - return Response(status=201) + return Response(status=200) class UserTokenView(APIView): @@ -67,9 +80,16 @@ class UserTokenView(APIView): A view for handling updates to a User's Token. """ + @extend_schema( + tags=['user'], + request=None, + responses={ + 201: OpenApiResponse(description='API Key regenerated successfully!'), + }, + ) def patch(self, request) -> Response: """ - Regenerate a User's Token + Regenerate the User's API Key """ try: obj = request.user.auth_token From 80aaf926fa96fe3fbac3649b8bd758761d93db97 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 17:11:53 +0200 Subject: [PATCH 23/28] added docs and prefetching to team member --- backend/api/models/team_member.py | 36 ++++++++++++++++ backend/api/serializers/team_member.py | 7 +++ backend/api/views/team_member.py | 59 +++++++++++++++++++++++--- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/backend/api/models/team_member.py b/backend/api/models/team_member.py index 2543cca2..58897afb 100644 --- a/backend/api/models/team_member.py +++ b/backend/api/models/team_member.py @@ -5,6 +5,42 @@ from django.db import models +class TeamMemberManager(models.Manager): + + def get_queryset(self): + return super().get_queryset().select_related( + 'character', + 'character__user', + 'bis_list', + 'bis_list__bis_body', + 'bis_list__bis_bracelet', + 'bis_list__bis_earrings', + 'bis_list__bis_feet', + 'bis_list__bis_hands', + 'bis_list__bis_head', + 'bis_list__bis_left_ring', + 'bis_list__bis_legs', + 'bis_list__bis_mainhand', + 'bis_list__bis_necklace', + 'bis_list__bis_offhand', + 'bis_list__bis_right_ring', + 'bis_list__current_body', + 'bis_list__current_bracelet', + 'bis_list__current_earrings', + 'bis_list__current_feet', + 'bis_list__current_hands', + 'bis_list__current_head', + 'bis_list__current_left_ring', + 'bis_list__current_legs', + 'bis_list__current_mainhand', + 'bis_list__current_necklace', + 'bis_list__current_offhand', + 'bis_list__current_right_ring', + 'bis_list__job', + 'bis_list__owner', + ) + + class TeamMember(models.Model): # Map of permission name to the number to compare against bitwise PERMISSION_FLAGS = { diff --git a/backend/api/serializers/team_member.py b/backend/api/serializers/team_member.py index 1752307a..b9dcef23 100644 --- a/backend/api/serializers/team_member.py +++ b/backend/api/serializers/team_member.py @@ -3,6 +3,7 @@ """ from typing import Dict # lib +from drf_spectacular.utils import extend_schema_field, inline_serializer from rest_framework import serializers # local from api.models import BISList, Character, TeamMember @@ -26,6 +27,12 @@ class Meta: model = TeamMember exclude = ['team'] + @extend_schema_field( + inline_serializer( + 'TeamMemberPermissions', + {notif: serializers.BooleanField() for notif in TeamMember.PERMISSION_FLAGS}, + ), + ) def get_permissions(self, obj: TeamMember) -> Dict[str, bool]: """ Generate a dictionary of permission classes to a flag stating whether or not this character has that permission diff --git a/backend/api/views/team_member.py b/backend/api/views/team_member.py index bf1c2ac3..dc444738 100644 --- a/backend/api/views/team_member.py +++ b/backend/api/views/team_member.py @@ -9,6 +9,9 @@ # lib from django.core.exceptions import ValidationError +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -25,10 +28,19 @@ class TeamMemberResource(APIView): """ Management of Team Member Objects """ - + queryset = TeamMember + serializer_class = TeamMemberSerializer + + @extend_schema( + tags=['team_member'], + responses={ + 200: TeamMemberSerializer, + 404: OpenApiResponse(description='The Team ID does not exist or the Member ID is not valid.'), + } + ) def get(self, request: Request, team_id: str, pk: id) -> Response: """ - Get the Data for a single Team Member record + Read the Membership data for a Character that the requesting User owns in a given Team. """ try: obj = TeamMember.objects.get(pk=pk, team_id=team_id, character__user=request.user) @@ -38,9 +50,18 @@ def get(self, request: Request, team_id: str, pk: id) -> Response: data = TeamMemberSerializer(instance=obj).data return Response(data) + @extend_schema( + tags=['team_member'], + request=TeamMemberModifySerializer, + responses={ + 204: OpenApiResponse(description='Membership information updated successfully!'), + 400: OpenApiResponse(description='Sent data is invalid.'), + 404: OpenApiResponse(description='The Team ID does not exist or the Member ID is not valid.'), + } + ) def put(self, request: Request, team_id: str, pk: id) -> Response: """ - Update a pre-existing Team Member object, potentially changing both the linked character and bis list + Update some of the Membership data for a Character that the requesting User owns in a given Team. """ try: obj = TeamMember.objects.get(pk=pk, team_id=team_id, character__user=request.user) @@ -61,10 +82,20 @@ def put(self, request: Request, team_id: str, pk: id) -> Response: return Response(status=204) + @extend_schema( + tags=['team_member'], + responses={ + 204: OpenApiResponse(description='Membership information deleted successfully!'), + 404: OpenApiResponse(description='The Team ID does not exist or the Member ID is not valid.'), + } + ) def delete(self, request: Request, team_id: str, pk: id) -> Response: """ - Team Members can leave a team - Team leaders can kick team members + Delete the Membership record for a Member of a Team. + + This method can be run by two people; + 1. The owner of the Member can run this method to leave the specified Team. + 2. The leader of the Team can run this method to kick the specified Member from the Team. """ try: obj = TeamMember.objects.get(pk=pk, team_id=team_id) @@ -122,9 +153,25 @@ class TeamMemberPermissionsResource(APIView): Allow for the updating of Team Member permissions by the Team Lead """ + @extend_schema( + tags=['team_member'], + request=TeamMemberPermissionsModifySerializer, + responses={ + 204: OpenApiResponse(description='The Member\'s permissions have been updated successfully!'), + 400: OpenApiResponse(description='Sent data is invalid.'), + 404: OpenApiResponse(description='The Team ID does not exist or the Member ID is not valid. Alternatively, the requesting User is not the Team Lead.'), + } + ) def put(self, request: Request, team_id: str, pk: id) -> Response: """ - Update a pre-existing Team Member object, potentially changing both the linked character and bis list + Update the Permissions that a Member of a Team has. + This method can only be run by the Team Leader. + + The permissions use a bitflag approach with the following numbers; + - `loot_manager` = `1` + - `proxy_manager` = `2` + + Sending a value of `3` gives both permissions. """ # Make sure the user in question is the Team Leader team = self._get_team_as_leader(request, team_id) From af37ed0f183ecd916e269a5e5a2b052fab7128ca Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 17:48:35 +0200 Subject: [PATCH 24/28] added documentation to the team proxy file --- backend/api/views/team_proxy.py | 84 +++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/backend/api/views/team_proxy.py b/backend/api/views/team_proxy.py index a0491594..194d9631 100644 --- a/backend/api/views/team_proxy.py +++ b/backend/api/views/team_proxy.py @@ -6,6 +6,9 @@ # lib from django.core.exceptions import ValidationError +from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -26,10 +29,34 @@ class TeamProxyCollection(APIView): Handle creation of new Proxy Characters for a Team """ + @extend_schema( + tags=['team_proxy'], + request=inline_serializer( + 'TeamProxyMemberCreateRequest', + { + 'character': CharacterCollectionSerializer, + 'bis_data': BISListModifySerializer, + }, + ), + responses={ + 201: OpenApiResponse( + response=inline_serializer('ProxyMemberCreateResponse', {'id': serializers.IntegerField()}), + description='The ID of the created Proxy Member', + ), + 400: OpenApiResponse( + response=inline_serializer( + 'ProxyMemberCreateValidationErrors', + {'character': CharacterCollectionSerializer, 'bis': BISListModifySerializer}, + ), + description='A map of any errors for the provided Character and BIS data. The values will all be lists of strings for any keys that are present.', + ), + 404: OpenApiResponse(description='The Team ID does not exist, the Member ID is not valid, or the requesting User does not have permission.'), + }, + ) def post(self, request: Request, team_id: str) -> Response: """ - Create a new Proxy Character in the specified Team. - Can currently only be done by the Team Lead. + Add a new Proxy Member to the Team. + This request can be run by anyone in the Team with the `proxy_manager` permissions. """ team = self._get_team_with_permission(request, team_id, PERMISSION_NAME) if team is None: @@ -69,10 +96,23 @@ class TeamProxyResource(APIView): Handle a single Proxy Character record (read / update) """ + @extend_schema( + tags=['team_proxy'], + responses={ + 200: inline_serializer( + 'ProxyMemberReadResponse', + { + 'team': TeamSerializer(), + 'member': TeamMemberSerializer(), + }, + ), + 404: OpenApiResponse(description='The Team ID does not exist, the Member ID is not valid, or the requesting User does not have permission.'), + }, + ) def get(self, request: Request, team_id: str, pk: int) -> Response: """ - Read the details of a single Proxy record. - Can only be performed by a Team Lead + Read the details of a specific Proxied Team Member. + This request can be run by anyone in the Team with the `proxy_manager` permissions. """ team = self._get_team_with_permission(request, team_id, PERMISSION_NAME) if team is None: @@ -88,11 +128,24 @@ def get(self, request: Request, team_id: str, pk: int) -> Response: member_data = TeamMemberSerializer(instance=obj).data return Response({'team': team_data, 'member': member_data}) + @extend_schema( + tags=['team_proxy'], + request=BISListModifySerializer, + responses={ + 204: OpenApiResponse( + description='The BIS data of the Proxy Member was updated successfully!', + ), + 400: OpenApiResponse( + response=BISListModifySerializer, + description='A map of any errors for the provided Character and BIS data. The values will all be lists of strings for any keys that are present.', + ), + 404: OpenApiResponse(description='The Team ID does not exist, the Member ID is not valid, or the requesting User does not have permission.'), + }, + ) def put(self, request: Request, team_id: str, pk: int) -> Response: """ - Update the details of a single Proxy record. - Can only be performed by a Team Lead. - Only really updates the BIS List since there's not much need to update anything else. + Update the BIS information for a Proxy Team Member. + This request can be run by anyone in the Team with the `proxy_manager` permissions. """ team = self._get_team_with_permission(request, team_id, PERMISSION_NAME) if team is None: @@ -122,9 +175,24 @@ class TeamProxyClaim(APIView): For a little security, the view will expect the invite code to be sent in the data. """ + @extend_schema( + tags=['team_proxy'], + request=inline_serializer('ProxyClaimRequest', {'invite_code': serializers.CharField()}), + responses={ + 201: OpenApiResponse( + response=inline_serializer('ProxyMemberClaimResponse', {'id': serializers.IntegerField()}), + description='The ID of the copy of the Proxy Member', + ), + 404: OpenApiResponse(description='The Team ID does not exist, the Member ID is not valid, or the `invite_code` is incorrect.'), + }, + ) def post(self, request: Request, team_id: str, pk: int) -> Response: """ - Make an attempt to claim a Proxy Character + Allow a User to make an attempt to claim ownership of a pre-existing Proxy Character. + This view can be used by anyone who has the `invite_code` for the Team. + + It will create a copy of the Character in the requesting User's account. + When they successfully validate their copy, the system will replace all instances of the proxied Character with their valid one. """ invite_code = request.data.get('invite_code', '') try: From 6835aad3cba5056b384c0c174579a762b9a8f78c Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 19:18:30 +0200 Subject: [PATCH 25/28] team docs written --- backend/api/views/team.py | 126 ++++++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 18 deletions(-) diff --git a/backend/api/views/team.py b/backend/api/views/team.py index eeb33af8..161d4220 100644 --- a/backend/api/views/team.py +++ b/backend/api/views/team.py @@ -7,6 +7,9 @@ # lib from django.core.exceptions import ValidationError +from drf_spectacular.utils import inline_serializer, OpenApiResponse, OpenApiParameter +from drf_spectacular.views import extend_schema +from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local @@ -26,11 +29,20 @@ class TeamCollection(APIView): Methods to interact with a list of Teams that the User has a character in. Provides list and create methods. """ - + queryset = Team + serializer_class = TeamSerializer(many=True) + + @extend_schema( + parameters=[ + OpenApiParameter( + 'char_id', + int, + description='Filter the response to Teams that the specified Character is in.'), + ], + ) def get(self, request: Request) -> Response: """ - Return a list of teams for the User. - Returns a list of all teams the User has characters in, which can be filtered further by query params + Return a list of the Teams that the requesting User has Characters in. """ objs = Team.objects.filter(members__character__user=request.user).order_by('name').distinct() @@ -46,9 +58,22 @@ def get(self, request: Request) -> Response: data = TeamSerializer(objs, many=True).data return Response(data) + @extend_schema( + request=TeamCreateSerializer, + responses={ + 201: OpenApiResponse( + response=inline_serializer('CreateResponse', {'id': serializers.UUIDField()}), + description='The ID of the created Team.', + ), + 400: OpenApiResponse( + response=TeamCreateSerializer, + description='Errors occurred during validation of sent data. The values will all be lists of strings for any keys that are present.', + ), + } + ) def post(self, request: Request) -> Response: """ - Create a new team, with the data for the team lead team member + Create a new Team. """ # Ensure the data we were sent is valid serializer = TeamCreateSerializer(data=request.data, context={'user': request.user}) @@ -80,10 +105,18 @@ class TeamResource(APIView): Handling team specific requests """ + @extend_schema( + responses={ + 200: TeamSerializer, + 404: OpenApiResponse( + description='Team ID is invalid, or the requesting User has no Characters in the Team.', + ), + } + ) def get(self, request: Request, pk: str) -> Response: """ - Read the data of a Team. - It must be a Team that the requesting user controls a character in, or the request will fail + Read the data of a specified Team. + The requesting User must have a Character in the Team to be able to read it. """ try: obj = Team.objects.filter(members__character__user=request.user).distinct().get(pk=pk) @@ -93,10 +126,23 @@ def get(self, request: Request, pk: str) -> Response: data = TeamSerializer(instance=obj).data return Response(data) + @extend_schema( + request=TeamUpdateSerializer, + responses={ + 204: OpenApiResponse(description='The Team details have been updated successfully!'), + 400: OpenApiResponse( + response=TeamUpdateSerializer, + description='A map of any errors for the provided Character and BIS data. The values will all be lists of strings for any keys that are present.', + ), + 404: OpenApiResponse( + description='Team ID is invalid, or the requesting User does not own the leader of the Team.', + ), + } + ) def put(self, request: Request, pk: str) -> Response: """ - Update some data about the Team - This request can only be run by the user whose character is the team lead + Update information about a specified Team. + The requesting User must own the Character who leads the Team in order to run this method. """ obj = self._get_team_as_leader(request, pk) if obj is None: @@ -121,9 +167,19 @@ def put(self, request: Request, pk: str) -> Response: self._send_to_user(tm.character.user, {'type': 'character', 'id': tm.character.pk}) return Response(status=204) + @extend_schema( + request=None, + responses={ + 204: OpenApiResponse(description='The `invite_code` was successfully regenerated!'), + 404: OpenApiResponse( + description='Team ID is invalid, or the requesting User does not own the leader of the Team.', + ), + } + ) def patch(self, request: Request, pk: str) -> Response: """ - Regenerate the Team's token + Regenerate the Team's `invite_code`. + The requesting User must own the Character who leads the Team in order to run this method. """ obj = self._get_team_as_leader(request, pk) if obj is None: @@ -133,11 +189,19 @@ def patch(self, request: Request, pk: str) -> Response: obj.save() return Response(status=204) + @extend_schema( + request=None, + responses={ + 204: OpenApiResponse(description='The Team was successfully disbanded!'), + 404: OpenApiResponse( + description='Team ID is invalid, or the requesting User does not own the leader of the Team.', + ), + } + ) def delete(self, request: Request, pk: str) -> Response: """ - Disband a Team - - Notify all non leader members of the Team being disbanded + Disband a Team. + The requesting User must own the Character who leads the Team in order to run this method. """ obj = self._get_team_as_leader(request, pk) if obj is None: @@ -161,10 +225,17 @@ class TeamInvite(APIView): Used to check that an invite code is valid, pull information, and add members to a Team """ + @extend_schema( + request=None, + responses={ + 200: OpenApiResponse(description='The supplied `invite_code` is valid!'), + 404: OpenApiResponse(description='The supplied `invite_code` is invalid.'), + } + ) def head(self, request: Request, invite_code: str) -> Response: """ - Check a Team's existence purely by URL. - Will be used by the frontend to check invite code validity without loading team data + Check that a Team exists using only its `invite_code`. + Used by the frontend to validate a User's supplied `invite_code` without having to load the whole Team's data. """ try: Team.objects.get(invite_code=invite_code) @@ -173,10 +244,16 @@ def head(self, request: Request, invite_code: str) -> Response: return Response(status=200) + @extend_schema( + request=None, + responses={ + 200: TeamSerializer, + 404: OpenApiResponse(description='A Team with the given `invite_code` does not exist.'), + } + ) def get(self, request: Request, invite_code: str) -> Response: """ - Return a list of teams for the User. - Returns a list of all teams the User has characters in, which can be filtered further by query params + Retrieve the data for a Team whose `invite_code` matches the one that is supplied. """ try: obj = Team.objects.get(invite_code=invite_code) @@ -186,10 +263,23 @@ def get(self, request: Request, invite_code: str) -> Response: data = TeamSerializer(obj).data return Response(data) + @extend_schema( + request=TeamMemberModifySerializer, + responses={ + 201: OpenApiResponse( + response=inline_serializer('CreateResponse', {'id': serializers.UUIDField()}), + description='The ID of the created Team Member', + ), + 400: OpenApiResponse( + response=TeamMemberModifySerializer, + description='Errors occurred during validation of sent data. The values will all be lists of strings for any keys that are present.', + ), + 404: OpenApiResponse(description='A Team with the given `invite_code` does not exist.'), + } + ) def post(self, request: Request, invite_code: str) -> Response: """ - Add a new team member to the team via the invite link - Characters can only be on a team once + Allow a User to accept an invitation to join a Team by choosing a Character and BISList. """ try: obj = Team.objects.get(invite_code=invite_code) From 338be85c37a6becfa4215a559081d656d25197a7 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 19:20:54 +0200 Subject: [PATCH 26/28] syncing up createresponses --- backend/api/views/bis_list.py | 2 +- backend/api/views/character.py | 2 +- backend/api/views/loot.py | 2 +- backend/api/views/team_proxy.py | 2 +- backend/backend/settings_live.py | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/api/views/bis_list.py b/backend/api/views/bis_list.py index 114ba6e0..a0a3e5ef 100644 --- a/backend/api/views/bis_list.py +++ b/backend/api/views/bis_list.py @@ -46,7 +46,7 @@ class BISListCollection(BISListBaseView): tags=['bis_list'], responses={ 201: OpenApiResponse( - response=inline_serializer('BISListCreateResponse', {'id': serializers.IntegerField()}), + response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), description='The ID of the created BISList', ), 404: OpenApiResponse(description='The given Character ID did not belong to a valid Character owned by the requesting User'), diff --git a/backend/api/views/character.py b/backend/api/views/character.py index 0a494901..dd5faa9e 100644 --- a/backend/api/views/character.py +++ b/backend/api/views/character.py @@ -49,7 +49,7 @@ def get(self, request: Request) -> Response: request=CharacterCollectionSerializer, responses={ 201: OpenApiResponse( - response=inline_serializer('CharacterCreateResponse', {'id': serializers.IntegerField()}), + response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), description='The ID of the created Character', ), }, diff --git a/backend/api/views/loot.py b/backend/api/views/loot.py index 7a670619..465a2d97 100644 --- a/backend/api/views/loot.py +++ b/backend/api/views/loot.py @@ -486,7 +486,7 @@ def get(self, request: Request, team_id: str) -> Response: tags=['team_loot'], responses={ 201: OpenApiResponse( - response=inline_serializer('LootRecordCreateResponse', {'id': serializers.IntegerField()}), + response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), description='The ID of the created Loot record', ), 404: OpenApiResponse(description='The provided Team ID does not refer to a valid Team that the User has a Character in.'), diff --git a/backend/api/views/team_proxy.py b/backend/api/views/team_proxy.py index 194d9631..cc17f023 100644 --- a/backend/api/views/team_proxy.py +++ b/backend/api/views/team_proxy.py @@ -40,7 +40,7 @@ class TeamProxyCollection(APIView): ), responses={ 201: OpenApiResponse( - response=inline_serializer('ProxyMemberCreateResponse', {'id': serializers.IntegerField()}), + response=inline_serializer('CreateResponse', {'id': serializers.IntegerField()}), description='The ID of the created Proxy Member', ), 400: OpenApiResponse( diff --git a/backend/backend/settings_live.py b/backend/backend/settings_live.py index 089ecce3..48ae5113 100644 --- a/backend/backend/settings_live.py +++ b/backend/backend/settings_live.py @@ -125,6 +125,7 @@ 'SERVE_INCLUDE_SCHEMA': False, 'SERVERS': [{'url': 'https://savageaim.com/', 'description': 'Main Site'}], 'SCHEMA_PATH_PREFIX': '/backend/api', + 'DISABLE_ERRORS_AND_WARNINGS': True, } CSRF_COOKIE_SECURE = True From 268439f872e768ecfc64d80e6b03d26dbdea7daa Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 19:29:03 +0200 Subject: [PATCH 27/28] linting --- backend/api/serializers/character.py | 2 +- backend/api/serializers/team_member.py | 2 +- backend/api/serializers/user.py | 2 +- backend/api/views/gear.py | 2 +- backend/api/views/loot.py | 3 ++- backend/api/views/loot_solver.py | 2 +- backend/api/views/notification.py | 3 +-- backend/api/views/plugin.py | 2 -- backend/api/views/team.py | 4 ++-- backend/api/views/team_member.py | 3 +-- backend/api/views/team_proxy.py | 4 ++-- backend/api/views/user.py | 3 +-- 12 files changed, 14 insertions(+), 18 deletions(-) diff --git a/backend/api/serializers/character.py b/backend/api/serializers/character.py index eab15c73..a9203586 100644 --- a/backend/api/serializers/character.py +++ b/backend/api/serializers/character.py @@ -38,7 +38,7 @@ def get_proxy(self, char: Character) -> bool: @extend_schema_field( inline_serializer( - 'CharacterCollectionBISListSummary', + 'CharacterCollectionBISListSummary', {'id': serializers.IntegerField(), 'name': serializers.CharField()}, ), ) diff --git a/backend/api/serializers/team_member.py b/backend/api/serializers/team_member.py index b9dcef23..4afe8b0d 100644 --- a/backend/api/serializers/team_member.py +++ b/backend/api/serializers/team_member.py @@ -29,7 +29,7 @@ class Meta: @extend_schema_field( inline_serializer( - 'TeamMemberPermissions', + 'TeamMemberPermissions', {notif: serializers.BooleanField() for notif in TeamMember.PERMISSION_FLAGS}, ), ) diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index 1ea0bf47..c0f77e12 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -42,7 +42,7 @@ def get_loot_manager_version(self, obj) -> str: @extend_schema_field( inline_serializer( - 'UserNotifications', + 'UserNotifications', {notif: serializers.BooleanField() for notif in Settings.NOTIFICATIONS}, ), ) diff --git a/backend/api/views/gear.py b/backend/api/views/gear.py index 8f65975d..a50638ee 100644 --- a/backend/api/views/gear.py +++ b/backend/api/views/gear.py @@ -3,7 +3,7 @@ """ # lib -from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter, OpenApiResponse +from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter from rest_framework import serializers from rest_framework.views import APIView from rest_framework.permissions import AllowAny diff --git a/backend/api/views/loot.py b/backend/api/views/loot.py index 465a2d97..3919c492 100644 --- a/backend/api/views/loot.py +++ b/backend/api/views/loot.py @@ -28,6 +28,7 @@ PERMISSION_NAME = 'loot_manager' + # Define Serializers for the response info. class GreedItemSerializer(serializers.Serializer): bis_list_id = serializers.IntegerField() @@ -286,7 +287,7 @@ def _get_history_loot_data(self, obj: Team, loot: QuerySet) -> Dict[str, List[Di 'LootReceived', { '[member_name: str]': inline_serializer( - 'LootReceivedEntry', + 'LootReceivedEntry', { 'need': serializers.IntegerField(), 'greed': serializers.IntegerField(), diff --git a/backend/api/views/loot_solver.py b/backend/api/views/loot_solver.py index 5f6f9b74..20ef10ea 100644 --- a/backend/api/views/loot_solver.py +++ b/backend/api/views/loot_solver.py @@ -10,7 +10,7 @@ # lib from django.core.exceptions import ValidationError from django.db.models import QuerySet -from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.utils import inline_serializer from drf_spectacular.views import extend_schema from rest_framework import serializers from rest_framework.request import Request diff --git a/backend/api/views/notification.py b/backend/api/views/notification.py index 3b4adada..ec949bdc 100644 --- a/backend/api/views/notification.py +++ b/backend/api/views/notification.py @@ -3,9 +3,8 @@ """ # lib -from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.utils import OpenApiResponse from drf_spectacular.views import extend_schema -from rest_framework import serializers from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response diff --git a/backend/api/views/plugin.py b/backend/api/views/plugin.py index cde1aad8..95539513 100644 --- a/backend/api/views/plugin.py +++ b/backend/api/views/plugin.py @@ -4,9 +4,7 @@ # stdlib from typing import Dict # lib -from drf_spectacular.utils import inline_serializer, OpenApiResponse from drf_spectacular.views import extend_schema -from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local diff --git a/backend/api/views/team.py b/backend/api/views/team.py index 161d4220..eba934a8 100644 --- a/backend/api/views/team.py +++ b/backend/api/views/team.py @@ -35,8 +35,8 @@ class TeamCollection(APIView): @extend_schema( parameters=[ OpenApiParameter( - 'char_id', - int, + 'char_id', + int, description='Filter the response to Teams that the specified Character is in.'), ], ) diff --git a/backend/api/views/team_member.py b/backend/api/views/team_member.py index dc444738..0975258e 100644 --- a/backend/api/views/team_member.py +++ b/backend/api/views/team_member.py @@ -9,9 +9,8 @@ # lib from django.core.exceptions import ValidationError -from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.utils import OpenApiResponse from drf_spectacular.views import extend_schema -from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response # local diff --git a/backend/api/views/team_proxy.py b/backend/api/views/team_proxy.py index cc17f023..a2425d7b 100644 --- a/backend/api/views/team_proxy.py +++ b/backend/api/views/team_proxy.py @@ -100,7 +100,7 @@ class TeamProxyResource(APIView): tags=['team_proxy'], responses={ 200: inline_serializer( - 'ProxyMemberReadResponse', + 'ProxyMemberReadResponse', { 'team': TeamSerializer(), 'member': TeamMemberSerializer(), @@ -190,7 +190,7 @@ def post(self, request: Request, team_id: str, pk: int) -> Response: """ Allow a User to make an attempt to claim ownership of a pre-existing Proxy Character. This view can be used by anyone who has the `invite_code` for the Team. - + It will create a copy of the Character in the requesting User's account. When they successfully validate their copy, the system will replace all instances of the proxied Character with their valid one. """ diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 4baf7e57..a1904edd 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -3,9 +3,8 @@ """ # lib -from drf_spectacular.utils import inline_serializer, OpenApiResponse +from drf_spectacular.utils import OpenApiResponse from drf_spectacular.views import extend_schema -from rest_framework import serializers from rest_framework.authtoken.models import Token from rest_framework.permissions import BasePermission from rest_framework.response import Response From b0cc4ff26218b3825a75d884ffd7e47881698804 Mon Sep 17 00:00:00 2001 From: freyamade Date: Fri, 12 Jul 2024 19:38:52 +0200 Subject: [PATCH 28/28] fix tests --- .../api/tests/test_lodestone_gear_import.py | 34 +++++++++---------- backend/api/tests/test_loot.py | 10 +++--- backend/api/tests/test_user.py | 4 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/backend/api/tests/test_lodestone_gear_import.py b/backend/api/tests/test_lodestone_gear_import.py index a9eaa4d2..600d0e5c 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': 'MNK'}) + url = reverse('api:lodestone_gear_import', kwargs={'character_id': '22909725', 'expected_job': 'GNB'}) user = self._get_user() self.client.force_authenticate(user) response = self.client.get(url) @@ -36,21 +36,21 @@ def test_import(self): # Build an expected data packet expected = { - 'job_id': 'MNK', - 'mainhand': Gear.objects.get(name='Voidcast').pk, - 'offhand': Gear.objects.get(name='Voidcast').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='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, + 'job_id': 'GNB', + 'mainhand': Gear.objects.get(name='Neo Kingdom').pk, + 'offhand': Gear.objects.get(name='Neo Kingdom').pk, + 'head': Gear.objects.get(name='Neo Kingdom', has_armour=True).pk, + 'body': Gear.objects.get(name='Neo Kingdom', has_armour=True).pk, + 'hands': Gear.objects.get(name='Neo Kingdom', has_armour=True).pk, + 'legs': Gear.objects.get(name='Neo Kingdom', has_armour=True).pk, + 'feet': Gear.objects.get(name='Neo Kingdom', has_armour=True).pk, + 'earrings': Gear.objects.get(name='Neo Kingdom', has_accessories=True).pk, + 'necklace': Gear.objects.get(name='Neo Kingdom', has_accessories=True).pk, + 'bracelet': Gear.objects.get(name='Neo Kingdom', has_accessories=True).pk, + 'right_ring': Gear.objects.get(name='Neo Kingdom', has_accessories=True).pk, + 'left_ring': Gear.objects.get(name='Epochal', has_accessories=True).pk, + 'min_il': 690, + 'max_il': 700, } self.maxDiff = None self.assertDictEqual(response.json(), expected) @@ -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 "PGL MNK" was found.', + 'Couldn\'t import Gear from Lodestone. Gear was expected to be for "SAM", but "GNB" was found.', ) diff --git a/backend/api/tests/test_loot.py b/backend/api/tests/test_loot.py index fe2b3965..81b4efb1 100644 --- a/backend/api/tests/test_loot.py +++ b/backend/api/tests/test_loot.py @@ -1486,7 +1486,7 @@ def test_delete(self): """ user = self._get_user() self.client.force_authenticate(user) - url = reverse('api:loot_collection', kwargs={'team_id': self.team.pk}) + url = reverse('api:loot_delete', kwargs={'team_id': self.team.pk}) # Create some Loot entries for this Team, along with one for a new Team (just to make sure filtering works) other_team = Team.objects.create( @@ -1529,7 +1529,7 @@ def test_delete(self): ) body = {'items': [l1.pk, l3.pk, l4.pk]} - response = self.client.delete(url, body) + response = self.client.post(url, body) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.content) with self.assertRaises(Loot.DoesNotExist): Loot.objects.get(pk=l1.pk) @@ -1553,7 +1553,7 @@ def test_404(self): url = reverse('api:loot_collection', kwargs={'team_id': 'abcde'}) self.assertEqual(self.client.get(url).status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(self.client.post(url).status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(self.client.delete(url).status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(self.client.post(f'{url}delete/').status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(self.client.post(f'{url}bis/').status_code, status.HTTP_404_NOT_FOUND) # Not having a character in the team @@ -1562,11 +1562,11 @@ def test_404(self): url = reverse('api:loot_collection', kwargs={'team_id': self.team.pk}) self.assertEqual(self.client.get(url).status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(self.client.post(url).status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(self.client.delete(url).status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(self.client.post(f'{url}delete/').status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(self.client.post(f'{url}bis/').status_code, status.HTTP_404_NOT_FOUND) # POST while not team lead self.client.force_authenticate(self.main_tank.user) self.assertEqual(self.client.post(url).status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(self.client.delete(url).status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(self.client.post(f'{url}delete/').status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(self.client.post(f'{url}bis/').status_code, status.HTTP_404_NOT_FOUND) diff --git a/backend/api/tests/test_user.py b/backend/api/tests/test_user.py index 00620426..912a2fb0 100644 --- a/backend/api/tests/test_user.py +++ b/backend/api/tests/test_user.py @@ -54,7 +54,7 @@ def test_update(self): data = {'theme': 'blue', 'notifications': {'verify_fail': False}, 'loot_manager_version': 'fight', 'username': 'abcde'} response = self.client.put(url, data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response) + self.assertEqual(response.status_code, status.HTTP_200_OK, response) user.refresh_from_db() self.assertEqual(user.settings.loot_manager_version, 'fight') self.assertEqual(user.settings.theme, 'blue') @@ -64,7 +64,7 @@ def test_update(self): # Run it again to hit the other block data = {'theme': 'purple', 'notifications': {'verify_success': True}, 'username': 'abcde'} response = self.client.put(url, data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) user.refresh_from_db() self.assertEqual(user.settings.theme, 'purple') self.assertFalse(user.settings.notifications['verify_fail'])