From a02bd923c03fc577aa090540de7b3308baf2a163 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 19 Jan 2025 21:56:11 +0330 Subject: [PATCH] a lot of things about category --- backend/core/settings.py | 7 +- backend/product/admin.py | 7 +- ...categorymodel_subcategorymodel_and_more.py | 49 ++++++++++ .../0014_alter_maincategorymodel_icon.py | 18 ++++ ...15_alter_productmodel_category_and_more.py | 24 +++++ backend/product/models.py | 60 ++++-------- backend/product/serializers.py | 23 ++--- backend/product/views.py | 58 ++++++------ docker-compose.yml | 94 +++++++++++-------- 9 files changed, 216 insertions(+), 124 deletions(-) create mode 100644 backend/product/migrations/0013_maincategorymodel_subcategorymodel_and_more.py create mode 100644 backend/product/migrations/0014_alter_maincategorymodel_icon.py create mode 100644 backend/product/migrations/0015_alter_productmodel_category_and_more.py diff --git a/backend/core/settings.py b/backend/core/settings.py index 6336fd9..228b1eb 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -297,7 +297,12 @@ UNFOLD = { { "title": _("دسته بندی"), "icon": "category", - "link": reverse_lazy("admin:product_categorymodel_changelist"), + "link": reverse_lazy("admin:product_maincategorymodel_changelist"), + }, + { + "title": _("زیر دسته بندی"), + "icon": "category", + "link": reverse_lazy("admin:product_subcategorymodel_changelist"), }, { "title": _("نظرات"), diff --git a/backend/product/admin.py b/backend/product/admin.py index 814d406..b1f3efd 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -7,10 +7,13 @@ from unfold.admin import ModelAdmin class ProductModelAdmin(ModelAdmin): pass -@admin.register(CategoryModel) -class CategoryModelAdmin(ModelAdmin): +@admin.register(MainCategoryModel) +class MainCategoryModelAdmin(ModelAdmin): pass +@admin.register(SubCategoryModel) +class SubCategoryModelAdmin(ModelAdmin): + pass @admin.register(CommentModel) class CommentAdmin(ModelAdmin): diff --git a/backend/product/migrations/0013_maincategorymodel_subcategorymodel_and_more.py b/backend/product/migrations/0013_maincategorymodel_subcategorymodel_and_more.py new file mode 100644 index 0000000..cde13ce --- /dev/null +++ b/backend/product/migrations/0013_maincategorymodel_subcategorymodel_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 5.1.2 on 2025-01-19 17:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0012_productmodel_category'), + ] + + operations = [ + migrations.CreateModel( + name='MainCategoryModel', + 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.CharField(blank=True, max_length=100, null=True, 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.CreateModel( + name='SubCategoryModel', + fields=[ + ('maincategorymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='product.maincategorymodel')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='product.maincategorymodel', verbose_name='دسته\u200cبندی والد')), + ], + options={ + 'verbose_name': 'زیر دسته\u200cبندی', + 'verbose_name_plural': 'زیر دسته\u200cبندی\u200cها', + }, + bases=('product.maincategorymodel',), + ), + migrations.AlterField( + model_name='productmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='product.subcategorymodel'), + ), + migrations.DeleteModel( + name='CategoryModel', + ), + ] diff --git a/backend/product/migrations/0014_alter_maincategorymodel_icon.py b/backend/product/migrations/0014_alter_maincategorymodel_icon.py new file mode 100644 index 0000000..94bbd39 --- /dev/null +++ b/backend/product/migrations/0014_alter_maincategorymodel_icon.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-01-19 17:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0013_maincategorymodel_subcategorymodel_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='maincategorymodel', + name='icon', + field=models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='آیکون'), + ), + ] diff --git a/backend/product/migrations/0015_alter_productmodel_category_and_more.py b/backend/product/migrations/0015_alter_productmodel_category_and_more.py new file mode 100644 index 0000000..dfbef5e --- /dev/null +++ b/backend/product/migrations/0015_alter_productmodel_category_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2025-01-19 18:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0014_alter_maincategorymodel_icon'), + ] + + operations = [ + migrations.AlterField( + model_name='productmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='product.subcategorymodel'), + ), + migrations.AlterField( + model_name='subcategorymodel', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategorys', to='product.maincategorymodel', verbose_name='دسته\u200cبندی والد'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 0d0cd2f..af0f400 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -5,55 +5,29 @@ from django.urls import reverse import requests -class CategoryModel(models.Model): - name = models.CharField(max_length=50, verbose_name='نام دسته‌بندی') - slug = models.SlugField( - max_length=50, - unique=True, - help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید" - ) - parent = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - related_name='children', - null=True, - blank=True, - verbose_name='دسته‌بندی والد' - ) - icon = models.CharField(max_length=100, 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 MainCategoryModel(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) + 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 = "دسته‌بندی‌ها" - ordering = ['parent__id', 'id'] # Optional: to order by hierarchy + verbose_name = "دسته‌بندی اصلی" + verbose_name_plural = "دسته‌بندی‌هااصلی" def __str__(self): return self.name - def get_absolute_url(self): - return reverse('category_detail', kwargs={'slug': self.slug}) + # def get_absolute_url(self): + # return reverse('category_detail', kwargs={'slug': self.slug}) - def get_breadcrumb(self): - breadcrumb = [] - category = self - while category: - breadcrumb.append(category) - category = category.parent - return breadcrumb[::-1] + +class SubCategoryModel(MainCategoryModel): + parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', null=True, blank=True, verbose_name='دسته‌بندی والد') + class Meta: + verbose_name = "زیر دسته‌بندی" + verbose_name_plural = "زیر دسته‌بندی‌ها" class DollorModel(models.Model): price = models.FloatField(null=True, blank=True) @@ -113,7 +87,7 @@ class ProductModel(models.Model): meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید') meta_rating = models.FloatField(default=5, help_text='امتیاز محصول') created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') - category = models.ForeignKey(CategoryModel, blank=True, null=True, on_delete=models.SET_NULL) + category = models.ForeignKey(SubCategoryModel, blank=True, null=True, on_delete=models.SET_NULL, related_name='products') def format_discount_price(self): discount_price = int(self.price * (100 - self.discount) / 100) formatted_num = "{:,.0f}".format(discount_price) diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 277e28b..982506e 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -36,16 +36,17 @@ class CommentSerializer(serializers.ModelSerializer): fields = "__all__" read_only_fields = ('show', 'product') - -class CategorySerializer(serializers.ModelSerializer): - children = serializers.SerializerMethodField() - +class SubCategorySerializer(serializers.ModelSerializer): + product_count = serializers.SerializerMethodField() class Meta: - model = CategoryModel - fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'parent', 'children'] + model = SubCategoryModel + fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count'] + def get_product_count(self, obj): + return obj.products.count() - def get_children(self, obj): - children = obj.children.all() - if children.exists(): - return CategorySerializer(children, many=True).data - return [] \ No newline at end of file + +class MainCategorySerializer(serializers.ModelSerializer): + subcategorys = SubCategorySerializer(many=True) + class Meta: + model = MainCategoryModel + fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys'] diff --git a/backend/product/views.py b/backend/product/views.py index 563e442..aca61be 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -23,30 +23,30 @@ from rest_framework.permissions import AllowAny class AllCategories(APIView): - serializer_class = CategorySerializer + serializer_class = MainCategorySerializer authentication_classes = [] @extend_schema( - parameters=[ - OpenApiParameter( - name="search", - description="Search by category name or description.", - required=False, - type=OpenApiTypes.STR, - ) - ], + # parameters=[ + # OpenApiParameter( + # name="search", + # description="Search by category name or description.", + # required=False, + # type=OpenApiTypes.STR, + # ) + # ], responses={ - 200: CategorySerializer(many=True), + 200: MainCategorySerializer(many=True), 404: OpenApiTypes.OBJECT, }, ) def get(self, request): - search_query = request.query_params.get('search', None) - if search_query: - categories = CategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query)) - else: - categories = CategoryModel.objects.all() + # search_query = request.query_params.get('search', None) + # if search_query: + # categories = MainCategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query)) + # else: + categories = MainCategoryModel.objects.all() categories_ser = self.serializer_class(instance=categories, many=True) - return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK) + return Response(categories_ser.data, status=status.HTTP_200_OK) class ProductView(APIView): serializer_class = ProductSerializer @@ -71,13 +71,18 @@ class AllProductsView(APIView): required=False, type=OpenApiTypes.STR, ), + # OpenApiParameter( + # name="category", + # type={'type': 'array', 'items': {'type': 'number'}}, + # location=OpenApiParameter.QUERY, + # required=False, + # style='form', + # explode=False, + # ), OpenApiParameter( name="category", - type={'type': 'array', 'items': {'type': 'number'}}, - location=OpenApiParameter.QUERY, + type=OpenApiTypes.INT, required=False, - style='form', - explode=False, ), OpenApiParameter( name="price_gte", @@ -137,13 +142,10 @@ class AllProductsView(APIView): ) def get(self, request): try: - # Get list of category IDs from query parameters - category_ids = request.query_params.getlist('category', []) - if category_ids: - # Convert category IDs to integers and filter products by these categories - category_ids = [int(id) for id in category_ids] - - products = ProductModel.objects.filter(category__id__in=category_ids) + category_id = int(request.query_params.get('category', None)) + if category_id: + sub_category = get_object_or_404(SubCategoryModel, pk=category_id) + products = ProductModel.objects.filter(category=sub_category) else: products = ProductModel.objects.all() @@ -183,7 +185,7 @@ class AllProductsView(APIView): serializer = self.serializer_class(paginated_products, many=True) return paginator.get_paginated_response(serializer.data) - except CategoryModel.DoesNotExist: + except MainCategoryModel.DoesNotExist: return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) diff --git a/docker-compose.yml b/docker-compose.yml index 464040f..e7a09b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,47 +1,63 @@ services: - frontend: - build: - context: ./frontend - ports: - - "80:3000" - depends_on: - - django - networks: - - default - - django: - build: - context: ./backend - ports: - - "8001:8000" - depends_on: - - db - volumes: - - ./backend:/app - - media_data:/app/media - command: ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"] - networks: - - default - - db: - image: postgres:16 - environment: - POSTGRES_DB: hshop - POSTGRES_USER: byeto - POSTGRES_PASSWORD: vuhbyq-cypMu0-sirbon - volumes: - - postgres_data:/var/lib/postgresql/data - ports: - - "5434:5432" - networks: - - default + frontend: + build: + context: ./frontend + ports: + - "80:3000" + depends_on: + - django + networks: + - default + django: + build: + context: ./backend + ports: + - "8001:8000" + depends_on: + - db + volumes: + - ./backend:/app + - media_data:/app/media + command: + [ + "sh", + "-c", + "python manage.py migrate && python manage.py runserver 0.0.0.0:8000", + ] + networks: + - default + db: + image: postgres:16 + environment: + POSTGRES_DB: hshop + POSTGRES_USER: byeto + POSTGRES_PASSWORD: vuhbyq-cypMu0-sirbon + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5434:5432" + networks: + - default + # nginx: + # image: nginx:latest + # volumes: + # - ./nginx.conf:/etc/nginx/nginx.conf + # - /etc/letsencrypt:/etc/letsencrypt + # ports: + # - "80:80" + # - "443:443" + # depends_on: + # - django + # - frontend + # networks: + # - default volumes: - postgres_data: - media_data: + postgres_data: + media_data: networks: - default: \ No newline at end of file + default: \ No newline at end of file