From d97e9a973b38f4f713960265e747ad3939162427 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 2 Mar 2026 01:31:41 +0300 Subject: [PATCH] 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. --- engine/core/docs/drf/viewsets.py | 14 ++++++++++++-- engine/core/graphene/mutations.py | 28 +--------------------------- engine/core/graphene/schema.py | 25 ++++++++++++++++++++++--- engine/core/widgets.py | 8 ++++++++ schon/settings/constance.py | 3 +-- 5 files changed, 44 insertions(+), 34 deletions(-) diff --git a/engine/core/docs/drf/viewsets.py b/engine/core/docs/drf/viewsets.py index eb6b0086..41b78250 100644 --- a/engine/core/docs/drf/viewsets.py +++ b/engine/core/docs/drf/viewsets.py @@ -1,7 +1,7 @@ from django.utils.translation import gettext_lazy as _ from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema -from rest_framework import status +from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer +from rest_framework import serializers, status from engine.core.docs.drf import BASE_ERRORS from engine.core.serializers import ( @@ -759,6 +759,16 @@ PRODUCT_SCHEMA = { "retrieve a list of products by identifier type (uuid, slug, or sku). " "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={ status.HTTP_200_OK: ProductSimpleSerializer(many=True), **BASE_ERRORS, diff --git a/engine/core/graphene/mutations.py b/engine/core/graphene/mutations.py index a0b82b4f..eca0f2c3 100644 --- a/engine/core/graphene/mutations.py +++ b/engine/core/graphene/mutations.py @@ -15,11 +15,10 @@ from engine.core.graphene.object_types import ( BulkProductInput, FeedbackType, OrderType, - ProductType, SearchResultsType, 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.caching import web_cache 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 class FeedbackProductAction(Mutation): class Meta: diff --git a/engine/core/graphene/schema.py b/engine/core/graphene/schema.py index 8c6ebc56..97bb1178 100644 --- a/engine/core/graphene/schema.py +++ b/engine/core/graphene/schema.py @@ -5,7 +5,7 @@ from django.core.cache import cache from django.core.exceptions import PermissionDenied from django.db.models import Case, Exists, IntegerField, OuterRef, Q, Value, When 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 ( DjangoFilterConnectionField, # ty:ignore[possibly-missing-import] ) @@ -41,7 +41,6 @@ from engine.core.graphene.mutations import ( RemoveOrderProductsOfAKind, RemoveWishlistProduct, RequestCursedURL, - RetrieveExactProducts, Search, ) from engine.core.graphene.object_types import ( @@ -135,6 +134,27 @@ class Query(ObjectType): promocodes = DjangoFilterConnectionField(PromoCodeType) brands = DjangoFilterConnectionField(BrandType, filterset_class=BrandFilter) 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 def resolve_parameters(_parent, _info): @@ -385,7 +405,6 @@ class Mutation(ObjectType): bulk_order_action = BulkOrderAction.Field() bulk_wishlist_action = BulkWishlistAction.Field() feedback_product_action = FeedbackProductAction.Field() - retrieve_exact_products = RetrieveExactProducts.Field() deposit = Deposit.Field() obtain_jwt_token = ObtainJSONWebToken.Field() refresh_jwt_token = RefreshJSONWebToken.Field() diff --git a/engine/core/widgets.py b/engine/core/widgets.py index 50d47c9b..ffeb61a5 100644 --- a/engine/core/widgets.py +++ b/engine/core/widgets.py @@ -4,10 +4,18 @@ from typing import Any, Mapping from django import forms from django.core.files.uploadedfile import UploadedFile from django.forms.renderers import BaseRenderer +from django.forms.widgets import PasswordInput from django.utils.datastructures import MultiValueDict 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): template_name = "json_table_widget.html" diff --git a/schon/settings/constance.py b/schon/settings/constance.py index 78b5dcec..e02ce44c 100644 --- a/schon/settings/constance.py +++ b/schon/settings/constance.py @@ -20,8 +20,7 @@ CONSTANCE_ADDITIONAL_FIELDS = { "django.forms.CharField", { "required": False, - "widget": "django.forms.PasswordInput", - "widget_attrs": {"render_value": True}, + "widget": "engine.core.widgets.PasswordInputRenderValue", }, ], }