Features:

1) Enhance lookup logic to support UUID validation for products in viewsets;
2) Add extensive filtering, sorting, and attributes documentation for product endpoints;
3) Define new OpenAPI parameters for querying products with detailed constraints.

Fixes:
1) Add missing import for `UUID` in core viewsets.

Extra:
1) Refactor and reorganize product API schema for clarity and consistency.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-05-08 17:33:40 +03:00
parent 9941b45bd3
commit f5c1d64d46
2 changed files with 67 additions and 39 deletions

View file

@ -256,9 +256,51 @@ WISHLIST_SCHEMA = {
), ),
} }
ATTRIBUTES_DESC = _(
"Filter by one or more attribute name/value pairs. \n"
"• **Syntax**: `attr_name=method-value[;attr2=method2-value2]…` \n"
"• **Methods** (defaults to `icontains` if omitted): "
"`iexact`, `exact`, `icontains`, `contains`, `isnull`, "
"`startswith`, `istartswith`, `endswith`, `iendswith`, "
"`regex`, `iregex`, `lt`, `lte`, `gt`, `gte`, `in` \n"
"• **Value typing**: JSON is attempted first (so you can pass lists/dicts), "
"`true`/`false` for booleans, integers, floats; otherwise treated as string. \n"
"• **Base64**: prefix with `b64-` to URL-safe base64-encode the raw value. \n"
"Examples: \n"
"`color=exact-red`, `size=gt-10`, `features=in-[\"wifi\",\"bluetooth\"]`, \n"
"`b64-description=icontains-aGVhdC1jb2xk`"
)
PRODUCT_SCHEMA = { PRODUCT_SCHEMA = {
"list": extend_schema( "list": extend_schema(
summary=_("list all products (simple view)"), summary=_("list all products (simple view)"),
parameters=[
OpenApiParameter("uuid", _("(exact) Product UUID"), OpenApiParameter.QUERY, type=str),
OpenApiParameter("name", _("(icontains) Product name"), OpenApiParameter.QUERY, type=str),
OpenApiParameter("categories", _("(list) Category names, case-insensitive"), OpenApiParameter.QUERY,
type=str),
OpenApiParameter("category_uuid", _("(exact) Category UUID"), OpenApiParameter.QUERY, type=str),
OpenApiParameter("tags", _("(list) Tag names, case-insensitive"), OpenApiParameter.QUERY, type=str),
OpenApiParameter("min_price", _("(gte) Minimum stock price"), OpenApiParameter.QUERY, type=float),
OpenApiParameter("max_price", _("(lte) Maximum stock price"), OpenApiParameter.QUERY, type=float),
OpenApiParameter("is_active", _("(exact) Only active products"), OpenApiParameter.QUERY, type=bool),
OpenApiParameter("brand", _("(iexact) Brand name"), OpenApiParameter.QUERY, type=str),
OpenApiParameter("attributes", ATTRIBUTES_DESC, OpenApiParameter.QUERY, type=str),
OpenApiParameter("quantity", _("(gt) Minimum stock quantity"), OpenApiParameter.QUERY, type=int),
OpenApiParameter("slug", _("(exact) Product slug"), OpenApiParameter.QUERY, type=str),
OpenApiParameter("is_digital", _("(exact) Digital vs. physical"), OpenApiParameter.QUERY, type=bool),
OpenApiParameter(
name="order_by",
description=_(
"Comma-separated list of fields to sort by. "
"Prefix with `-` for descending. \n"
"**Allowed:** uuid, rating, name, slug, created, modified, price, random"
),
required=False,
type=str,
location=OpenApiParameter.QUERY,
),
],
responses={ responses={
status.HTTP_200_OK: ProductSimpleSerializer(many=True), status.HTTP_200_OK: ProductSimpleSerializer(many=True),
**BASE_ERRORS, **BASE_ERRORS,
@ -266,46 +308,26 @@ PRODUCT_SCHEMA = {
), ),
"retrieve": extend_schema( "retrieve": extend_schema(
summary=_("retrieve a single product (detailed view)"), summary=_("retrieve a single product (detailed view)"),
parameters=[ parameters=[OpenApiParameter("lookup", _("Product UUID or slug"), OpenApiParameter.PATH, type=str)],
OpenApiParameter( responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
name="lookup",
description=_("Product UUID or slug"),
required=True,
type=str,
location=OpenApiParameter.PATH,
),
],
responses={
status.HTTP_200_OK: ProductDetailSerializer,
**BASE_ERRORS,
},
), ),
"create": extend_schema( "create": extend_schema(
summary=_("create a product"), summary=_("create a product"),
responses={ responses={status.HTTP_201_CREATED: ProductDetailSerializer, **BASE_ERRORS},
status.HTTP_201_CREATED: ProductDetailSerializer,
**BASE_ERRORS,
},
),
"destroy": extend_schema(
summary=_("delete a product"),
responses={
status.HTTP_204_NO_CONTENT: {},
**BASE_ERRORS,
},
), ),
"update": extend_schema( "update": extend_schema(
summary=_("rewrite an existing product, preserving non-editable fields"), summary=_("rewrite an existing product, preserving non-editable fields"),
responses={ parameters=[OpenApiParameter("lookup", _("Product UUID or slug"), OpenApiParameter.PATH, type=str)],
status.HTTP_200_OK: ProductDetailSerializer, responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
**BASE_ERRORS,
},
), ),
"partial_update": extend_schema( "partial_update": extend_schema(
summary=_("update some fields of an existing product, preserving non-editable fields"), summary=_("update some fields of an existing product, preserving non-editable fields"),
responses={ parameters=[OpenApiParameter("lookup", _("Product UUID or slug"), OpenApiParameter.PATH, type=str)],
status.HTTP_200_OK: ProductDetailSerializer, responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
**BASE_ERRORS, ),
}, "destroy": extend_schema(
summary=_("delete a product"),
parameters=[OpenApiParameter("lookup", _("Product UUID or slug"), OpenApiParameter.PATH, type=str)],
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
), ),
} }

View file

@ -1,3 +1,5 @@
from uuid import UUID
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
@ -168,13 +170,17 @@ class ProductViewSet(EvibesViewSet):
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
lookup_value = self.kwargs[self.lookup_url_kwarg] lookup_value = self.kwargs[self.lookup_url_kwarg]
obj = ( obj = None
queryset.filter(uuid=lookup_value)
.first() try:
or uuid_obj = UUID(lookup_value)
queryset.filter(slug=lookup_value) obj = queryset.filter(uuid=uuid_obj).first()
.first() except (ValueError, TypeError):
) pass
if not obj:
obj = queryset.filter(slug=lookup_value).first()
if not obj: if not obj:
raise Http404(f"No Product found matching uuid or slug '{lookup_value}'") raise Http404(f"No Product found matching uuid or slug '{lookup_value}'")