Fix many openAPI schema errors

This commit is contained in:
Roland Geider
2023-04-19 21:57:14 +02:00
parent 1b498be761
commit 82386ce47b
12 changed files with 278 additions and 42 deletions

View File

@@ -26,11 +26,22 @@ from django.views.decorators.cache import cache_page
# Third Party
from django_email_verification import send_email
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
inline_serializer,
)
from rest_framework import (
status,
viewsets,
)
from rest_framework.decorators import action
from rest_framework.fields import (
BooleanField,
CharField,
)
from rest_framework.permissions import (
AllowAny,
IsAuthenticated,
@@ -87,6 +98,10 @@ class UserProfileViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return UserProfile.objects.none()
return UserProfile.objects.filter(user=self.request.user)
def get_owner_objects(self):
@@ -158,6 +173,12 @@ class ApplicationVersionView(viewsets.ViewSet):
permission_classes = (AllowAny, )
@staticmethod
@extend_schema(
parameters=[],
responses={
200: OpenApiTypes.STR,
},
)
def get(request):
return Response(get_version())
@@ -169,6 +190,26 @@ class PermissionView(viewsets.ViewSet):
permission_classes = (AllowAny, )
@staticmethod
@extend_schema(
parameters=[
OpenApiParameter(
'permission',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='The name of the django permission such as "exercises.change_muscle"',
),
],
responses={
201:
inline_serializer(name='PermissionResponse', fields={
'result': BooleanField(),
}),
400:
OpenApiResponse(
description="Please pass a permission name in the 'permission' parameter"
),
},
)
def get(request):
permission = request.query_params.get('permission')
@@ -191,6 +232,12 @@ class RequiredApplicationVersionView(viewsets.ViewSet):
permission_classes = (AllowAny, )
@staticmethod
@extend_schema(
parameters=[],
responses={
200: OpenApiTypes.STR,
},
)
def get(request):
return Response(get_version(MIN_APP_VERSION, True))

View File

@@ -84,6 +84,7 @@ class ExerciseImageSerializer(serializers.ModelSerializer):
"""
ExerciseImage serializer
"""
author_history = serializers.ListSerializer(child=serializers.CharField())
class Meta:
model = ExerciseImage
@@ -105,6 +106,7 @@ class ExerciseVideoSerializer(serializers.ModelSerializer):
ExerciseVideo serializer
"""
exercise_base_uuid = serializers.ReadOnlyField(source='exercise_base.uuid')
author_history = serializers.ListSerializer(child=serializers.CharField())
class Meta:
model = ExerciseVideo
@@ -193,6 +195,8 @@ class MuscleSerializer(serializers.ModelSerializer):
"""
Muscle serializer
"""
image_url_main = serializers.CharField()
image_url_secondary = serializers.CharField()
class Meta:
model = Muscle
@@ -218,6 +222,7 @@ class ExerciseSerializer(serializers.ModelSerializer):
muscles_secondary = serializers.PrimaryKeyRelatedField(many=True, queryset=Muscle.objects.all())
equipment = serializers.PrimaryKeyRelatedField(many=True, queryset=Equipment.objects.all())
variations = serializers.PrimaryKeyRelatedField(many=True, queryset=Variation.objects.all())
author_history = serializers.ListSerializer(child=serializers.CharField())
class Meta:
model = Exercise
@@ -252,6 +257,7 @@ class ExerciseTranslationBaseInfoSerializer(serializers.ModelSerializer):
)
aliases = ExerciseInfoAliasSerializer(source='alias_set', many=True, read_only=True)
notes = ExerciseCommentSerializer(source='exercisecomment_set', many=True, read_only=True)
author_history = serializers.ListSerializer(child=serializers.CharField())
class Meta:
model = Exercise
@@ -340,6 +346,7 @@ class ExerciseInfoSerializer(serializers.ModelSerializer):
equipment = EquipmentSerializer(many=True, read_only=True)
variations = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
aliases = ExerciseInfoAliasSerializer(source='alias_set', many=True, read_only=True)
author_history = serializers.ListSerializer(child=serializers.CharField())
class Meta:
model = Exercise
@@ -380,6 +387,8 @@ class ExerciseBaseInfoSerializer(serializers.ModelSerializer):
exercises = ExerciseTranslationBaseInfoSerializer(many=True, read_only=True)
videos = ExerciseVideoSerializer(source='exercisevideo_set', many=True, read_only=True)
variations = serializers.PrimaryKeyRelatedField(read_only=True)
author_history = serializers.ListSerializer(child=serializers.CharField())
total_authors_history = serializers.ListSerializer(child=serializers.CharField())
class Meta:
model = ExerciseBase

View File

@@ -30,6 +30,12 @@ from django.views.decorators.cache import cache_page
import bleach
from actstream import action as actstream_action
from bleach.css_sanitizer import CSSSanitizer
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
extend_schema,
inline_serializer,
)
from easy_thumbnails.alias import aliases
from easy_thumbnails.files import get_thumbnailer
from rest_framework import viewsets
@@ -37,6 +43,10 @@ from rest_framework.decorators import (
action,
api_view,
)
from rest_framework.fields import (
CharField,
IntegerField,
)
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
@@ -257,6 +267,46 @@ class ExerciseViewSet(viewsets.ReadOnlyModelViewSet):
return qs
@extend_schema(
parameters=[
OpenApiParameter(
'term',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='The name of the exercise to search"',
required=True,
),
OpenApiParameter(
'language',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='Comma separated list of language codes to search',
required=True,
),
],
responses={
200:
inline_serializer(
name='ExerciseSearchResponse',
fields={
'value':
CharField(),
'data':
inline_serializer(
name='ExerciseSearchItemResponse',
fields={
'id': IntegerField(),
'base_id': IntegerField(),
'name': CharField(),
'category': CharField(),
'image': CharField(),
'image_thumbnail': CharField()
}
)
}
)
},
)
@api_view(['GET'])
def search(request):
"""

View File

@@ -34,7 +34,7 @@ from wger.gallery.models import Image
logger = logging.getLogger(__name__)
class ImageViewSet(viewsets.ModelViewSet):
class GalleryImageViewSet(viewsets.ModelViewSet):
"""
API endpoint for gallery image
"""
@@ -53,6 +53,10 @@ class ImageViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Image.objects.none()
return Image.objects.filter(user=self.request.user)
def perform_create(self, serializer):

View File

@@ -72,6 +72,10 @@ class WorkoutViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Workout.objects.none()
return Workout.objects.filter(user=self.request.user)
def perform_create(self, serializer):
@@ -132,6 +136,10 @@ class UserWorkoutTemplateViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Workout.objects.none()
return Workout.templates.filter(user=self.request.user)
def perform_create(self, serializer):
@@ -189,6 +197,7 @@ class WorkoutSessionViewSet(WgerOwnerObjectModelViewSet):
"""
API endpoint for workout sessions objects
"""
serializer_class = WorkoutSessionSerializer
is_private = True
ordering_fields = '__all__'
@@ -205,6 +214,11 @@ class WorkoutSessionViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return WorkoutSession.objects.none()
return WorkoutSession.objects.filter(user=self.request.user)
def perform_create(self, serializer):
@@ -238,6 +252,10 @@ class ScheduleStepViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return ScheduleStep.objects.none()
return ScheduleStep.objects.filter(schedule__user=self.request.user)
def get_owner_objects(self):
@@ -265,6 +283,10 @@ class ScheduleViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Schedule.objects.none()
return Schedule.objects.filter(user=self.request.user)
def perform_create(self, serializer):
@@ -291,6 +313,10 @@ class DayViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Day.objects.none()
return Day.objects.filter(training__user=self.request.user)
def get_owner_objects(self):
@@ -317,6 +343,10 @@ class SetViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Set.objects.none()
return Set.objects.filter(exerciseday__training__user=self.request.user)
def get_owner_objects(self):
@@ -363,6 +393,10 @@ class SettingViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Setting.objects.none()
return Setting.objects.filter(set__exerciseday__training__user=self.request.user)
def perform_create(self, serializer):
@@ -397,6 +431,9 @@ class WorkoutLogViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return WorkoutLog.objects.none()
return WorkoutLog.objects.filter(user=self.request.user)

View File

@@ -51,6 +51,10 @@ class CategoryViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Category.objects.none()
return Category.objects.filter(user=self.request.user)
def perform_create(self, serializer):
@@ -80,4 +84,8 @@ class MeasurementViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Measurement.objects.none()
return Measurement.objects.filter(category__user=self.request.user)

View File

@@ -74,7 +74,7 @@ class WeightUnitSerializer(serializers.ModelSerializer):
]
class ImageSerializer(serializers.ModelSerializer):
class IngredientImageSerializer(serializers.ModelSerializer):
"""
Image serializer
"""
@@ -213,7 +213,7 @@ class MealItemInfoSerializer(serializers.ModelSerializer):
ingredient_obj = IngredientInfoSerializer(source='ingredient', read_only=True)
weight_unit = serializers.PrimaryKeyRelatedField(read_only=True)
weight_unit_obj = IngredientWeightUnitSerializer(source='weight_unit', read_only=True)
image = ImageSerializer(source='ingredient.image', read_only=True)
image = IngredientImageSerializer(source='ingredient.image', read_only=True)
class Meta:
model = MealItem
@@ -245,6 +245,20 @@ class MealSerializer(serializers.ModelSerializer):
fields = ['id', 'plan', 'order', 'time', 'name']
class NutritionalValuesSerializer(serializers.Serializer):
"""
Nutritional values serializer
"""
energy = serializers.FloatField()
protein = serializers.FloatField()
carbohydrates = serializers.FloatField()
carbohydrates_sugar = serializers.FloatField()
fat = serializers.FloatField()
fat_saturated = serializers.FloatField()
fibres = serializers.FloatField()
sodium = serializers.FloatField()
class MealInfoSerializer(serializers.ModelSerializer):
"""
Meal info serializer
@@ -252,6 +266,7 @@ class MealInfoSerializer(serializers.ModelSerializer):
meal_items = MealItemInfoSerializer(source='mealitem_set', many=True)
plan = serializers.PrimaryKeyRelatedField(read_only=True)
get_nutritional_values = NutritionalValuesSerializer(read_only=True)
class Meta:
model = Meal
@@ -282,6 +297,7 @@ class NutritionPlanInfoSerializer(serializers.ModelSerializer):
"""
meals = MealInfoSerializer(source='meal_set', many=True)
get_nutritional_values = NutritionalValuesSerializer(read_only=True)
class Meta:
model = NutritionPlan

View File

@@ -25,6 +25,12 @@ from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
# Third Party
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
extend_schema,
inline_serializer,
)
from easy_thumbnails.alias import aliases
from easy_thumbnails.files import get_thumbnailer
from rest_framework import viewsets
@@ -32,11 +38,15 @@ from rest_framework.decorators import (
action,
api_view,
)
from rest_framework.fields import (
CharField,
IntegerField,
)
from rest_framework.response import Response
# wger
from wger.nutrition.api.serializers import (
ImageSerializer,
IngredientImageSerializer,
IngredientInfoSerializer,
IngredientSerializer,
IngredientWeightUnitSerializer,
@@ -152,6 +162,45 @@ class IngredientInfoViewSet(IngredientViewSet):
serializer_class = IngredientInfoSerializer
@extend_schema(
parameters=[
OpenApiParameter(
'term',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='The name of the ingredient to search"',
required=True,
),
OpenApiParameter(
'language',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='Comma separated list of language codes to search',
required=True,
),
],
responses={
200:
inline_serializer(
name='IngredientSearchResponse',
fields={
'value':
CharField(),
'data':
inline_serializer(
name='IngredientSearchItemResponse',
fields={
'id': IntegerField(),
'name': CharField(),
'category': CharField(),
'image': CharField(),
'image_thumbnail': CharField()
}
)
}
)
},
)
@api_view(['GET'])
def search(request):
"""
@@ -205,7 +254,7 @@ class ImageViewSet(viewsets.ReadOnlyModelViewSet):
API endpoint for ingredient images
"""
queryset = Image.objects.all()
serializer_class = ImageSerializer
serializer_class = IngredientImageSerializer
ordering_fields = '__all__'
filterset_fields = ('uuid', 'ingredient_id', 'ingredient__uuid')
@@ -258,6 +307,10 @@ class NutritionPlanViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return NutritionPlan.objects.none()
return NutritionPlan.objects.filter(user=self.request.user)
def perform_create(self, serializer):
@@ -304,7 +357,7 @@ class NutritionPlanInfoViewSet(NutritionPlanViewSet):
Read-only info API endpoint for nutrition plan objects. Returns nested data
structures for more easy parsing.
"""
serializer_class = NutritionPlanInfoSerializer
serializer_class = NutritionPlanInfoSerializer(read_only=True)
class MealViewSet(WgerOwnerObjectModelViewSet):
@@ -324,6 +377,10 @@ class MealViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return Meal.objects.none()
return Meal.objects.filter(plan__user=self.request.user)
def perform_create(self, serializer):
@@ -365,6 +422,10 @@ class MealItemViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return MealItem.objects.none()
return MealItem.objects.filter(meal__plan__user=self.request.user)
def perform_create(self, serializer):
@@ -406,6 +467,10 @@ class LogItemViewSet(WgerOwnerObjectModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return LogItem.objects.none()
return LogItem.objects.filter(plan__user=self.request.user)
def get_owner_objects(self):

View File

@@ -24,7 +24,6 @@ from datetime import timedelta
from wger import get_version
from wger.utils.constants import DOWNLOAD_INGREDIENT_WGER
"""
This file contains the global settings that don't usually need to be changed.
For a full list of options, visit:
@@ -175,7 +174,7 @@ TEMPLATES = [
'django.template.loaders.app_directories.Loader',
],
'debug':
False
False
},
},
]
@@ -258,7 +257,7 @@ AVAILABLE_LANGUAGES = (
LANGUAGE_CODE = 'en'
# All translation files are in one place
LOCALE_PATHS = (os.path.join(SITE_ROOT, 'locale'), )
LOCALE_PATHS = (os.path.join(SITE_ROOT, 'locale'),)
# Primary keys are AutoFields
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
@@ -451,29 +450,16 @@ REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'login': '10/min'
},
'DEFAULT_SCHEMA_CLASS':
'drf_spectacular.openapi.AutoSchema',
}
# Api docs
SPECTACULAR_SETTINGS = {
'TITLE': 'wger workout manager',
'DESCRIPTION': 'FLOSS self hosted workout and fitness tracker',
'VERSION': get_version(),
'SERVE_INCLUDE_SCHEMA': False,
'SCHEMA_PATH_PREFIX': '/api/v[0-9]',
'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR',
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
# yapf: enable
# Api docs
SPECTACULAR_SETTINGS = {
'TITLE': 'wger workout manager',
'DESCRIPTION': 'FLOSS self hosted workout and fitness tracker',
'TITLE': 'wger',
'DESCRIPTION': 'Self hosted FLOSS workout and fitness tracker',
'VERSION': get_version(),
'SERVE_INCLUDE_SCHEMA': False,
'SERVE_INCLUDE_SCHEMA': True,
'SCHEMA_PATH_PREFIX': '/api/v[0-9]',
'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
@@ -500,7 +486,7 @@ CORS_URLS_REGEX = r'^/api/.*$'
#
# Ignore these URLs if they cause 404
#
IGNORABLE_404_URLS = (re.compile(r'^/favicon\.ico$'), )
IGNORABLE_404_URLS = (re.compile(r'^/favicon\.ico$'),)
#
# Password rules

View File

@@ -9,6 +9,15 @@
<p>wger Workout Manager provides a full REST API to all database
objects: <a href="/api/v2/" rel="nofollow">https://wger.de/api/v2/</a></p>
<h3>Documentation</h3>
<p>The API is documented with openAPI:</p>
<ul>
<li><a href="{% url 'schema' %}">Download schema file</a></li>
<li><a href="{% url 'api-swagger-ui' %}">Swagger UI</a></li>
<li><a href="{% url 'api-redoc' %}">Redoc</a></li>
</ul>
<h3>Authentication</h3>
<p>Public endpoints, such as the list of exercises or the
@@ -19,7 +28,8 @@
<h6>JWT Authentication</h6>
<p>
You can generate access token via <code>/token/</code> endpoint. Send a username and password, and you will get
You can generate access token via <code>/token/</code> endpoint. Send a username and
password, and you will get
the
<code>access</code> token which you can use to access the private endpoints.
<pre>
@@ -36,7 +46,8 @@ curl \
}
</pre>
<p>Additionally, you can send an access token to <code>/token/verify/</code> endpoint to verify that token.</p>
<p>Additionally, you can send an access token to <code>/token/verify/</code> endpoint to verify
that token.</p>
<p>When this short-lived access token expires, you can use the longer-lived <code>refresh</code>
token to obtain another access token.
@@ -55,7 +66,8 @@ curl \
<p>You should always use HTTPS if possible when communicating with the server.</p>
<p>At the moment it is not possible to register via the API.</p>
<p><strong>Deprecated: </strong>You can also generate a token via the <code>login</code> endpoint. Send a
<p><strong>Deprecated: </strong>You can also generate a token via the <code>login</code>
endpoint. Send a
username and password, and you will get the user's token or a new one will be
generated.</p>
@@ -197,7 +209,8 @@ curl \
<p>
You can easily filter all resources by specifying the filter queries in the
URL: <code>?&lt;fieldname&gt;=&lt;value&gt;</code>, combinations are possible,
the filters will be AND-joined: <code>?&lt;f1&gt;=&lt;v1&gt;&amp;&lt;f2&gt;=&lt;v2&gt;</code>.
the filters will be AND-joined:
<code>?&lt;f1&gt;=&lt;v1&gt;&amp;&lt;f2&gt;=&lt;v2&gt;</code>.
Please note that for boolean values you must pass 'False' or 'True' other
values, e.g. 1, 0, false, etc. will be ignored. Like with not filtered queries,
your objects will be available under the 'results' key.
@@ -449,9 +462,11 @@ curl -H "Authorization: Token 123456..." \
{% block sidebar %}
<div class="alert alert-info" style="margin-top:1em;">
This is also new for us, if you plan on using the API,
<a href="https://github.com/wger-project/wger" class="alert-link">we'd love to hear from you</a>.
<a href="https://github.com/wger-project/wger" class="alert-link">we'd love to hear from
you</a>.
</div>
<p><a href="/api/v2/" class="btn btn-block btn-light" rel="nofollow">Browse the API</a></p>
<p><a href="{% url 'core:user:api-key' %}" class="btn btn-block btn-light">Generate API KEY</a></p>
<p><a href="{% url 'core:user:api-key' %}" class="btn btn-block btn-light">Generate API KEY</a>
</p>
{% endblock %}

View File

@@ -53,6 +53,7 @@ from wger.nutrition.sitemap import NutritionSitemap
from wger.utils.generic_views import TextTemplateView
from wger.weight.api import views as weight_api_views
# admin.autodiscover()
#
@@ -196,7 +197,7 @@ router.register(r'ingredient-image', nutrition_api_views.ImageViewSet, basename=
router.register(r'weightentry', weight_api_views.WeightEntryViewSet, basename='weightentry')
# Gallery app
router.register(r'gallery', gallery_api_views.ImageViewSet, basename='gallery')
router.register(r'gallery', gallery_api_views.GalleryImageViewSet, basename='gallery')
# Measurements app
router.register(
@@ -288,15 +289,9 @@ urlpatterns += [
name='schema',
),
path(
'api/schema/swagger-ui/',
SpectacularSwaggerView.as_view(url_name='schema'),
name='swagger-ui'
),
path(
'api/schema/redoc/',
SpectacularRedocView.as_view(url_name='schema'),
name='redoc'
'api/schema/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='api-swagger-ui'
),
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api-redoc'),
]
#

View File

@@ -37,6 +37,10 @@ class WeightEntryViewSet(viewsets.ModelViewSet):
"""
Only allow access to appropriate objects
"""
# REST API generation
if getattr(self, "swagger_fake_view", False):
return WeightEntry.objects.none()
return WeightEntry.objects.filter(user=self.request.user)
def perform_create(self, serializer):