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.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-12-19 17:27:36 +03:00
parent dc7f8be926
commit 29fb56be89
5 changed files with 41 additions and 17 deletions

View file

@ -1,6 +1,6 @@
from django.conf import settings from django.conf import settings
from django.http import FileResponse
from django.utils.translation import gettext_lazy as _ 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 drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
from rest_framework import status from rest_framework import status
from rest_framework.fields import CharField, DictField, JSONField, ListField 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. " "OpenApi3 schema for this API. Format can be selected via content negotiation. "
"Language can be selected with Accept-Language and query parameter both." "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"), summary=_("download a digital asset from purchased digital order"),
responses={ responses={
status.HTTP_200_OK: FileResponse, status.HTTP_200_OK: OpenApiTypes.BINARY,
status.HTTP_400_BAD_REQUEST: error, status.HTTP_400_BAD_REQUEST: error,
}, },
) )

View file

@ -234,7 +234,7 @@ CATEGORY_SCHEMA = {
name="lookup_value", name="lookup_value",
location="path", location="path",
description=_("Category UUID or slug"), description=_("Category UUID or slug"),
type=str, type=OpenApiTypes.STR,
), ),
], ],
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
@ -284,7 +284,7 @@ CATEGORY_SCHEMA = {
name="lookup_value", name="lookup_value",
location="path", location="path",
description=_("Category UUID or slug"), description=_("Category UUID or slug"),
type=str, type=OpenApiTypes.STR,
), ),
], ],
responses={ responses={
@ -369,7 +369,7 @@ ORDER_SCHEMA = {
name="lookup_value", name="lookup_value",
location="path", location="path",
description=_("Order UUID or human-readable id"), description=_("Order UUID or human-readable id"),
type=str, type=OpenApiTypes.STR,
), ),
], ],
responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS},
@ -1006,7 +1006,7 @@ BRAND_SCHEMA = {
name="lookup_value", name="lookup_value",
location="path", location="path",
description=_("Brand UUID or slug"), description=_("Brand UUID or slug"),
type=str, type=OpenApiTypes.STR,
), ),
], ],
responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS},
@ -1049,7 +1049,7 @@ BRAND_SCHEMA = {
name="lookup_value", name="lookup_value",
location="path", location="path",
description=_("Brand UUID or slug"), description=_("Brand UUID or slug"),
type=str, type=OpenApiTypes.STR,
), ),
], ],
responses={status.HTTP_200_OK: SeoSnapshotSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: SeoSnapshotSerializer(), **BASE_ERRORS},

View file

@ -228,7 +228,7 @@ class CategoryViewSet(EvibesViewSet):
action_serializer_classes = { action_serializer_classes = {
"list": CategorySimpleSerializer, "list": CategorySimpleSerializer,
} }
lookup_field = "lookup_value" lookup_field = "uuid"
lookup_url_kwarg = "lookup_value" lookup_url_kwarg = "lookup_value"
additional = {"seo_meta": "ALLOW"} additional = {"seo_meta": "ALLOW"}
@ -356,7 +356,7 @@ class BrandViewSet(EvibesViewSet):
action_serializer_classes = { action_serializer_classes = {
"list": BrandSimpleSerializer, "list": BrandSimpleSerializer,
} }
lookup_field = "lookup_value" lookup_field = "uuid"
lookup_url_kwarg = "lookup_value" lookup_url_kwarg = "lookup_value"
additional = {"seo_meta": "ALLOW"} additional = {"seo_meta": "ALLOW"}
@ -658,7 +658,7 @@ class OrderViewSet(EvibesViewSet):
"performed and enforces permissions accordingly while interacting with order data." "performed and enforces permissions accordingly while interacting with order data."
) )
lookup_field = "lookup_value" lookup_field = "uuid"
lookup_url_kwarg = "lookup_value" lookup_url_kwarg = "lookup_value"
queryset = Order.objects.prefetch_related("order_products").all() queryset = Order.objects.prefetch_related("order_products").all()
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
@ -690,7 +690,7 @@ class OrderViewSet(EvibesViewSet):
return qs.filter(user=user) return qs.filter(user=user)
def get_object(self): def get_object(self):
lookup_val = self.kwargs[self.lookup_field] lookup_val = self.kwargs[self.lookup_url_kwarg]
qs = self.get_queryset() qs = self.get_queryset()
try: try:

View file

@ -3,6 +3,7 @@ import traceback
from os import getenv from os import getenv
from typing import Any, Callable, cast from typing import Any, Callable, cast
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ( from django.core.exceptions import (
BadRequest, BadRequest,
@ -81,11 +82,11 @@ class GrapheneJWTAuthorizationMiddleware:
return next(root, info, **args) return next(root, info, **args)
@staticmethod @staticmethod
def get_jwt_user(request: HttpRequest) -> "User" | AnonymousUser: def get_jwt_user(request: HttpRequest) -> User | AnonymousUser:
jwt_authenticator = JWTAuthentication() jwt_authenticator = JWTAuthentication()
try: try:
user_obj, _ = jwt_authenticator.authenticate(request) # type: ignore[assignment] 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: except InvalidToken:
user = AnonymousUser() user = AnonymousUser()
except TypeError: except TypeError:
@ -175,10 +176,27 @@ class RateLimitMiddleware:
class CamelCaseMiddleWare: class CamelCaseMiddleWare:
async_capable = True
sync_capable = True
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = 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( underscoreized_get = underscoreize(
{k: v for k, v in request.GET.lists()}, {k: v for k, v in request.GET.lists()},
**JSON_UNDERSCOREIZE, **JSON_UNDERSCOREIZE,
@ -194,6 +212,3 @@ class CamelCaseMiddleWare:
new_get._mutable = False new_get._mutable = False
request.GET = new_get request.GET = new_get
response = self.get_response(request)
return response

View file

@ -143,4 +143,10 @@ SPECTACULAR_SETTINGS = {
"email": "contact@fureunoir.com", "email": "contact@fureunoir.com",
"URL": "https://t.me/fureunoir", "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",
},
} }