From 30144c5b6ae5c33045918c41267620f4120dc6eb Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 18 Jun 2025 23:03:50 +0300 Subject: [PATCH] Features: 1) Add `image` field to search result types (`SearchProductsResultsType`, `SearchCategoriesResultsType`, `SearchBrandsResultsType`); 2) Enhance `tags` field for categories with `DjangoFilterConnectionField`; 3) Implement descriptive error handling in `rebuild_slugs` script. Fixes: 1) Correct inconsistent function calls with improved attribute formatting; 2) Fix invalid key usage (`uuid` vs. `pk`) in slug rebuilding logic to prevent uniqueness errors; 3) Ensure correct `query` usage in `process_query` function calls. Extra: Reformat code for readability by updating indentation, and breaking long function signatures and expressions into multiple lines. --- core/graphene/mutations.py | 75 +++++++++++---- core/graphene/object_types.py | 111 +++++++++++++++++----- core/management/commands/rebuild_slugs.py | 9 +- 3 files changed, 153 insertions(+), 42 deletions(-) diff --git a/core/graphene/mutations.py b/core/graphene/mutations.py index 0fccb0dd..d285c37d 100644 --- a/core/graphene/mutations.py +++ b/core/graphene/mutations.py @@ -35,7 +35,9 @@ class CacheOperator(BaseMutation): description = _("cache I/O") class Arguments: - key = String(required=True, description=_("key to look for in or set into the cache")) + key = String( + required=True, description=_("key to look for in or set into the cache") + ) data = GenericScalar(required=False, description=_("data to store in cache")) timeout = Int( required=False, @@ -65,7 +67,9 @@ class RequestCursedURL(BaseMutation): try: data = cache.get(url, None) if not data: - response = requests.get(url, headers={"content-type": "application/json"}) + response = requests.get( + url, headers={"content-type": "application/json"} + ) response.raise_for_status() data = camelize(response.json()) cache.set(url, data, 86400) @@ -93,7 +97,9 @@ class AddOrderProduct(BaseMutation): if not (user.has_perm("core.add_orderproduct") or user == order.user): raise PermissionDenied(permission_denied_message) - order = order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes)) + order = order.add_product( + product_uuid=product_uuid, attributes=format_attributes(attributes) + ) return AddOrderProduct(order=order) except Order.DoesNotExist: @@ -119,7 +125,9 @@ class RemoveOrderProduct(BaseMutation): if not (user.has_perm("core.change_orderproduct") or user == order.user): raise PermissionDenied(permission_denied_message) - order = order.remove_product(product_uuid=product_uuid, attributes=format_attributes(attributes)) + order = order.remove_product( + product_uuid=product_uuid, attributes=format_attributes(attributes) + ) return AddOrderProduct(order=order) except Order.DoesNotExist: @@ -198,7 +206,11 @@ class BuyOrder(BaseMutation): billing_address=None, ): if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): - raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive")) + raise BadRequest( + _( + "please provide either order_uuid or order_hr_id - mutually exclusive" + ) + ) user = info.context.user try: order = None @@ -222,7 +234,11 @@ class BuyOrder(BaseMutation): case "": return BuyOrder(order=instance) case _: - raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}")) + raise TypeError( + _( + f"wrong type came from order.buy() method: {type(instance)!s}" + ) + ) except Order.DoesNotExist: raise Http404(_(f"order {order_uuid} not found")) @@ -250,7 +266,11 @@ class BulkOrderAction(BaseMutation): order_hr_id=None, ): if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): - raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive")) + raise BadRequest( + _( + "please provide either order_uuid or order_hr_id - mutually exclusive" + ) + ) user = info.context.user try: order = None @@ -432,14 +452,20 @@ class BuyWishlist(BaseMutation): ): order.add_product(product_uuid=product.pk) - instance = order.buy(force_balance=force_balance, force_payment=force_payment) + instance = order.buy( + force_balance=force_balance, force_payment=force_payment + ) match str(type(instance)): case "": return BuyWishlist(transaction=instance) case "": return BuyWishlist(order=instance) case _: - raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}")) + raise TypeError( + _( + f"wrong type came from order.buy() method: {type(instance)!s}" + ) + ) except Wishlist.DoesNotExist: raise Http404(_(f"wishlist {wishlist_uuid} not found")) @@ -453,7 +479,9 @@ class BuyProduct(BaseMutation): product_uuid = UUID(required=True) attributes = String( required=False, - description=_("please send the attributes as the string formatted like attr1=value1,attr2=value2"), + description=_( + "please send the attributes as the string formatted like attr1=value1,attr2=value2" + ), ) force_balance = Boolean(required=False) force_payment = Boolean(required=False) @@ -462,10 +490,19 @@ class BuyProduct(BaseMutation): transaction = Field(TransactionType, required=False) @staticmethod - def mutate(_parent, info, product_uuid, attributes=None, force_balance=False, force_payment=False): + def mutate( + _parent, + info, + product_uuid, + attributes=None, + force_balance=False, + force_payment=False, + ): user = info.context.user order = Order.objects.create(user=user, status="MOMENTAL") - order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes)) + order.add_product( + product_uuid=product_uuid, attributes=format_attributes(attributes) + ) instance = order.buy(force_balance=force_balance, force_payment=force_payment) match str(type(instance)): case "": @@ -473,7 +510,9 @@ class BuyProduct(BaseMutation): case "": return BuyProduct(order=instance) case _: - raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}")) + raise TypeError( + _(f"wrong type came from order.buy() method: {type(instance)!s}") + ) class CreateProduct(BaseMutation): @@ -489,7 +528,9 @@ class CreateProduct(BaseMutation): if not info.context.user.has_perm("core.add_product"): raise PermissionDenied(permission_denied_message) category = Category.objects.get(uuid=category_uuid) - product = Product.objects.create(name=name, description=description, category=category) + product = Product.objects.create( + name=name, description=description, category=category + ) return CreateProduct(product=product) @@ -536,7 +577,9 @@ class DeleteProduct(BaseMutation): class CreateAddress(BaseMutation): class Arguments: - raw_data = String(required=True, description=_("original address string provided by the user")) + raw_data = String( + required=True, description=_("original address string provided by the user") + ) address = Field(AddressType) @@ -632,7 +675,7 @@ class Search(BaseMutation): @staticmethod def mutate(_parent, info, query): - data = process_query(query) + data = process_query(query=query, request=info.context) return Search( results=SearchResultsType( diff --git a/core/graphene/object_types.py b/core/graphene/object_types.py index d63a7171..6475d7e8 100644 --- a/core/graphene/object_types.py +++ b/core/graphene/object_types.py @@ -2,7 +2,18 @@ from django.core.cache import cache from django.db.models import Max, Min from django.db.models.functions import Length from django.utils.translation import gettext_lazy as _ -from graphene import UUID, Field, Float, InputObjectType, Int, List, NonNull, ObjectType, String, relay +from graphene import ( + UUID, + Field, + Float, + InputObjectType, + Int, + List, + NonNull, + ObjectType, + String, + relay, +) from graphene.types.generic import GenericScalar from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField @@ -90,10 +101,16 @@ class BrandType(DjangoObjectType): return self.categories.filter(is_active=True) def resolve_big_logo(self: Brand, info): - return info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else "" + return ( + info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else "" + ) def resolve_small_logo(self: Brand, info): - return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else "" + return ( + info.context.build_absolute_uri(self.small_logo.url) + if self.small_logo + else "" + ) class FilterableAttributeType(ObjectType): @@ -115,14 +132,22 @@ class CategoryType(DjangoObjectType): markup_percent = Float(required=False, description=_("markup percentage")) filterable_attributes = List( NonNull(FilterableAttributeType), - description=_("which attributes and values can be used for filtering this category."), + description=_( + "which attributes and values can be used for filtering this category." + ), ) min_max_prices = Field( NonNull(MinMaxPriceType), - description=_("minimum and maximum prices for products in this category, if available."), + description=_( + "minimum and maximum prices for products in this category, if available." + ), + ) + tags = DjangoFilterConnectionField( + lambda: CategoryTagType, description=_("tags for this category") + ) + products = DjangoFilterConnectionField( + lambda: ProductType, description=_("products in this category") ) - tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category")) - products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category")) class Meta: model = Category @@ -198,9 +223,14 @@ class CategoryType(DjangoObjectType): ) min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0) min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0) - cache.set(key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400) + cache.set( + key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400 + ) - return {"min_price": min_max_prices["min_price"], "max_price": min_max_prices["max_price"]} + return { + "min_price": min_max_prices["min_price"], + "max_price": min_max_prices["max_price"], + } class VendorType(DjangoObjectType): @@ -245,7 +275,9 @@ class AddressType(DjangoObjectType): class FeedbackType(DjangoObjectType): comment = String(description=_("comment")) - rating = Int(description=_("rating value from 1 to 10, inclusive, or 0 if not set.")) + rating = Int( + description=_("rating value from 1 to 10, inclusive, or 0 if not set.") + ) class Meta: model = Feedback @@ -258,7 +290,9 @@ class FeedbackType(DjangoObjectType): class OrderProductType(DjangoObjectType): attributes = GenericScalar(description=_("attributes")) notifications = GenericScalar(description=_("notifications")) - download_url = String(description=_("download url for this order product if applicable")) + download_url = String( + description=_("download url for this order product if applicable") + ) class Meta: model = OrderProduct @@ -292,7 +326,9 @@ class OrderType(DjangoObjectType): billing_address = Field(AddressType, description=_("billing address")) shipping_address = Field( AddressType, - description=_("shipping address for this order, leave blank if same as billing address or if not applicable"), + description=_( + "shipping address for this order, leave blank if same as billing address or if not applicable" + ), ) total_price = Float(description=_("total price of this order")) total_quantity = Int(description=_("total quantity of products in order")) @@ -350,7 +386,9 @@ class ProductType(DjangoObjectType): images = DjangoFilterConnectionField(ProductImageType, description=_("images")) feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks")) brand = Field(BrandType, description=_("brand")) - attribute_groups = DjangoFilterConnectionField(AttributeGroupType, description=_("attribute groups")) + attribute_groups = DjangoFilterConnectionField( + AttributeGroupType, description=_("attribute groups") + ) price = Float(description=_("price")) quantity = Float(description=_("quantity")) feedbacks_count = Int(description=_("number of feedbacks")) @@ -387,7 +425,9 @@ class ProductType(DjangoObjectType): def resolve_attribute_groups(self: Product, info): info.context._product_uuid = self.uuid - return AttributeGroup.objects.filter(attributes__values__product=self).distinct() + return AttributeGroup.objects.filter( + attributes__values__product=self + ).distinct() def resolve_quantity(self, _info) -> int: return self.quantity or 0 @@ -422,14 +462,20 @@ class PromoCodeType(DjangoObjectType): description = _("promocodes") def resolve_discount(self: PromoCode, _info) -> float: - return float(self.discount_percent) if self.discount_percent else float(self.discount_amount) + return ( + float(self.discount_percent) + if self.discount_percent + else float(self.discount_amount) + ) def resolve_discount_type(self: PromoCode, _info) -> str: return "percent" if self.discount_percent else "amount" class PromotionType(DjangoObjectType): - products = DjangoFilterConnectionField(ProductType, description=_("products on sale")) + products = DjangoFilterConnectionField( + ProductType, description=_("products on sale") + ) class Meta: model = Promotion @@ -452,7 +498,9 @@ class StockType(DjangoObjectType): class WishlistType(DjangoObjectType): - products = DjangoFilterConnectionField(ProductType, description=_("wishlisted products")) + products = DjangoFilterConnectionField( + ProductType, description=_("wishlisted products") + ) class Meta: model = Wishlist @@ -462,7 +510,9 @@ class WishlistType(DjangoObjectType): class ProductTagType(DjangoObjectType): - product_set = DjangoFilterConnectionField(ProductType, description=_("tagged products")) + product_set = DjangoFilterConnectionField( + ProductType, description=_("tagged products") + ) class Meta: model = ProductTag @@ -473,7 +523,9 @@ class ProductTagType(DjangoObjectType): class CategoryTagType(DjangoObjectType): - category_set = DjangoFilterConnectionField(CategoryType, description=_("tagged categories")) + category_set = DjangoFilterConnectionField( + CategoryType, description=_("tagged categories") + ) class Meta: model = CategoryTag @@ -489,7 +541,11 @@ class ConfigType(ObjectType): company_name = String(description=_("company name")) company_address = String(description=_("company address")) company_phone_number = String(description=_("company phone number")) - email_from = String(description=_("email from, sometimes it must be used instead of host user value")) + email_from = String( + description=_( + "email from, sometimes it must be used instead of host user value" + ) + ) email_host_user = String(description=_("email host user")) payment_gateway_maximum = Float(description=_("maximum amount for payment")) payment_gateway_minimum = Float(description=_("minimum amount for payment")) @@ -513,18 +569,21 @@ class SearchProductsResultsType(ObjectType): uuid = UUID() name = String() slug = String() + image = String() class SearchCategoriesResultsType(ObjectType): uuid = UUID() name = String() slug = String() + image = String() class SearchBrandsResultsType(ObjectType): uuid = UUID() name = String() slug = String() + image = String() class SearchPostsResultsType(ObjectType): @@ -534,9 +593,15 @@ class SearchPostsResultsType(ObjectType): class SearchResultsType(ObjectType): - products = List(description=_("products search results"), of_type=SearchProductsResultsType) - categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType) - brands = List(description=_("products search results"), of_type=SearchBrandsResultsType) + products = List( + description=_("products search results"), of_type=SearchProductsResultsType + ) + categories = List( + description=_("products search results"), of_type=SearchCategoriesResultsType + ) + brands = List( + description=_("products search results"), of_type=SearchBrandsResultsType + ) posts = List(description=_("posts search results"), of_type=SearchPostsResultsType) diff --git a/core/management/commands/rebuild_slugs.py b/core/management/commands/rebuild_slugs.py index 4aacc752..af39671a 100644 --- a/core/management/commands/rebuild_slugs.py +++ b/core/management/commands/rebuild_slugs.py @@ -15,8 +15,10 @@ class Command(BaseCommand): ) for idx, instance in enumerate(queryset.iterator(), start=1): try: - if ( - queryset.filter(name=instance.name).exclude(pk=instance.pk).count() + while ( + queryset.filter(name=instance.name) + .exclude(uuid=instance.uuid) + .count() >= 1 ): instance.name = f"{instance.name} - {get_random_string(length=3, allowed_chars='0123456789')}" @@ -34,7 +36,8 @@ class Command(BaseCommand): except Exception as e: self.stderr.write( self.style.ERROR( - f"[{idx}/{total}] ({queryset.model._meta.verbose_name_plural}: {instance.pk}) ERROR: {e}" + f"[{idx}/{total}] ({queryset.model._meta.verbose_name_plural}: {instance.name}/{instance.uuid})" + f" ERROR: {e}" ) )