From 2f1236a83dc5ddb23779dcd61f40142674a0d9bc Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 19 May 2025 20:41:54 +0330 Subject: [PATCH 1/8] new route --- backend/core/settings/unfold_conf.py | 15 +- ...ermodel_options_remove_slidermodel_link.py | 21 +++ ...er_slidermodel_options_slidermodel_link.py | 23 +++ .../0017_remove_showcaseslider_link.py | 17 +++ backend/home/models.py | 2 - backend/product/admin.py | 2 +- .../0041_alter_detailmodel_detail_model.py | 19 +++ .../0042_productvariant_porsant_category.py | 20 +++ ...category_productvariant_slider_category.py | 18 +++ ...44_alter_productvariant_slider_category.py | 20 +++ backend/product/models.py | 3 +- backend/product/serializers.py | 7 +- backend/product/urls.py | 3 +- backend/product/views.py | 138 ++++++++++++++++++ 14 files changed, 294 insertions(+), 14 deletions(-) create mode 100644 backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py create mode 100644 backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py create mode 100644 backend/home/migrations/0017_remove_showcaseslider_link.py create mode 100644 backend/product/migrations/0041_alter_detailmodel_detail_model.py create mode 100644 backend/product/migrations/0042_productvariant_porsant_category.py create mode 100644 backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py create mode 100644 backend/product/migrations/0044_alter_productvariant_slider_category.py diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index a143954..0055b12 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -160,8 +160,12 @@ UNFOLD = { "title": _("زیر دسته بندی"), "icon": "category", "link": reverse_lazy("admin:product_subcategorymodel_changelist"), - } - + }, + { + "title": _("دسته بندی پورسانتی"), + "icon": "devices", + "link": reverse_lazy("admin:home_showcaseslider_changelist"), + }, ], }, { @@ -186,12 +190,7 @@ UNFOLD = { "icon": "newsmode", "link": reverse_lazy("admin:blog_blogmodel_changelist"), }, - - { - "title": _("نمایش کیس ها"), - "icon": "devices", - "link": reverse_lazy("admin:home_showcaseslider_changelist"), - }, + ], }, diff --git a/backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py b/backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py new file mode 100644 index 0000000..e89c7af --- /dev/null +++ b/backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2025-05-18 10:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0014_homeimagemodel_video1_homeimagemodel_video2_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='slidermodel', + options={'verbose_name': 'دسته بندی پورسانت', 'verbose_name_plural': 'دسته بندی پورسانت'}, + ), + migrations.RemoveField( + model_name='slidermodel', + name='link', + ), + ] diff --git a/backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py b/backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py new file mode 100644 index 0000000..db6124c --- /dev/null +++ b/backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2025-05-18 11:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0015_alter_slidermodel_options_remove_slidermodel_link'), + ] + + operations = [ + migrations.AlterModelOptions( + name='slidermodel', + options={'verbose_name': 'اسلایدر', 'verbose_name_plural': 'اسلایدر ها'}, + ), + migrations.AddField( + model_name='slidermodel', + name='link', + field=models.URLField(default='', verbose_name='لینک'), + preserve_default=False, + ), + ] diff --git a/backend/home/migrations/0017_remove_showcaseslider_link.py b/backend/home/migrations/0017_remove_showcaseslider_link.py new file mode 100644 index 0000000..ae6a399 --- /dev/null +++ b/backend/home/migrations/0017_remove_showcaseslider_link.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-05-18 11:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0016_alter_slidermodel_options_slidermodel_link'), + ] + + operations = [ + migrations.RemoveField( + model_name='showcaseslider', + name='link', + ), + ] diff --git a/backend/home/models.py b/backend/home/models.py index d59f71a..d90318d 100644 --- a/backend/home/models.py +++ b/backend/home/models.py @@ -1,5 +1,4 @@ from django.db import models -from product.models import ProductModel from django.urls import reverse class SliderModel(models.Model): @@ -42,7 +41,6 @@ class HomeImageModel(models.Model): class ShowCaseSlider(models.Model): title = models.CharField(max_length=30, verbose_name='عنوان') description = models.CharField(max_length=150, verbose_name='توضیحات') - link = models.URLField(verbose_name='لینک') image = models.ImageField(upload_to='show_case/', verbose_name='عکس') def __str__(self): return self.title diff --git a/backend/product/admin.py b/backend/product/admin.py index 1f1b709..20c58cd 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -153,7 +153,7 @@ class ProductVariantInLine(StackedInline): readonly_fields = ['price'] # inlines = [DetailModelInLine] autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details'] - fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details','sell'] + fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category'] # search_fields = [''] diff --git a/backend/product/migrations/0041_alter_detailmodel_detail_model.py b/backend/product/migrations/0041_alter_detailmodel_detail_model.py new file mode 100644 index 0000000..4a71a7b --- /dev/null +++ b/backend/product/migrations/0041_alter_detailmodel_detail_model.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-04-24 22:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0040_remove_detailmodel_name'), + ] + + operations = [ + migrations.AlterField( + model_name='detailmodel', + name='detail_model', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='product.productdetailmodel', verbose_name='دسته بندی جزيات'), + ), + ] diff --git a/backend/product/migrations/0042_productvariant_porsant_category.py b/backend/product/migrations/0042_productvariant_porsant_category.py new file mode 100644 index 0000000..7643dd7 --- /dev/null +++ b/backend/product/migrations/0042_productvariant_porsant_category.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-05-18 10:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0015_alter_slidermodel_options_remove_slidermodel_link'), + ('product', '0041_alter_detailmodel_detail_model'), + ] + + operations = [ + migrations.AddField( + model_name='productvariant', + name='porsant_category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.slidermodel', verbose_name='دسته بندی پورسانتی'), + ), + ] diff --git a/backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py b/backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py new file mode 100644 index 0000000..a67d07a --- /dev/null +++ b/backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-05-18 11:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0042_productvariant_porsant_category'), + ] + + operations = [ + migrations.RenameField( + model_name='productvariant', + old_name='porsant_category', + new_name='slider_category', + ), + ] diff --git a/backend/product/migrations/0044_alter_productvariant_slider_category.py b/backend/product/migrations/0044_alter_productvariant_slider_category.py new file mode 100644 index 0000000..5bf8022 --- /dev/null +++ b/backend/product/migrations/0044_alter_productvariant_slider_category.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-05-18 11:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0017_remove_showcaseslider_link'), + ('product', '0043_rename_porsant_category_productvariant_slider_category'), + ] + + operations = [ + migrations.AlterField( + model_name='productvariant', + name='slider_category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.showcaseslider', verbose_name='دسته بندی پورسانتی'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 7d8c160..310a147 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -5,7 +5,7 @@ from django.urls import reverse import requests from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError - +from home.models import ShowCaseSlider class MainCategoryModel(models.Model): name = models.CharField(max_length=50, verbose_name='نام دسته بندی') slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") @@ -243,6 +243,7 @@ class ProductVariant(models.Model): images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها') video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو') details = models.ManyToManyField(ProductDetailModel, verbose_name='جزییات محصول', related_name='product') + slider_category = models.ForeignKey(ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE) class Meta: verbose_name = 'تنوع محصول' verbose_name_plural = 'تنوع‌های محصول' diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 4d03b07..4cf3178 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -130,12 +130,17 @@ class DynamicProductSerializer(serializers.ModelSerializer): fields = "__all__" view_type = { 'list': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors'], + 'slider': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors'], 'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors'], 'chat': ['id', 'name', 'description', 'variants'] } def get_variants(self, obj): - varients = obj.variants.all() + view_type = self.context.get('view_type') + if view_type == 'slider': + varients = obj.variants.filter(slider_category__isnull=False) + else: + varients = obj.variants.all() colors = set(varient.color for varient in varients) return ProductVariantSerialzier(instance=varients, many=True, context=self.context).data diff --git a/backend/product/urls.py b/backend/product/urls.py index faad234..b5c8fd5 100644 --- a/backend/product/urls.py +++ b/backend/product/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import AllCategories, ProductView, AllProductsView, CommentView +from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView urlpatterns = [ path('', AllProductsView.as_view(), name='category-products'), + path('slider_category', ShowCaseProductsView.as_view(), name='category-products'), path('categories', AllCategories.as_view(), name='all-categories'), path('', ProductView.as_view(), name='product-detail'), path('comments/', CommentView.as_view(), name='comment-views'), diff --git a/backend/product/views.py b/backend/product/views.py index 797b97e..c8e978c 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -13,6 +13,7 @@ from rest_framework.permissions import AllowAny from order.serializers import OrderItemSerailzier from order.models import OrderModel from django.db.models import Min, Max +from home.models import ShowCaseSlider # class APIView(APIView): # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) @@ -205,6 +206,143 @@ class AllProductsView(APIView): return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) + +class ShowCaseProductsView(APIView): + serializer_class = DynamicProductSerializer + pagination_class = StructurePagination + authentication_classes = [] + @extend_schema( + parameters=[ + OpenApiParameter( + name="search", + description="Search by product name or description.", + required=False, + type=OpenApiTypes.STR, + ), + OpenApiParameter( + name="slider_category", + type=OpenApiTypes.INT, + required=False, + ), + OpenApiParameter( + name="price_gte", + description="Filter products with price greater than or equal to this value.", + required=False, + type=OpenApiTypes.FLOAT, + ), + OpenApiParameter( + name="price_lte", + description="Filter products with price less than or equal to this value.", + required=False, + type=OpenApiTypes.FLOAT, + ), + OpenApiParameter( + name="sort", + description=( + "Sort results by one of the following fields:\n" + "`name`, `-name`, `price`, `-price`, `created_at`, `-created_at`." + "\nPrefix with `-` for descending order." + "remove the price form sorting templory " + ), + required=False, + type=OpenApiTypes.STR, + ), + OpenApiParameter( + name="limit", + description="Number of results to return per page (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="offset", + description="The starting position of the results (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="in_stock", + description="Filter products that are in stock (positive stock).", + required=False, + type=OpenApiTypes.BOOL, + ), + OpenApiParameter( + name="has_discount", + description="Filter products that have a discount.", + required=False, + type=OpenApiTypes.BOOL, + ) + ], + description=( + "Retrieve products with optional filters and sorting. " + "Provide a list of category IDs to filter products by those categories and their subcategories." + ), + responses={ + 200: DynamicProductSerializer(many=True, context={'view_type': 'list'}), + 404: OpenApiTypes.OBJECT, + }, + ) + def get(self, request): + try: + category_id = request.query_params.get('slider_category', None) + if category_id: + try: + category_id = int(category_id) + except ValueError: + return Response({'detail': 'value error category id should be a number'}, status=status.HTTP_400_BAD_REQUEST) + + slider_category = get_object_or_404(ShowCaseSlider, pk=category_id) + + products = ProductModel.objects.filter(variants__slider_category=slider_category).distinct() + else: + products = ProductModel.objects.filter(variants__slider_category__isnull=False).distinct() + + # Filter by stock status if `in_stock` is specified + in_stock = request.query_params.get('in_stock', "false") == 'true' + if in_stock: + products = products.filter(variants__in_stock__gt=0) + + # Filter by discount if `has_discount` is specified + has_discount = request.query_params.get('has_discount', "false") == 'true' + if has_discount: + products = products.filter(variants__discount__gt=0) + + # Search filter + search_query = request.query_params.get('search', None) + if search_query: + products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query)) + + # Price filters + price_gte = request.query_params.get('price_gte', None) + price_lte = request.query_params.get('price_lte', None) + + products = products.annotate(min_price=Min('variants__price'), max_price=Max('variants__price')) + + if price_gte: + products = products.filter(max_price__gte=price_gte) + if price_lte: + products = products.filter(min_price__lte=price_lte) + # Sorting + sort_by = request.query_params.get('sort', None) + if sort_by in ['name', '-name', 'created_at', '-created_at']: + products = products.order_by(sort_by) + else: + products = products.order_by('name') + + # Pagination + paginator = self.pagination_class() + paginated_products = paginator.paginate_queryset(products, request) + serializer = self.serializer_class(paginated_products, many=True, context={'request': request, 'view_type': 'slider'}) + return paginator.get_paginated_response(serializer.data) + + except MainCategoryModel.DoesNotExist: + return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) + + + + + + + class CommentView(APIView): serializer_class = CommentSerializer permission_classes = [IsAuthenticatedOrReadOnly] From 0b55ae8b2aa67f62d23ccb4063026bcb8f6b93e7 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 19 May 2025 21:37:47 +0330 Subject: [PATCH 2/8] categorys slider view --- backend/product/urls.py | 3 ++- backend/product/views.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/product/urls.py b/backend/product/urls.py index b5c8fd5..59cc686 100644 --- a/backend/product/urls.py +++ b/backend/product/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView +from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView, ShowCaseCategoryListView urlpatterns = [ path('', AllProductsView.as_view(), name='category-products'), path('slider_category', ShowCaseProductsView.as_view(), name='category-products'), path('categories', AllCategories.as_view(), name='all-categories'), + path('slider_categories', ShowCaseCategoryListView.as_view(), name='all-categories'), path('', ProductView.as_view(), name='product-detail'), path('comments/', CommentView.as_view(), name='comment-views'), ] \ No newline at end of file diff --git a/backend/product/views.py b/backend/product/views.py index c8e978c..a1d873a 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -14,6 +14,7 @@ from order.serializers import OrderItemSerailzier from order.models import OrderModel from django.db.models import Min, Max from home.models import ShowCaseSlider +from home.serializers import ShowCaseSliderSerialzier # class APIView(APIView): # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) @@ -206,6 +207,16 @@ class AllProductsView(APIView): return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) +class ShowCaseCategoryListView(APIView): + serializer_class = ShowCaseSliderSerialzier + permission_classes = [AllowAny] + def get(self, request): + categoryes = ShowCaseSlider.objects.all() + categoryes_ser = self.serializer_class(instance=categoryes, many=True, context={'request': request}) + return Response(categoryes_ser.data, status=status.HTTP_200_OK) + + + class ShowCaseProductsView(APIView): serializer_class = DynamicProductSerializer From 236582a20a440a8ab59979962cfca8e40f916d80 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 21 May 2025 14:08:50 +0330 Subject: [PATCH 3/8] category update --- backend/product/admin.py | 4 +-- ...egorymodel_show_maincategorymodel_image.py | 22 ++++++++++++ backend/product/models.py | 2 +- backend/product/serializers.py | 4 +-- backend/product/views.py | 36 ++++++++++++++----- 5 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 backend/product/migrations/0045_remove_subcategorymodel_show_maincategorymodel_image.py diff --git a/backend/product/admin.py b/backend/product/admin.py index 20c58cd..9fe5c5f 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -260,10 +260,10 @@ class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): @admin.register(SubCategoryModel) class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): - list_display = ['name', 'parent', 'show'] + list_display = ['name', 'parent'] search_fields = ['name', 'slug'] - list_filter = ['parent', 'show', ] + list_filter = ['parent', ] import_form_class = ImportForm export_form_class = ExportForm diff --git a/backend/product/migrations/0045_remove_subcategorymodel_show_maincategorymodel_image.py b/backend/product/migrations/0045_remove_subcategorymodel_show_maincategorymodel_image.py new file mode 100644 index 0000000..4dd979a --- /dev/null +++ b/backend/product/migrations/0045_remove_subcategorymodel_show_maincategorymodel_image.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2025-05-21 10:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0044_alter_productvariant_slider_category'), + ] + + operations = [ + migrations.RemoveField( + model_name='subcategorymodel', + name='show', + ), + migrations.AddField( + model_name='maincategorymodel', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='عکس'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 310a147..f7aacc8 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -10,6 +10,7 @@ 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) + 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) @@ -34,7 +35,6 @@ class SubCategoryModel(models.Model): 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) parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', verbose_name='دسته‌بندی والد') - show = models.BooleanField(default=False, verbose_name='نمایش در خانه') class Meta: verbose_name = "زیر دسته‌بندی" diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 4cf3178..447190a 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -92,7 +92,7 @@ class SubCategorySerializer(serializers.ModelSerializer): parent = serializers.SerializerMethodField() class Meta: model = SubCategoryModel - fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'show', 'parent', 'image'] + fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'parent', 'image'] def get_product_count(self, obj): return obj.products.count() def get_parent(self, obj): @@ -103,7 +103,7 @@ class MainCategorySerializer(serializers.ModelSerializer): subcategorys = SubCategorySerializer(many=True) class Meta: model = MainCategoryModel - fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys'] + fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys', 'image'] class DynamicProductSerializer(serializers.ModelSerializer): diff --git a/backend/product/views.py b/backend/product/views.py index a1d873a..901e9fb 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -94,6 +94,12 @@ class AllProductsView(APIView): type=OpenApiTypes.INT, required=False, ), + OpenApiParameter( + name="category_type", + type=OpenApiTypes.STR, + required=False, + enum=['sub', 'main'], + ), OpenApiParameter( name="price_gte", description="Filter products with price greater than or equal to this value.", @@ -153,15 +159,25 @@ class AllProductsView(APIView): ) def get(self, request): try: - category_id = request.query_params.get('category', None) - if category_id: - try: - category_id = int(category_id) - except ValueError: - return Response({'detail': 'value error category id should be a number'}, status=status.HTTP_400_BAD_REQUEST) + category_slug = request.query_params.get('category') + category_type = request.query_params.get('category_type') - sub_category = get_object_or_404(SubCategoryModel, pk=category_id) - products = ProductModel.objects.filter(category=sub_category) + VALID_CATEGORY_TYPES = {'sub', 'main'} + + if category_slug: + if category_type not in VALID_CATEGORY_TYPES: + return Response( + {"detail": "category_type must be one of ['sub', 'main']"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if category_type == 'sub': + sub_category = get_object_or_404(SubCategoryModel, slug=category_slug) + products = ProductModel.objects.filter(category=sub_category) + else: # category_type == 'main' + main_category = get_object_or_404(MainCategoryModel, slug=category_slug) + sub_categories = main_category.subcategorys.all() + products = ProductModel.objects.filter(category__in=sub_categories) else: products = ProductModel.objects.all() @@ -204,8 +220,10 @@ class AllProductsView(APIView): return paginator.get_paginated_response(serializer.data) except MainCategoryModel.DoesNotExist: - return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) + return Response({"detail": "Main Category not found."}, status=status.HTTP_404_NOT_FOUND) + except SubCategoryModel.DoesNotExist: + return Response({"detail": "Sub Category not found."}, status=status.HTTP_404_NOT_FOUND) class ShowCaseCategoryListView(APIView): serializer_class = ShowCaseSliderSerialzier From b5eb6d8fd051da98d55d790c3263e1d99c39b042 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 21 May 2025 14:12:03 +0330 Subject: [PATCH 4/8] clean code AllProductsView --- backend/product/views.py | 54 ++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/backend/product/views.py b/backend/product/views.py index 901e9fb..432db49 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -163,12 +163,13 @@ class AllProductsView(APIView): category_type = request.query_params.get('category_type') VALID_CATEGORY_TYPES = {'sub', 'main'} + products = ProductModel.objects.all() if category_slug: if category_type not in VALID_CATEGORY_TYPES: return Response( {"detail": "category_type must be one of ['sub', 'main']"}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST ) if category_type == 'sub': @@ -178,36 +179,48 @@ class AllProductsView(APIView): main_category = get_object_or_404(MainCategoryModel, slug=category_slug) sub_categories = main_category.subcategorys.all() products = ProductModel.objects.filter(category__in=sub_categories) - else: - products = ProductModel.objects.all() + in_stock = request.query_params.get('in_stock') + if in_stock is not None: + if in_stock.lower() == 'true': + products = products.filter(variants__in_stock__gt=0) + elif in_stock.lower() != 'false': + return Response({'detail': 'in_stock must be "true" or "false".'}, status=status.HTTP_400_BAD_REQUEST) - # Filter by stock status if `in_stock` is specified - in_stock = request.query_params.get('in_stock', "false") == 'true' - if in_stock: - products = products.filter(variants__in_stock__gt=0) - - # Filter by discount if `has_discount` is specified - has_discount = request.query_params.get('has_discount', "false") == 'true' - if has_discount: - products = products.filter(variants__discount__gt=0) + # Filter by discount + has_discount = request.query_params.get('has_discount') + if has_discount is not None: + if has_discount.lower() == 'true': + products = products.filter(variants__discount__gt=0) + elif has_discount.lower() != 'false': + return Response({'detail': 'has_discount must be "true" or "false".'}, status=status.HTTP_400_BAD_REQUEST) # Search filter - search_query = request.query_params.get('search', None) + search_query = request.query_params.get('search') if search_query: products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query)) # Price filters - price_gte = request.query_params.get('price_gte', None) - price_lte = request.query_params.get('price_lte', None) + price_gte = request.query_params.get('price_gte') + price_lte = request.query_params.get('price_lte') products = products.annotate(min_price=Min('variants__price'), max_price=Max('variants__price')) if price_gte: - products = products.filter(max_price__gte=price_gte) + try: + price_gte = float(price_gte) + products = products.filter(max_price__gte=price_gte) + except ValueError: + return Response({'detail': 'price_gte must be a number.'}, status=status.HTTP_400_BAD_REQUEST) + if price_lte: - products = products.filter(min_price__lte=price_lte) + try: + price_lte = float(price_lte) + products = products.filter(min_price__lte=price_lte) + except ValueError: + return Response({'detail': 'price_lte must be a number.'}, status=status.HTTP_400_BAD_REQUEST) + # Sorting - sort_by = request.query_params.get('sort', None) + sort_by = request.query_params.get('sort') if sort_by in ['name', '-name', 'created_at', '-created_at']: products = products.order_by(sort_by) else: @@ -216,7 +229,10 @@ class AllProductsView(APIView): # Pagination paginator = self.pagination_class() paginated_products = paginator.paginate_queryset(products, request) - serializer = self.serializer_class(paginated_products, many=True, context={'request': request, 'view_type': 'list'}) + serializer = self.serializer_class( + paginated_products, many=True, + context={'request': request, 'view_type': 'list'} + ) return paginator.get_paginated_response(serializer.data) except MainCategoryModel.DoesNotExist: From 7e54d8a62f3d57f432c1282530100c8d150630ff Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 21 May 2025 14:17:18 +0330 Subject: [PATCH 5/8] OpenApiParameter( name="category", type=OpenApiTypes.STR, --- backend/product/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/product/views.py b/backend/product/views.py index 432db49..ed5a967 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -91,7 +91,8 @@ class AllProductsView(APIView): # ), OpenApiParameter( name="category", - type=OpenApiTypes.INT, + type=OpenApiTypes.STR, + description="slug category (send it with category type)", required=False, ), OpenApiParameter( From 2874798c67780993c2a6ce8b732ca2fb8befa340 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 21 May 2025 14:51:24 +0330 Subject: [PATCH 6/8] Add video field to MainCategoryModel and update related serializers and views --- backend/home/views.py | 10 +++++----- .../migrations/0046_maincategorymodel_video.py | 18 ++++++++++++++++++ .../0047_alter_maincategorymodel_video.py | 18 ++++++++++++++++++ backend/product/models.py | 2 +- backend/product/serializers.py | 2 +- 5 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 backend/product/migrations/0046_maincategorymodel_video.py create mode 100644 backend/product/migrations/0047_alter_maincategorymodel_video.py diff --git a/backend/home/views.py b/backend/home/views.py index 28241e8..eeb8e40 100644 --- a/backend/home/views.py +++ b/backend/home/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render, get_object_or_404, redirect from rest_framework.views import APIView, Response -from product.models import ProductModel, SubCategoryModel, DollorModel -from product.serializers import SubCategorySerializer, DynamicProductSerializer +from product.models import ProductModel, SubCategoryModel, MainCategoryModel +from product.serializers import SubCategorySerializer, DynamicProductSerializer, MainCategorySerializer from .serializers import * from .models import * from rest_framework import status @@ -24,8 +24,8 @@ class HomeView(APIView): sliders = SliderModel.objects.all() slider_ser = SliderSerializer(instance=sliders, many=True, context={'request': request}) - sub_categories = SubCategoryModel.objects.filter(show=True) - sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request}) + main_categories = MainCategoryModel.objects.all() + main_category_ser = MainCategorySerializer(instance=main_categories, many=True, context={'request': request}) products_to_show = ProductModel.objects.filter(show=True) product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'view_type': 'list'}) @@ -38,7 +38,7 @@ class HomeView(APIView): response = { 'sliders': slider_ser.data, - 'sub_categories': sub_category_ser.data, + 'main_categories': main_category_ser.data, 'products': product_ser.data, 'difreance_section': home_image_ser.data, 'show_case_slider': show_cases_ser.data diff --git a/backend/product/migrations/0046_maincategorymodel_video.py b/backend/product/migrations/0046_maincategorymodel_video.py new file mode 100644 index 0000000..eeb22f1 --- /dev/null +++ b/backend/product/migrations/0046_maincategorymodel_video.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-05-21 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0045_remove_subcategorymodel_show_maincategorymodel_image'), + ] + + operations = [ + migrations.AddField( + model_name='maincategorymodel', + name='video', + field=models.FileField(blank=True, null=True, upload_to='product_videos/', verbose_name='ویدیو'), + ), + ] diff --git a/backend/product/migrations/0047_alter_maincategorymodel_video.py b/backend/product/migrations/0047_alter_maincategorymodel_video.py new file mode 100644 index 0000000..9085cd4 --- /dev/null +++ b/backend/product/migrations/0047_alter_maincategorymodel_video.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-05-21 11:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0046_maincategorymodel_video'), + ] + + operations = [ + migrations.AlterField( + model_name='maincategorymodel', + name='video', + field=models.FileField(blank=True, null=True, upload_to='category_videos/', verbose_name='ویدیو'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index f7aacc8..fa404df 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -13,7 +13,7 @@ class MainCategoryModel(models.Model): 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) - + video = models.FileField(upload_to='category_videos/', blank=True, null=True, verbose_name='ویدیو') class Meta: verbose_name = "دسته‌بندی اصلی" verbose_name_plural = "دسته‌بندی‌هااصلی" diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 447190a..d6058dc 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -103,7 +103,7 @@ class MainCategorySerializer(serializers.ModelSerializer): subcategorys = SubCategorySerializer(many=True) class Meta: model = MainCategoryModel - fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys', 'image'] + fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys', 'image', 'video'] class DynamicProductSerializer(serializers.ModelSerializer): From bc214a2bdcc42605785e8b1f57fb7a868e820020 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 21 May 2025 15:02:02 +0330 Subject: [PATCH 7/8] product get slug --- backend/product/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/product/views.py b/backend/product/views.py index ed5a967..1a17238 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -55,8 +55,8 @@ class ProductView(APIView): serializer_class = DynamicProductSerializer permission_classes = [AllowAny] # authentication_classes = [] - def get(self, request, pk): - product = get_object_or_404(ProductModel, id=pk) + def get(self, request, slug): + product = get_object_or_404(ProductModel, slug=slug) if request.user.is_authenticated: cart_obj, _ = OrderModel.objects.get_or_create(user=request.user, status='CART') cart_items = cart_obj.items.all() From af45440e43843dbff35f8a11253bf3b58c8c09cb Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 21 May 2025 15:04:21 +0330 Subject: [PATCH 8/8] blog slug --- backend/blog/urls.py | 2 +- backend/blog/views.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/blog/urls.py b/backend/blog/urls.py index ca90f37..07d635b 100644 --- a/backend/blog/urls.py +++ b/backend/blog/urls.py @@ -3,5 +3,5 @@ from . import views urlpatterns = [ path('all', views.AllBlogView.as_view(), name='product-chat-view'), - path('', views.BlogView.as_view(), name='product-chat-view'), + path('', views.BlogView.as_view(), name='product-chat-view'), ] \ No newline at end of file diff --git a/backend/blog/views.py b/backend/blog/views.py index f450023..fb503cb 100644 --- a/backend/blog/views.py +++ b/backend/blog/views.py @@ -64,12 +64,12 @@ class BlogView(APIView): return ip - def get(self, request, pk): - blog = get_object_or_404(BlogModel, pk=pk) + def get(self, request, slug): + blog = get_object_or_404(BlogModel, slug=slug) if blog.is_published: # Track views using session client_ip = self.get_client_ip(request) - session_key = f'viewed_blog_{pk}_{client_ip}' + session_key = f'viewed_blog_{slug}_{client_ip}' if not request.session.get(session_key): blog.views += 1