feat(viewsets, schema): unify retrieveExactProducts API in DRF and GraphQL

replace the custom Graphene mutation `RetrieveExactProducts` with a unified DRF implementation using `inline_serializer`. Updated GraphQL schema to resolve `retrieve_exact_products` query using a standardized approach.

This change improves consistency across DRF and GraphQL APIs, reduces duplicate logic, and centralizes request validations.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-03-02 01:31:41 +03:00
parent 85576cf4ad
commit d97e9a973b
5 changed files with 44 additions and 34 deletions

View file

@ -1,7 +1,7 @@
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.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
from rest_framework import status from rest_framework import serializers, status
from engine.core.docs.drf import BASE_ERRORS from engine.core.docs.drf import BASE_ERRORS
from engine.core.serializers import ( from engine.core.serializers import (
@ -759,6 +759,16 @@ PRODUCT_SCHEMA = {
"retrieve a list of products by identifier type (uuid, slug, or sku). " "retrieve a list of products by identifier type (uuid, slug, or sku). "
"Send a POST request with `identificator_type` and `identificators` (list of values)." "Send a POST request with `identificator_type` and `identificators` (list of values)."
), ),
request=inline_serializer(
name="ExactProductsRequest",
fields={
"identificator_type": serializers.ChoiceField(
choices=["uuid", "slug", "sku"]
),
"identificators": serializers.ListField(child=serializers.CharField()),
},
),
parameters=[],
responses={ responses={
status.HTTP_200_OK: ProductSimpleSerializer(many=True), status.HTTP_200_OK: ProductSimpleSerializer(many=True),
**BASE_ERRORS, **BASE_ERRORS,

View file

@ -15,11 +15,10 @@ from engine.core.graphene.object_types import (
BulkProductInput, BulkProductInput,
FeedbackType, FeedbackType,
OrderType, OrderType,
ProductType,
SearchResultsType, SearchResultsType,
WishlistType, WishlistType,
) )
from engine.core.models import Address, Order, OrderProduct, Product, Wishlist from engine.core.models import Address, Order, OrderProduct, Wishlist
from engine.core.utils import format_attributes, is_url_safe from engine.core.utils import format_attributes, is_url_safe
from engine.core.utils.caching import web_cache from engine.core.utils.caching import web_cache
from engine.core.utils.emailing import contact_us_email from engine.core.utils.emailing import contact_us_email
@ -575,31 +574,6 @@ class BuyProduct(Mutation):
) )
class RetrieveExactProducts(Mutation):
class Meta:
description = _("retrieve exact products by identificator")
class Arguments:
identificator_type = String(required=True)
identificators = List(String, required=True)
products = List(ProductType, required=True)
def mutate(self, info, identificator_type: str, identificators: list[str]):
match identificator_type:
case "uuid":
products = Product.objects.filter(uuid__in=identificators)
case "slug":
products = Product.objects.filter(slug__in=identificators)
case "sku":
products = Product.objects.filter(sku__in=identificators)
case _:
raise BadRequest(
_("identificator_type must be one of: uuid, slug, sku")
)
return RetrieveExactProducts(products=products) # ty: ignore[unknown-argument]
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class FeedbackProductAction(Mutation): class FeedbackProductAction(Mutation):
class Meta: class Meta:

View file

@ -5,7 +5,7 @@ from django.core.cache import cache
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import Case, Exists, IntegerField, OuterRef, Q, Value, When from django.db.models import Case, Exists, IntegerField, OuterRef, Q, Value, When
from django.utils import timezone from django.utils import timezone
from graphene import Field, List, ObjectType, Schema from graphene import Argument, Field, List, ObjectType, Schema, String
from graphene_django.filter import ( from graphene_django.filter import (
DjangoFilterConnectionField, # ty:ignore[possibly-missing-import] DjangoFilterConnectionField, # ty:ignore[possibly-missing-import]
) )
@ -41,7 +41,6 @@ from engine.core.graphene.mutations import (
RemoveOrderProductsOfAKind, RemoveOrderProductsOfAKind,
RemoveWishlistProduct, RemoveWishlistProduct,
RequestCursedURL, RequestCursedURL,
RetrieveExactProducts,
Search, Search,
) )
from engine.core.graphene.object_types import ( from engine.core.graphene.object_types import (
@ -135,6 +134,27 @@ class Query(ObjectType):
promocodes = DjangoFilterConnectionField(PromoCodeType) promocodes = DjangoFilterConnectionField(PromoCodeType)
brands = DjangoFilterConnectionField(BrandType, filterset_class=BrandFilter) brands = DjangoFilterConnectionField(BrandType, filterset_class=BrandFilter)
posts = DjangoFilterConnectionField(PostType, filterset_class=PostFilter) posts = DjangoFilterConnectionField(PostType, filterset_class=PostFilter)
retrieve_exact_products = List(
ProductType,
identificator_type=Argument(String, required=True),
identificators=Argument(List(String), required=True),
)
@staticmethod
def resolve_retrieve_exact_products(
_parent, _info, identificator_type: str, identificators: list[str]
):
from graphql import GraphQLError
match identificator_type:
case "uuid":
return Product.objects.filter(uuid__in=identificators)
case "slug":
return Product.objects.filter(slug__in=identificators)
case "sku":
return Product.objects.filter(sku__in=identificators)
case _:
raise GraphQLError("identificator_type must be one of: uuid, slug, sku")
@staticmethod @staticmethod
def resolve_parameters(_parent, _info): def resolve_parameters(_parent, _info):
@ -385,7 +405,6 @@ class Mutation(ObjectType):
bulk_order_action = BulkOrderAction.Field() bulk_order_action = BulkOrderAction.Field()
bulk_wishlist_action = BulkWishlistAction.Field() bulk_wishlist_action = BulkWishlistAction.Field()
feedback_product_action = FeedbackProductAction.Field() feedback_product_action = FeedbackProductAction.Field()
retrieve_exact_products = RetrieveExactProducts.Field()
deposit = Deposit.Field() deposit = Deposit.Field()
obtain_jwt_token = ObtainJSONWebToken.Field() obtain_jwt_token = ObtainJSONWebToken.Field()
refresh_jwt_token = RefreshJSONWebToken.Field() refresh_jwt_token = RefreshJSONWebToken.Field()

View file

@ -4,10 +4,18 @@ from typing import Any, Mapping
from django import forms from django import forms
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.forms.renderers import BaseRenderer from django.forms.renderers import BaseRenderer
from django.forms.widgets import PasswordInput
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
class PasswordInputRenderValue(PasswordInput):
"""PasswordInput with render_value=True so constance re-displays the current value."""
def __init__(self, attrs=None):
super().__init__(attrs=attrs, render_value=True)
class JSONTableWidget(forms.Widget): class JSONTableWidget(forms.Widget):
template_name = "json_table_widget.html" template_name = "json_table_widget.html"

View file

@ -20,8 +20,7 @@ CONSTANCE_ADDITIONAL_FIELDS = {
"django.forms.CharField", "django.forms.CharField",
{ {
"required": False, "required": False,
"widget": "django.forms.PasswordInput", "widget": "engine.core.widgets.PasswordInputRenderValue",
"widget_attrs": {"render_value": True},
}, },
], ],
} }