Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Schema Documentation #77

Merged
merged 28 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
23b6e67
add new footer icons
freyamade Jul 11, 2024
7027e49
add spectacular to the requirements
freyamade Jul 11, 2024
46a1189
moved version info to backend/__init__
freyamade Jul 11, 2024
bdf5023
adding spectacular to settings/urls files
freyamade Jul 11, 2024
29e6972
set up schema details for bis list views
freyamade Jul 11, 2024
f4e2cd7
update servers list
freyamade Jul 11, 2024
4462d58
fixed bis_list docs
freyamade Jul 11, 2024
9d7de89
added documentation to character views
freyamade Jul 11, 2024
fc35d90
documenting etro import view
freyamade Jul 11, 2024
ee9d37b
added documentation to gear views
freyamade Jul 11, 2024
78dd571
add documentation to job views
freyamade Jul 11, 2024
809fc52
added docs to lodestone views
freyamade Jul 11, 2024
b30872e
added docs to team_loot page
freyamade Jul 11, 2024
0ccb848
updating changelog
freyamade Jul 11, 2024
d0772f1
add concrete operation ids
freyamade Jul 12, 2024
ed2d548
changing names to avoid duplicates
freyamade Jul 12, 2024
ad7cb34
fixed inline serializer name
freyamade Jul 12, 2024
9ce4133
added docs to loot views
freyamade Jul 12, 2024
8198377
added docs to the notification views
freyamade Jul 12, 2024
b8416a0
add docs to plugin view
freyamade Jul 12, 2024
21dce2a
added documentation to the tier view
freyamade Jul 12, 2024
99f1309
added docs to user views
freyamade Jul 12, 2024
80aaf92
added docs and prefetching to team member
freyamade Jul 12, 2024
af37ed0
added documentation to the team proxy file
freyamade Jul 12, 2024
6835aad
team docs written
freyamade Jul 12, 2024
338be85
syncing up createresponses
freyamade Jul 12, 2024
268439f
linting
freyamade Jul 12, 2024
b0cc4ff
fix tests
freyamade Jul 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions backend/api/models/team_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
7 changes: 7 additions & 0 deletions backend/api/serializers/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions backend/api/serializers/team_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions backend/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
34 changes: 17 additions & 17 deletions backend/api/tests/test_lodestone_gear_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,29 @@ 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)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())

# 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)
Expand Down Expand Up @@ -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.',
)
10 changes: 5 additions & 5 deletions backend/api/tests/test_loot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
4 changes: 2 additions & 2 deletions backend/api/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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'])
Expand Down
1 change: 1 addition & 0 deletions backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

# Loot
path('team/<str:team_id>/loot/', views.LootCollection.as_view(), name='loot_collection'),
path('team/<str:team_id>/loot/delete/', views.LootDelete.as_view(), name='loot_delete'),
path('team/<str:team_id>/loot/bis/', views.LootWithBIS.as_view(), name='loot_with_bis'),

# LootSolver
Expand Down
3 changes: 2 additions & 1 deletion backend/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +36,7 @@
'LodestoneResource',

'LootCollection',
'LootDelete',
'LootWithBIS',

'LootSolver',
Expand Down
96 changes: 90 additions & 6 deletions backend/api/views/bis_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,10 +38,23 @@ class BISListCollection(BISListBaseView):
"""
Allows for the creation of new BIS Lists
"""

queryset = BISList
serializer_class = BISListModifySerializer

@extend_schema(
operation_id='bis_list_create',
tags=['bis_list'],
responses={
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'),
},
)
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)
Expand All @@ -65,9 +81,22 @@ class BISListResource(BISListBaseView):
Allows for the reading and updating of a BISList
"""

@extend_schema(
operation_id='bis_list_read',
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)
Expand All @@ -82,9 +111,23 @@ def get(self, request: Request, character_id: int, pk: int) -> Response:
data = BISListSerializer(instance=obj).data
return Response(data)

@extend_schema(
operation_id='bis_list_update',
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)
Expand Down Expand Up @@ -120,9 +163,34 @@ class BISListDelete(APIView):
Has a GET request to get information on if we can delete this BISList.
"""

@extend_schema(
operation_id='bis_list_delete_check',
tags=['bis_list'],
responses={
200: OpenApiResponse(
response=inline_serializer(
'BISListDeleteReadResponse',
{
'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.
Expand All @@ -148,9 +216,25 @@ 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!'),
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)
Expand Down
Loading
Loading