From 87ed875fe64e35b6bc99b835ff79c465c1117598 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Sat, 21 Feb 2026 18:27:10 +0300 Subject: [PATCH] feat(category): add brands relationship and resolve method enable brands association with categories and allow querying of active brands within a category. Updated GraphQL schema, models, and serializers to include this relationship while deprecating redundant category-to-brand ManyToManyField. --- engine/core/admin.py | 5 +---- engine/core/graphene/object_types.py | 5 +++++ engine/core/models.py | 12 ++++++++++-- engine/core/serializers/detail.py | 3 +++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/engine/core/admin.py b/engine/core/admin.py index 94df948e..d6f06de4 100644 --- a/engine/core/admin.py +++ b/engine/core/admin.py @@ -421,10 +421,7 @@ class BrandAdmin( "priority", "is_active", ) - list_filter = ( - "categories", - "is_active", - ) + list_filter = ("is_active",) search_fields = ( "uuid", "name", diff --git a/engine/core/graphene/object_types.py b/engine/core/graphene/object_types.py index 44ffffc3..bc2b323a 100644 --- a/engine/core/graphene/object_types.py +++ b/engine/core/graphene/object_types.py @@ -230,6 +230,7 @@ class CategoryType(DjangoObjectType): "minimum and maximum prices for products in this category, if available." ), ) + brands = List(lambda: BrandType, description=_("brands in this category")) tags = DjangoFilterConnectionField( lambda: CategoryTagType, description=_("tags for this category") ) @@ -249,6 +250,7 @@ class CategoryType(DjangoObjectType): "slug", "description", "image", + "brands", "min_max_prices", ) filter_fields = ["uuid"] @@ -294,6 +296,9 @@ class CategoryType(DjangoObjectType): "max_price": min_max_prices["max_price"], } + def resolve_brands(self: Category, info) -> QuerySet[Brand]: + return self.brands + def resolve_seo_meta(self: Category, info): lang = graphene_current_lang() base = f"https://{settings.BASE_DOMAIN}" diff --git a/engine/core/models.py b/engine/core/models.py index 55218d23..5e0b9e3d 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -443,6 +443,14 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): # Fallback to favicon.png from static files return static("favicon.png") + @cached_property + def brands(self) -> QuerySet["Brand"]: + return Brand.objects.filter( + products__category=self, + products__is_active=True, + is_active=True, + ).distinct() + class Meta: verbose_name = _("category") verbose_name_plural = _("categories") @@ -490,8 +498,8 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel): categories = ManyToManyField( "core.Category", blank=True, - help_text=_("optional categories that this brand is associated with"), - verbose_name=_("associated categories"), + help_text=_("DEPRECATED"), + verbose_name=_("DEPRECATED"), ) slug = AutoSlugField( populate_from=("name",), diff --git a/engine/core/serializers/detail.py b/engine/core/serializers/detail.py index ee3878a0..535704ac 100644 --- a/engine/core/serializers/detail.py +++ b/engine/core/serializers/detail.py @@ -26,6 +26,7 @@ from engine.core.models import ( Wishlist, ) from engine.core.serializers.simple import ( + BrandSimpleSerializer, CategorySimpleSerializer, ProductSimpleSerializer, ) @@ -60,6 +61,7 @@ class CategoryDetailListSerializer(ListSerializer): class CategoryDetailSerializer(ModelSerializer): children = SerializerMethodField() filterable_attributes = SerializerMethodField() + brands = BrandSimpleSerializer(many=True, read_only=True) class Meta: model = Category @@ -71,6 +73,7 @@ class CategoryDetailSerializer(ModelSerializer): "image", "markup_percent", "filterable_attributes", + "brands", "children", "slug", "created",