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.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-06-18 23:03:50 +03:00
parent d4ff637169
commit 30144c5b6a
3 changed files with 153 additions and 42 deletions

View file

@ -35,7 +35,9 @@ class CacheOperator(BaseMutation):
description = _("cache I/O") description = _("cache I/O")
class Arguments: 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")) data = GenericScalar(required=False, description=_("data to store in cache"))
timeout = Int( timeout = Int(
required=False, required=False,
@ -65,7 +67,9 @@ class RequestCursedURL(BaseMutation):
try: try:
data = cache.get(url, None) data = cache.get(url, None)
if not data: 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() response.raise_for_status()
data = camelize(response.json()) data = camelize(response.json())
cache.set(url, data, 86400) cache.set(url, data, 86400)
@ -93,7 +97,9 @@ class AddOrderProduct(BaseMutation):
if not (user.has_perm("core.add_orderproduct") or user == order.user): if not (user.has_perm("core.add_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message) 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) return AddOrderProduct(order=order)
except Order.DoesNotExist: except Order.DoesNotExist:
@ -119,7 +125,9 @@ class RemoveOrderProduct(BaseMutation):
if not (user.has_perm("core.change_orderproduct") or user == order.user): if not (user.has_perm("core.change_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message) 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) return AddOrderProduct(order=order)
except Order.DoesNotExist: except Order.DoesNotExist:
@ -198,7 +206,11 @@ class BuyOrder(BaseMutation):
billing_address=None, billing_address=None,
): ):
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): 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 user = info.context.user
try: try:
order = None order = None
@ -222,7 +234,11 @@ class BuyOrder(BaseMutation):
case "<class 'core.models.Order'>": case "<class 'core.models.Order'>":
return BuyOrder(order=instance) return BuyOrder(order=instance)
case _: 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: except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found")) raise Http404(_(f"order {order_uuid} not found"))
@ -250,7 +266,11 @@ class BulkOrderAction(BaseMutation):
order_hr_id=None, order_hr_id=None,
): ):
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): 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 user = info.context.user
try: try:
order = None order = None
@ -432,14 +452,20 @@ class BuyWishlist(BaseMutation):
): ):
order.add_product(product_uuid=product.pk) 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)): match str(type(instance)):
case "<class 'payments.models.Transaction'>": case "<class 'payments.models.Transaction'>":
return BuyWishlist(transaction=instance) return BuyWishlist(transaction=instance)
case "<class 'core.models.Order'>": case "<class 'core.models.Order'>":
return BuyWishlist(order=instance) return BuyWishlist(order=instance)
case _: 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: except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) raise Http404(_(f"wishlist {wishlist_uuid} not found"))
@ -453,7 +479,9 @@ class BuyProduct(BaseMutation):
product_uuid = UUID(required=True) product_uuid = UUID(required=True)
attributes = String( attributes = String(
required=False, 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_balance = Boolean(required=False)
force_payment = Boolean(required=False) force_payment = Boolean(required=False)
@ -462,10 +490,19 @@ class BuyProduct(BaseMutation):
transaction = Field(TransactionType, required=False) transaction = Field(TransactionType, required=False)
@staticmethod @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 user = info.context.user
order = Order.objects.create(user=user, status="MOMENTAL") 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) instance = order.buy(force_balance=force_balance, force_payment=force_payment)
match str(type(instance)): match str(type(instance)):
case "<class 'payments.models.Transaction'>": case "<class 'payments.models.Transaction'>":
@ -473,7 +510,9 @@ class BuyProduct(BaseMutation):
case "<class 'core.models.Order'>": case "<class 'core.models.Order'>":
return BuyProduct(order=instance) return BuyProduct(order=instance)
case _: 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): class CreateProduct(BaseMutation):
@ -489,7 +528,9 @@ class CreateProduct(BaseMutation):
if not info.context.user.has_perm("core.add_product"): if not info.context.user.has_perm("core.add_product"):
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
category = Category.objects.get(uuid=category_uuid) 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) return CreateProduct(product=product)
@ -536,7 +577,9 @@ class DeleteProduct(BaseMutation):
class CreateAddress(BaseMutation): class CreateAddress(BaseMutation):
class Arguments: 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) address = Field(AddressType)
@ -632,7 +675,7 @@ class Search(BaseMutation):
@staticmethod @staticmethod
def mutate(_parent, info, query): def mutate(_parent, info, query):
data = process_query(query) data = process_query(query=query, request=info.context)
return Search( return Search(
results=SearchResultsType( results=SearchResultsType(

View file

@ -2,7 +2,18 @@ from django.core.cache import cache
from django.db.models import Max, Min from django.db.models import Max, Min
from django.db.models.functions import Length from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _ 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.types.generic import GenericScalar
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
@ -90,10 +101,16 @@ class BrandType(DjangoObjectType):
return self.categories.filter(is_active=True) return self.categories.filter(is_active=True)
def resolve_big_logo(self: Brand, info): 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): 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): class FilterableAttributeType(ObjectType):
@ -115,14 +132,22 @@ class CategoryType(DjangoObjectType):
markup_percent = Float(required=False, description=_("markup percentage")) markup_percent = Float(required=False, description=_("markup percentage"))
filterable_attributes = List( filterable_attributes = List(
NonNull(FilterableAttributeType), 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( min_max_prices = Field(
NonNull(MinMaxPriceType), 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: class Meta:
model = Category model = Category
@ -198,9 +223,14 @@ class CategoryType(DjangoObjectType):
) )
min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0) min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0)
min_max_prices["max_price"] = price_aggregation.get("max_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): class VendorType(DjangoObjectType):
@ -245,7 +275,9 @@ class AddressType(DjangoObjectType):
class FeedbackType(DjangoObjectType): class FeedbackType(DjangoObjectType):
comment = String(description=_("comment")) 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: class Meta:
model = Feedback model = Feedback
@ -258,7 +290,9 @@ class FeedbackType(DjangoObjectType):
class OrderProductType(DjangoObjectType): class OrderProductType(DjangoObjectType):
attributes = GenericScalar(description=_("attributes")) attributes = GenericScalar(description=_("attributes"))
notifications = GenericScalar(description=_("notifications")) 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: class Meta:
model = OrderProduct model = OrderProduct
@ -292,7 +326,9 @@ class OrderType(DjangoObjectType):
billing_address = Field(AddressType, description=_("billing address")) billing_address = Field(AddressType, description=_("billing address"))
shipping_address = Field( shipping_address = Field(
AddressType, 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_price = Float(description=_("total price of this order"))
total_quantity = Int(description=_("total quantity of products in order")) total_quantity = Int(description=_("total quantity of products in order"))
@ -350,7 +386,9 @@ class ProductType(DjangoObjectType):
images = DjangoFilterConnectionField(ProductImageType, description=_("images")) images = DjangoFilterConnectionField(ProductImageType, description=_("images"))
feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks")) feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks"))
brand = Field(BrandType, description=_("brand")) brand = Field(BrandType, description=_("brand"))
attribute_groups = DjangoFilterConnectionField(AttributeGroupType, description=_("attribute groups")) attribute_groups = DjangoFilterConnectionField(
AttributeGroupType, description=_("attribute groups")
)
price = Float(description=_("price")) price = Float(description=_("price"))
quantity = Float(description=_("quantity")) quantity = Float(description=_("quantity"))
feedbacks_count = Int(description=_("number of feedbacks")) feedbacks_count = Int(description=_("number of feedbacks"))
@ -387,7 +425,9 @@ class ProductType(DjangoObjectType):
def resolve_attribute_groups(self: Product, info): def resolve_attribute_groups(self: Product, info):
info.context._product_uuid = self.uuid 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: def resolve_quantity(self, _info) -> int:
return self.quantity or 0 return self.quantity or 0
@ -422,14 +462,20 @@ class PromoCodeType(DjangoObjectType):
description = _("promocodes") description = _("promocodes")
def resolve_discount(self: PromoCode, _info) -> float: 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: def resolve_discount_type(self: PromoCode, _info) -> str:
return "percent" if self.discount_percent else "amount" return "percent" if self.discount_percent else "amount"
class PromotionType(DjangoObjectType): class PromotionType(DjangoObjectType):
products = DjangoFilterConnectionField(ProductType, description=_("products on sale")) products = DjangoFilterConnectionField(
ProductType, description=_("products on sale")
)
class Meta: class Meta:
model = Promotion model = Promotion
@ -452,7 +498,9 @@ class StockType(DjangoObjectType):
class WishlistType(DjangoObjectType): class WishlistType(DjangoObjectType):
products = DjangoFilterConnectionField(ProductType, description=_("wishlisted products")) products = DjangoFilterConnectionField(
ProductType, description=_("wishlisted products")
)
class Meta: class Meta:
model = Wishlist model = Wishlist
@ -462,7 +510,9 @@ class WishlistType(DjangoObjectType):
class ProductTagType(DjangoObjectType): class ProductTagType(DjangoObjectType):
product_set = DjangoFilterConnectionField(ProductType, description=_("tagged products")) product_set = DjangoFilterConnectionField(
ProductType, description=_("tagged products")
)
class Meta: class Meta:
model = ProductTag model = ProductTag
@ -473,7 +523,9 @@ class ProductTagType(DjangoObjectType):
class CategoryTagType(DjangoObjectType): class CategoryTagType(DjangoObjectType):
category_set = DjangoFilterConnectionField(CategoryType, description=_("tagged categories")) category_set = DjangoFilterConnectionField(
CategoryType, description=_("tagged categories")
)
class Meta: class Meta:
model = CategoryTag model = CategoryTag
@ -489,7 +541,11 @@ class ConfigType(ObjectType):
company_name = String(description=_("company name")) company_name = String(description=_("company name"))
company_address = String(description=_("company address")) company_address = String(description=_("company address"))
company_phone_number = String(description=_("company phone number")) 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")) email_host_user = String(description=_("email host user"))
payment_gateway_maximum = Float(description=_("maximum amount for payment")) payment_gateway_maximum = Float(description=_("maximum amount for payment"))
payment_gateway_minimum = Float(description=_("minimum amount for payment")) payment_gateway_minimum = Float(description=_("minimum amount for payment"))
@ -513,18 +569,21 @@ class SearchProductsResultsType(ObjectType):
uuid = UUID() uuid = UUID()
name = String() name = String()
slug = String() slug = String()
image = String()
class SearchCategoriesResultsType(ObjectType): class SearchCategoriesResultsType(ObjectType):
uuid = UUID() uuid = UUID()
name = String() name = String()
slug = String() slug = String()
image = String()
class SearchBrandsResultsType(ObjectType): class SearchBrandsResultsType(ObjectType):
uuid = UUID() uuid = UUID()
name = String() name = String()
slug = String() slug = String()
image = String()
class SearchPostsResultsType(ObjectType): class SearchPostsResultsType(ObjectType):
@ -534,9 +593,15 @@ class SearchPostsResultsType(ObjectType):
class SearchResultsType(ObjectType): class SearchResultsType(ObjectType):
products = List(description=_("products search results"), of_type=SearchProductsResultsType) products = List(
categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType) description=_("products search results"), of_type=SearchProductsResultsType
brands = List(description=_("products search results"), of_type=SearchBrandsResultsType) )
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) posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)

View file

@ -15,8 +15,10 @@ class Command(BaseCommand):
) )
for idx, instance in enumerate(queryset.iterator(), start=1): for idx, instance in enumerate(queryset.iterator(), start=1):
try: try:
if ( while (
queryset.filter(name=instance.name).exclude(pk=instance.pk).count() queryset.filter(name=instance.name)
.exclude(uuid=instance.uuid)
.count()
>= 1 >= 1
): ):
instance.name = f"{instance.name} - {get_random_string(length=3, allowed_chars='0123456789')}" instance.name = f"{instance.name} - {get_random_string(length=3, allowed_chars='0123456789')}"
@ -34,7 +36,8 @@ class Command(BaseCommand):
except Exception as e: except Exception as e:
self.stderr.write( self.stderr.write(
self.style.ERROR( 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}"
) )
) )