From 68368fc531ca33e2e742ca1fe6f664b2cdd74eec Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Thu, 4 Dec 2025 13:37:55 +0330 Subject: [PATCH] unit category added --- backend/core/settings/unfold_conf.py | 6 +++ backend/product/admin.py | 6 +++ .../0058_unitcategorymodel_and_more.py | 44 +++++++++++++++++++ .../0059_alter_maincategorymodel_parent.py | 19 ++++++++ backend/product/models.py | 37 +++++++++++++++- backend/product/serializers.py | 7 +++ backend/product/urls.py | 3 +- backend/product/views.py | 42 ++++++++++++++++++ 8 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 backend/product/migrations/0058_unitcategorymodel_and_more.py create mode 100644 backend/product/migrations/0059_alter_maincategorymodel_parent.py diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index eb8f419..92320d7 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -182,6 +182,12 @@ UNFOLD = { "collapsible": True, "items": [ + { + "title": _("دسته بندی واحد"), + "icon": "category", + "link": reverse_lazy("admin:product_unitcategorymodel_changelist"), + "permission": lambda request: request.user.is_superuser, + }, { "title": _("دسته بندی"), "icon": "category", diff --git a/backend/product/admin.py b/backend/product/admin.py index 7a8ab0c..4eb077f 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -30,6 +30,12 @@ class ProductDetailCategoryAdmin(ModelAdmin, ImportExportModelAdmin): return request.user.is_superuser +@admin.register(UnitCategoryModel) +class UnitCategoryAdmin(ModelAdmin): + pass + + + @admin.register(InPackItems) class InPackItemsAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm diff --git a/backend/product/migrations/0058_unitcategorymodel_and_more.py b/backend/product/migrations/0058_unitcategorymodel_and_more.py new file mode 100644 index 0000000..6e268ab --- /dev/null +++ b/backend/product/migrations/0058_unitcategorymodel_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.1.2 on 2025-12-04 09:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0057_productvariant_profit_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='UnitCategoryModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='نام دسته بندی')), + ('slug', models.SlugField(help_text='اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید', unique=True)), + ('icon', models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='آیکون')), + ('image', models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='عکس')), + ('meta_title', models.CharField(blank=True, help_text='عنوان متا برای SEO', max_length=60, null=True, verbose_name='عنوان متا')), + ('meta_description', models.TextField(blank=True, help_text='توضیحات متا برای SEO', max_length=160, null=True, verbose_name='توضیحات متا')), + ], + options={ + 'verbose_name': 'دسته\u200cبندی اصلی', + 'verbose_name_plural': 'دسته\u200cبندی\u200cهااصلی', + }, + ), + migrations.RenameIndex( + model_name='maincategorymodel', + new_name='unit_category_slug_idx', + old_name='main_category_slug_idx', + ), + migrations.AddIndex( + model_name='unitcategorymodel', + index=models.Index(fields=['slug'], name='main_category_slug_idx'), + ), + migrations.AddField( + model_name='maincategorymodel', + name='parent', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='unitcategorys', to='product.unitcategorymodel', verbose_name='دسته\u200cبندی والد'), + ), + ] diff --git a/backend/product/migrations/0059_alter_maincategorymodel_parent.py b/backend/product/migrations/0059_alter_maincategorymodel_parent.py new file mode 100644 index 0000000..551f7f5 --- /dev/null +++ b/backend/product/migrations/0059_alter_maincategorymodel_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-12-04 09:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0058_unitcategorymodel_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='maincategorymodel', + name='parent', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='maincategorys', to='product.unitcategorymodel', verbose_name='دسته\u200cبندی والد'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index cc994e2..b31c113 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -8,6 +8,38 @@ from django.core.exceptions import ValidationError from home.models import ShowCaseSlider + +class UnitCategoryModel(models.Model): + name = models.CharField(max_length=50, verbose_name='نام دسته بندی') + slug = models.SlugField(max_length=50, unique=True, + help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") + icon = models.ImageField(upload_to='category_model/', + verbose_name='آیکون', blank=True, null=True) + image = models.ImageField( + upload_to='category_model/', verbose_name='عکس', blank=True, null=True) + meta_title = models.CharField( + max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True) + meta_description = models.TextField( + max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True) + + class Meta: + verbose_name = "دسته‌بندی اصلی" + verbose_name_plural = "دسته‌بندی‌هااصلی" + indexes = [ + models.Index(fields=['slug'], name='main_category_slug_idx'), + ] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = 'unit-category-' + slugify(self.name, allow_unicode=True) + super().save(*args, **kwargs) + + + + class MainCategoryModel(models.Model): name = models.CharField(max_length=50, verbose_name='نام دسته بندی') slug = models.SlugField(max_length=50, unique=True, @@ -22,12 +54,13 @@ class MainCategoryModel(models.Model): max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True) video = models.FileField(upload_to='category_videos/', blank=True, null=True, verbose_name='ویدیو') - + parent = models.ForeignKey(UnitCategoryModel, on_delete=models.CASCADE, + related_name='maincategorys', verbose_name='دسته‌بندی والد', null=True) class Meta: verbose_name = "دسته‌بندی اصلی" verbose_name_plural = "دسته‌بندی‌هااصلی" indexes = [ - models.Index(fields=['slug'], name='main_category_slug_idx'), + models.Index(fields=['slug'], name='unit_category_slug_idx'), ] def __str__(self): diff --git a/backend/product/serializers.py b/backend/product/serializers.py index ec6d695..295a219 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -110,6 +110,13 @@ class SubCategorySerializer(serializers.ModelSerializer): return obj.parent.name +class UnitCategorySerializer(serializers.ModelSerializer): + class Meta: + model = UnitCategoryModel + fields = ['id', 'name', 'slug', 'icon', 'meta_title', + 'meta_description', 'image'] + + class MainCategorySerializer(serializers.ModelSerializer): subcategorys = SubCategorySerializer(many=True) diff --git a/backend/product/urls.py b/backend/product/urls.py index 891a251..5b0a275 100644 --- a/backend/product/urls.py +++ b/backend/product/urls.py @@ -1,11 +1,12 @@ from django.urls import path, re_path -from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView, ShowCaseCategoryListView, BotProductsView,BotProductDetailView,BotCategoryView +from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView, ShowCaseCategoryListView, BotProductsView,BotProductDetailView,BotCategoryView ,AllCategoriesV2 urlpatterns = [ path('slider_category', ShowCaseProductsView.as_view(), name='category-products'), path('bot', BotProductsView.as_view(), name='bot-products'), path('bot//', BotProductDetailView.as_view(), name='bot-product-detail'), path('categories', AllCategories.as_view(), name='all-categories'), + path('categories/v2', AllCategoriesV2.as_view(), name='all-categories'), path('categories/bot', BotCategoryView.as_view(), name='bot-categories'), path('slider_categories', ShowCaseCategoryListView.as_view(), name='all-categories'), re_path(r'^comments/(?P[\w\u0600-\u06FF\-]+)$', CommentView.as_view(), name='comment-views'), diff --git a/backend/product/views.py b/backend/product/views.py index 86dd14b..f37c20a 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -65,6 +65,48 @@ class AllCategories(APIView): categories_ser = self.serializer_class( instance=categories, many=True, context={'request': request}) return Response(categories_ser.data, status=status.HTTP_200_OK) +class UnitCategorySerializerV2(serializers.ModelSerializer): + maincategorys = serializers.SerializerMethodField() + + class Meta: + model = UnitCategoryModel + fields = ['id', 'name', 'slug', 'icon', 'meta_title', + 'meta_description', 'image', 'maincategorys'] + + def get_maincategorys(self, obj): + main_categories = obj.maincategorys.all() + return MainCategorySerializer(main_categories, many=True, context=self.context).data + + +class AllCategoriesV2(APIView): + serializer_class = UnitCategorySerializerV2 + authentication_classes = [] + + @extend_schema( + responses={ + 200: UnitCategorySerializerV2(many=True), + 404: OpenApiTypes.OBJECT, + }, + ) + def get(self, request): + # Optimize query with prefetch_related to avoid N+1 queries + unit_categories = UnitCategoryModel.objects.prefetch_related( + Prefetch( + 'maincategorys', + queryset=MainCategoryModel.objects.prefetch_related( + Prefetch( + 'subcategorys', + queryset=SubCategoryModel.objects.annotate( + product_count=Count('products') + ) + ) + ) + ) + ).all() + + categories_ser = self.serializer_class( + instance=unit_categories, many=True, context={'request': request}) + return Response(categories_ser.data, status=status.HTTP_200_OK) class ProductView(APIView):