diff --git a/backend/product/migrations/0011_productmodel_created_at.py b/backend/product/migrations/0011_productmodel_created_at.py new file mode 100644 index 0000000..d600e23 --- /dev/null +++ b/backend/product/migrations/0011_productmodel_created_at.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-01-14 17:41 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0010_remove_productmodel_link_of_metas'), + ] + + operations = [ + migrations.AddField( + model_name='productmodel', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='زمان ثبت محصول'), + preserve_default=False, + ), + ] diff --git a/backend/product/migrations/0012_productmodel_category.py b/backend/product/migrations/0012_productmodel_category.py new file mode 100644 index 0000000..2aafeb4 --- /dev/null +++ b/backend/product/migrations/0012_productmodel_category.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-01-14 18:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0011_productmodel_created_at'), + ] + + operations = [ + migrations.AddField( + model_name='productmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='product.categorymodel'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index fd56048..0d0cd2f 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -112,7 +112,8 @@ class ProductModel(models.Model): meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید') 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) 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/views.py b/backend/product/views.py index adeb220..6cd40d4 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -12,6 +12,17 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter from drf_spectacular.types import OpenApiTypes from rest_framework.permissions import AllowAny + +# class CustomAPIView(APIView): +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# print('here') +# print(self.permission_classes) +# if not getattr(self, 'permission_classes')[0] != AllowAny or not self.permission_classes: +# print('asdf') +# self.authentication_classes = [] + + class AllCategories(APIView): serializer_class = CategorySerializer authentication_classes = [] @@ -29,10 +40,17 @@ class ProductView(APIView): product_ser = self.serializer_class(instance=product, many=False) return Response(product_ser.data, status=status.HTTP_200_OK) +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes +from django.db.models import Q + class AllProductsView(APIView): serializer_class = ProductSerializer pagination_class = StructurePagination - authentication_classes = [] + authentication_classes = [] # Add authentication if required + @extend_schema( parameters=[ OpenApiParameter( @@ -43,10 +61,12 @@ class AllProductsView(APIView): ), OpenApiParameter( name="category", - description="Filter by category ID.", + type={'type': 'array', 'items': {'type': 'number'}}, + location=OpenApiParameter.QUERY, required=False, - type=OpenApiTypes.INT, - ), + style='form', + explode=False, + ), OpenApiParameter( name="price_gte", description="Filter products with price greater than or equal to this value.", @@ -63,7 +83,7 @@ class AllProductsView(APIView): name="sort", description=( "Sort results by one of the following fields:\n" - "`name`, `-name`, `price`, `-price`, `discount`, `-discount`." + "`name`, `-name`, `price`, `-price`, `discount`, `-discount`, `created_at`, `-created_at`." "\nPrefix with `-` for descending order." ), required=False, @@ -81,32 +101,56 @@ class AllProductsView(APIView): 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 category ID to filter products in that category and its subcategories." + "Provide a list of category IDs to filter products by those categories and their subcategories." ), responses={ 200: ProductSerializer(many=True), 404: OpenApiTypes.OBJECT, }, ) - def get(self, request, pk=None): + def get(self, request): try: - if pk: - category = Category.objects.get(pk=pk) - products = ProductModel.objects.filter(category__in=category.get_descendants(include_self=True)) + # 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) else: products = ProductModel.objects.all() + # 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(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(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)) - category_filter = request.query_params.get('category', None) - if category_filter: - products = products.filter(category__id=category_filter) - + # Price filters price_gte = request.query_params.get('price_gte', None) price_lte = request.query_params.get('price_lte', None) if price_gte: @@ -114,18 +158,20 @@ class AllProductsView(APIView): if price_lte: products = products.filter(price__lte=price_lte) + # Sorting sort_by = request.query_params.get('sort', None) - if sort_by in ['name', '-name', 'price', '-price', 'discount', '-discount']: + if sort_by in ['name', '-name', 'price', '-price', 'discount', '-discount', '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) return paginator.get_paginated_response(serializer.data) - except Category.DoesNotExist: + except CategoryModel.DoesNotExist: return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND)