From 606119abe69b23bb0690b23e74ccfb59b54f66f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Odini?= Date: Mon, 5 Aug 2024 14:42:23 +0200 Subject: [PATCH] refactor(Django): improve API documentation (#379) --- config/settings.py | 8 ++++++++ open_prices/api/auth/serializers.py | 18 ++++++++++++++++++ open_prices/api/auth/views.py | 10 ++++++++++ open_prices/api/prices/views.py | 3 ++- open_prices/api/proofs/views.py | 6 ++++-- open_prices/api/serializers.py | 5 +++++ open_prices/api/urls.py | 10 +++++----- open_prices/api/views.py | 4 ++++ open_prices/prices/models.py | 6 ++++-- 9 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 open_prices/api/auth/serializers.py create mode 100644 open_prices/api/serializers.py diff --git a/config/settings.py b/config/settings.py index 7a4b55ba..8a05d1fb 100644 --- a/config/settings.py +++ b/config/settings.py @@ -60,6 +60,8 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +APPEND_SLASH = False + ROOT_URLCONF = "config.urls" TEMPLATES = [ @@ -147,6 +149,7 @@ REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", "EXCEPTION_HANDLER": "open_prices.common.middleware.custom_exception_handler", "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], "ORDERING_PARAM": "order_by", @@ -166,8 +169,13 @@ "name": " AGPL-3.0", "url": "https://www.gnu.org/licenses/agpl-3.0.en.html", }, + "SCHEMA_PATH_PREFIX": "/api/v[0-9]", + "ENUM_NAME_OVERRIDES": { + "LocationOsmTypeEnum": "open_prices.locations.constants.OSM_TYPE_CHOICES" + }, } + # Django Q2 # https://django-q2.readthedocs.io/ # ------------------------------------------------------------------------------ diff --git a/open_prices/api/auth/serializers.py b/open_prices/api/auth/serializers.py new file mode 100644 index 00000000..00d83274 --- /dev/null +++ b/open_prices/api/auth/serializers.py @@ -0,0 +1,18 @@ +from rest_framework import serializers + + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField() + + +class SessionResponseSerializer(serializers.Serializer): + access_token = serializers.CharField() + token_type = serializers.CharField() + + +class SessionFullSerializer(serializers.Serializer): + user_id = serializers.CharField() + token = serializers.CharField() + created = serializers.CharField() + last_used = serializers.CharField() diff --git a/open_prices/api/auth/views.py b/open_prices/api/auth/views.py index 660ef162..159d182c 100644 --- a/open_prices/api/auth/views.py +++ b/open_prices/api/auth/views.py @@ -1,12 +1,18 @@ import time from django.conf import settings +from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView +from open_prices.api.auth.serializers import ( + LoginSerializer, + SessionFullSerializer, + SessionResponseSerializer, +) from open_prices.common import openfoodfacts as common_openfoodfacts from open_prices.common.authentication import ( CustomAuthentication, @@ -17,6 +23,9 @@ class LoginView(APIView): + serializer_class = LoginSerializer + + @extend_schema(responses=SessionResponseSerializer) def post(self, request: Request) -> Response: """ Authentication: provide username/password @@ -74,6 +83,7 @@ def post(self, request: Request) -> Response: class SessionView(APIView): authentication_classes = [CustomAuthentication] permission_classes = [IsAuthenticated] + serializer_class = SessionFullSerializer def get(self, request: Request) -> Response: session = get_request_session(request) diff --git a/open_prices/api/prices/views.py b/open_prices/api/prices/views.py index ab301169..ea5d3aaf 100644 --- a/open_prices/api/prices/views.py +++ b/open_prices/api/prices/views.py @@ -33,7 +33,8 @@ class PriceViewSet( def get_queryset(self): if self.request.method in ["PATCH", "DELETE"]: # only return prices owned by the current user - return Price.objects.filter(owner=self.request.user.user_id) + if self.request.user.is_authenticated: + return Price.objects.filter(owner=self.request.user.user_id) return self.queryset def get_serializer_class(self): diff --git a/open_prices/api/proofs/views.py b/open_prices/api/proofs/views.py index f7834301..5b9c5aec 100644 --- a/open_prices/api/proofs/views.py +++ b/open_prices/api/proofs/views.py @@ -28,7 +28,7 @@ class ProofViewSet( permission_classes = [IsAuthenticated] # parser_classes = [FormParser, MultiPartParser] http_method_names = ["get", "post", "patch", "delete"] # disable "put" - # queryset = Proof.objects.all() + queryset = Proof.objects.none() serializer_class = ProofFullSerializer filter_backends = [DjangoFilterBackend, filters.OrderingFilter] filterset_class = ProofFilter @@ -36,7 +36,9 @@ class ProofViewSet( def get_queryset(self): # only return proofs owned by the current user - return Proof.objects.filter(owner=self.request.user.user_id) + if self.request.user.is_authenticated: + return Proof.objects.filter(owner=self.request.user.user_id) + return self.queryset def get_serializer_class(self): if self.request.method == "PATCH": diff --git a/open_prices/api/serializers.py b/open_prices/api/serializers.py new file mode 100644 index 00000000..62ff92b4 --- /dev/null +++ b/open_prices/api/serializers.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class StatusSerializer(serializers.Serializer): + status = serializers.CharField() diff --git a/open_prices/api/urls.py b/open_prices/api/urls.py index 40bd3bd0..03686e1e 100644 --- a/open_prices/api/urls.py +++ b/open_prices/api/urls.py @@ -15,7 +15,7 @@ app_name = "api" -router = routers.DefaultRouter() +router = routers.DefaultRouter(trailing_slash=False) router.register(r"v1/users", UserViewSet, basename="users") router.register(r"v1/locations", LocationViewSet, basename="locations") router.register(r"v1/products", ProductViewSet, basename="products") @@ -25,15 +25,15 @@ urlpatterns = [ path("v1/auth/", include("open_prices.api.auth.urls")), # health check - path("v1/status", StatusView.as_view(), name="status"), + path("status", StatusView.as_view(), name="status"), # Swagger / OpenAPI documentation - path("v1/schema", SpectacularAPIView.as_view(), name="schema"), + path("schema", SpectacularAPIView.as_view(), name="schema"), path( - "v1/docs", + "docs", SpectacularSwaggerView.as_view(url_name="api:schema"), name="swagger-ui", ), - path("v1/redoc", SpectacularRedocView.as_view(url_name="api:schema"), name="redoc"), + path("redoc", SpectacularRedocView.as_view(url_name="api:schema"), name="redoc"), ] urlpatterns += router.urls diff --git a/open_prices/api/views.py b/open_prices/api/views.py index 3440e7e2..fe16c0f4 100644 --- a/open_prices/api/views.py +++ b/open_prices/api/views.py @@ -1,8 +1,12 @@ +from drf_spectacular.utils import extend_schema from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView +from open_prices.api.serializers import StatusSerializer + class StatusView(APIView): + @extend_schema(responses=StatusSerializer, tags=["status"]) def get(self, request: Request) -> Response: return Response({"status": "running"}) diff --git a/open_prices/prices/models.py b/open_prices/prices/models.py index 6b7ca449..509e2eac 100644 --- a/open_prices/prices/models.py +++ b/open_prices/prices/models.py @@ -1,3 +1,5 @@ +import decimal + from django.core.validators import MinValueValidator, ValidationError from django.db import models from django.db.models import signals @@ -46,7 +48,7 @@ class Price(models.Model): price = models.DecimalField( max_digits=10, decimal_places=2, - validators=[MinValueValidator(0)], + validators=[MinValueValidator(decimal.Decimal(0))], blank=True, null=True, ) @@ -56,7 +58,7 @@ class Price(models.Model): price_without_discount = models.DecimalField( max_digits=10, decimal_places=2, - validators=[MinValueValidator(0)], + validators=[MinValueValidator(decimal.Decimal(0))], blank=True, null=True, )