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.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-02-21 18:27:10 +03:00
parent 10f5c798d4
commit 87ed875fe6
4 changed files with 19 additions and 6 deletions

View file

@ -421,10 +421,7 @@ class BrandAdmin(
"priority", "priority",
"is_active", "is_active",
) )
list_filter = ( list_filter = ("is_active",)
"categories",
"is_active",
)
search_fields = ( search_fields = (
"uuid", "uuid",
"name", "name",

View file

@ -230,6 +230,7 @@ class CategoryType(DjangoObjectType):
"minimum and maximum prices for products in this category, if available." "minimum and maximum prices for products in this category, if available."
), ),
) )
brands = List(lambda: BrandType, description=_("brands in this category"))
tags = DjangoFilterConnectionField( tags = DjangoFilterConnectionField(
lambda: CategoryTagType, description=_("tags for this category") lambda: CategoryTagType, description=_("tags for this category")
) )
@ -249,6 +250,7 @@ class CategoryType(DjangoObjectType):
"slug", "slug",
"description", "description",
"image", "image",
"brands",
"min_max_prices", "min_max_prices",
) )
filter_fields = ["uuid"] filter_fields = ["uuid"]
@ -294,6 +296,9 @@ class CategoryType(DjangoObjectType):
"max_price": min_max_prices["max_price"], "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): def resolve_seo_meta(self: Category, info):
lang = graphene_current_lang() lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}" base = f"https://{settings.BASE_DOMAIN}"

View file

@ -443,6 +443,14 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
# Fallback to favicon.png from static files # Fallback to favicon.png from static files
return static("favicon.png") 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: class Meta:
verbose_name = _("category") verbose_name = _("category")
verbose_name_plural = _("categories") verbose_name_plural = _("categories")
@ -490,8 +498,8 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel):
categories = ManyToManyField( categories = ManyToManyField(
"core.Category", "core.Category",
blank=True, blank=True,
help_text=_("optional categories that this brand is associated with"), help_text=_("DEPRECATED"),
verbose_name=_("associated categories"), verbose_name=_("DEPRECATED"),
) )
slug = AutoSlugField( slug = AutoSlugField(
populate_from=("name",), populate_from=("name",),

View file

@ -26,6 +26,7 @@ from engine.core.models import (
Wishlist, Wishlist,
) )
from engine.core.serializers.simple import ( from engine.core.serializers.simple import (
BrandSimpleSerializer,
CategorySimpleSerializer, CategorySimpleSerializer,
ProductSimpleSerializer, ProductSimpleSerializer,
) )
@ -60,6 +61,7 @@ class CategoryDetailListSerializer(ListSerializer):
class CategoryDetailSerializer(ModelSerializer): class CategoryDetailSerializer(ModelSerializer):
children = SerializerMethodField() children = SerializerMethodField()
filterable_attributes = SerializerMethodField() filterable_attributes = SerializerMethodField()
brands = BrandSimpleSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Category model = Category
@ -71,6 +73,7 @@ class CategoryDetailSerializer(ModelSerializer):
"image", "image",
"markup_percent", "markup_percent",
"filterable_attributes", "filterable_attributes",
"brands",
"children", "children",
"slug", "slug",
"created", "created",