From 29fb56be89ff74df30b5123a267a3ccb3f125c73 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Fri, 19 Dec 2025 17:27:36 +0300 Subject: [PATCH] Features: 1) Add async and sync capabilities to `CamelCaseMiddleWare`; 2) Include OpenAPI support for Enum name overrides in DRF settings; 3) Integrate OpenAPI types in DRF views for improved schema accuracy. Fixes: 1) Correct `lookup_field` to `uuid` in various viewsets; 2) Replace `type=str` with `OpenApiTypes.STR` in path parameters of multiple DRF endpoints; 3) Add missing import `iscoroutinefunction` and `markcoroutinefunction`. Extra: 1) Refactor `__call__` method in `CamelCaseMiddleWare` to separate sync and async logic; 2) Enhance documentation schema responses with precise types in multiple DRF views. --- engine/core/docs/drf/views.py | 7 +++++-- engine/core/docs/drf/viewsets.py | 10 +++++----- engine/core/viewsets.py | 8 ++++---- evibes/middleware.py | 27 +++++++++++++++++++++------ evibes/settings/drf.py | 6 ++++++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/engine/core/docs/drf/views.py b/engine/core/docs/drf/views.py index 361cc4ec..de61f3de 100644 --- a/engine/core/docs/drf/views.py +++ b/engine/core/docs/drf/views.py @@ -1,6 +1,6 @@ from django.conf import settings -from django.http import FileResponse from django.utils.translation import gettext_lazy as _ +from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer from rest_framework import status from rest_framework.fields import CharField, DictField, JSONField, ListField @@ -32,6 +32,9 @@ CUSTOM_OPENAPI_SCHEMA = { "OpenApi3 schema for this API. Format can be selected via content negotiation. " "Language can be selected with Accept-Language and query parameter both." ), + responses={ + status.HTTP_200_OK: OpenApiTypes.OBJECT, + }, ) } @@ -176,7 +179,7 @@ DOWNLOAD_DIGITAL_ASSET_SCHEMA = { ], summary=_("download a digital asset from purchased digital order"), responses={ - status.HTTP_200_OK: FileResponse, + status.HTTP_200_OK: OpenApiTypes.BINARY, status.HTTP_400_BAD_REQUEST: error, }, ) diff --git a/engine/core/docs/drf/viewsets.py b/engine/core/docs/drf/viewsets.py index 42e254a0..5d7e0a67 100644 --- a/engine/core/docs/drf/viewsets.py +++ b/engine/core/docs/drf/viewsets.py @@ -234,7 +234,7 @@ CATEGORY_SCHEMA = { name="lookup_value", location="path", description=_("Category UUID or slug"), - type=str, + type=OpenApiTypes.STR, ), ], responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS}, @@ -284,7 +284,7 @@ CATEGORY_SCHEMA = { name="lookup_value", location="path", description=_("Category UUID or slug"), - type=str, + type=OpenApiTypes.STR, ), ], responses={ @@ -369,7 +369,7 @@ ORDER_SCHEMA = { name="lookup_value", location="path", description=_("Order UUID or human-readable id"), - type=str, + type=OpenApiTypes.STR, ), ], responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS}, @@ -1006,7 +1006,7 @@ BRAND_SCHEMA = { name="lookup_value", location="path", description=_("Brand UUID or slug"), - type=str, + type=OpenApiTypes.STR, ), ], responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS}, @@ -1049,7 +1049,7 @@ BRAND_SCHEMA = { name="lookup_value", location="path", description=_("Brand UUID or slug"), - type=str, + type=OpenApiTypes.STR, ), ], responses={status.HTTP_200_OK: SeoSnapshotSerializer(), **BASE_ERRORS}, diff --git a/engine/core/viewsets.py b/engine/core/viewsets.py index 3ea81fbf..b06111f9 100644 --- a/engine/core/viewsets.py +++ b/engine/core/viewsets.py @@ -228,7 +228,7 @@ class CategoryViewSet(EvibesViewSet): action_serializer_classes = { "list": CategorySimpleSerializer, } - lookup_field = "lookup_value" + lookup_field = "uuid" lookup_url_kwarg = "lookup_value" additional = {"seo_meta": "ALLOW"} @@ -356,7 +356,7 @@ class BrandViewSet(EvibesViewSet): action_serializer_classes = { "list": BrandSimpleSerializer, } - lookup_field = "lookup_value" + lookup_field = "uuid" lookup_url_kwarg = "lookup_value" additional = {"seo_meta": "ALLOW"} @@ -658,7 +658,7 @@ class OrderViewSet(EvibesViewSet): "performed and enforces permissions accordingly while interacting with order data." ) - lookup_field = "lookup_value" + lookup_field = "uuid" lookup_url_kwarg = "lookup_value" queryset = Order.objects.prefetch_related("order_products").all() filter_backends = [DjangoFilterBackend] @@ -690,7 +690,7 @@ class OrderViewSet(EvibesViewSet): return qs.filter(user=user) def get_object(self): - lookup_val = self.kwargs[self.lookup_field] + lookup_val = self.kwargs[self.lookup_url_kwarg] qs = self.get_queryset() try: diff --git a/evibes/middleware.py b/evibes/middleware.py index a4632511..c2d01e0e 100644 --- a/evibes/middleware.py +++ b/evibes/middleware.py @@ -3,6 +3,7 @@ import traceback from os import getenv from typing import Any, Callable, cast +from asgiref.sync import iscoroutinefunction, markcoroutinefunction from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ( BadRequest, @@ -81,11 +82,11 @@ class GrapheneJWTAuthorizationMiddleware: return next(root, info, **args) @staticmethod - def get_jwt_user(request: HttpRequest) -> "User" | AnonymousUser: + def get_jwt_user(request: HttpRequest) -> User | AnonymousUser: jwt_authenticator = JWTAuthentication() try: user_obj, _ = jwt_authenticator.authenticate(request) # type: ignore[assignment] - user: "User" | AnonymousUser = cast(User, user_obj) + user: User | AnonymousUser = cast(User, user_obj) except InvalidToken: user = AnonymousUser() except TypeError: @@ -175,10 +176,27 @@ class RateLimitMiddleware: class CamelCaseMiddleWare: + async_capable = True + sync_capable = True + def __init__(self, get_response): self.get_response = get_response + if iscoroutinefunction(get_response): + markcoroutinefunction(self) # ty:ignore[invalid-argument-type] - def __call__(self, request): + async def __call__(self, request): + if iscoroutinefunction(self.get_response): + self._underscoreize_request(request) + response = await self.get_response(request) + return response + return self._sync_call(request) + + def _sync_call(self, request): + self._underscoreize_request(request) + response = self.get_response(request) + return response + + def _underscoreize_request(self, request): underscoreized_get = underscoreize( {k: v for k, v in request.GET.lists()}, **JSON_UNDERSCOREIZE, @@ -194,6 +212,3 @@ class CamelCaseMiddleWare: new_get._mutable = False request.GET = new_get - - response = self.get_response(request) - return response diff --git a/evibes/settings/drf.py b/evibes/settings/drf.py index 37473c48..df6c81e1 100644 --- a/evibes/settings/drf.py +++ b/evibes/settings/drf.py @@ -143,4 +143,10 @@ SPECTACULAR_SETTINGS = { "email": "contact@fureunoir.com", "URL": "https://t.me/fureunoir", }, + "ENUM_NAME_OVERRIDES": { + "OrderStatusEnum": "engine.core.choices.ORDER_STATUS_CHOICES", + "OrderProductStatusEnum": "engine.core.choices.ORDER_PRODUCT_STATUS_CHOICES", + "TransactionStatusEnum": "engine.core.choices.TRANSACTION_STATUS_CHOICES", + "ThreadStatusEnum": "engine.vibes_auth.choices.ThreadStatus", + }, }