From 7ae2cf9f4c2c9926eda0be10ed1cc5835ce45945 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 14 Jan 2025 19:58:32 +0330 Subject: [PATCH 01/26] open a few apis --- backend/product/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/product/views.py b/backend/product/views.py index 63b1c5d..adeb220 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -14,6 +14,7 @@ from rest_framework.permissions import AllowAny class AllCategories(APIView): serializer_class = CategorySerializer + authentication_classes = [] def get(self, request): categories = CategoryModel.objects.all() categories_ser = self.serializer_class(instance=categories, many=True) @@ -22,6 +23,7 @@ class AllCategories(APIView): class ProductView(APIView): serializer_class = ProductSerializer permission_classes = [AllowAny] + authentication_classes = [] def get(self, request, pk): product = get_object_or_404(ProductModel, id=pk) product_ser = self.serializer_class(instance=product, many=False) @@ -30,7 +32,7 @@ class ProductView(APIView): class AllProductsView(APIView): serializer_class = ProductSerializer pagination_class = StructurePagination - + authentication_classes = [] @extend_schema( parameters=[ OpenApiParameter( From 2ad1f38b8ae6d56498bbc7eb215afb805c841e7a Mon Sep 17 00:00:00 2001 From: marzban-dev Date: Tue, 14 Jan 2025 21:18:42 +0330 Subject: [PATCH 02/26] Import useAuth --- frontend/composables/api/account/useGetAccount.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/composables/api/account/useGetAccount.ts b/frontend/composables/api/account/useGetAccount.ts index 4366999..7c2ed83 100644 --- a/frontend/composables/api/account/useGetAccount.ts +++ b/frontend/composables/api/account/useGetAccount.ts @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/vue-query"; import { API_ENDPOINTS, QUERY_KEYS } from "~/constants"; +import { useAuth } from "~/composables/api/auth/useAuth"; // types From 59fa06c59942a8dc154bf5d4a8db57ac90bd2b00 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 14 Jan 2025 22:02:58 +0330 Subject: [PATCH 03/26] category list filter created at field of products in_stuck filter created at filter has discount filter --- .../0011_productmodel_created_at.py | 20 +++++ .../migrations/0012_productmodel_category.py | 19 +++++ backend/product/models.py | 3 +- backend/product/views.py | 78 +++++++++++++++---- 4 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 backend/product/migrations/0011_productmodel_created_at.py create mode 100644 backend/product/migrations/0012_productmodel_category.py 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) From 6401ce2a24298cac7612065abe0ea6b83cbbba81 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 14 Jan 2025 22:43:34 +0330 Subject: [PATCH 04/26] add category search --- backend/product/views.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/product/views.py b/backend/product/views.py index 6cd40d4..563e442 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -8,8 +8,7 @@ from django.db.models import Q from django.shortcuts import get_object_or_404 from rest_framework.permissions import IsAuthenticatedOrReadOnly from utils.pagination import StructurePagination -from drf_spectacular.utils import extend_schema, OpenApiParameter -from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from rest_framework.permissions import AllowAny @@ -26,8 +25,26 @@ from rest_framework.permissions import AllowAny class AllCategories(APIView): serializer_class = CategorySerializer authentication_classes = [] + @extend_schema( + parameters=[ + OpenApiParameter( + name="search", + description="Search by category name or description.", + required=False, + type=OpenApiTypes.STR, + ) + ], + responses={ + 200: CategorySerializer(many=True), + 404: OpenApiTypes.OBJECT, + }, + ) def get(self, request): - categories = CategoryModel.objects.all() + 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() categories_ser = self.serializer_class(instance=categories, many=True) return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK) @@ -40,11 +57,6 @@ 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 From 830e20dec35072220614ef4dfc618308e94d94ed Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 14 Jan 2025 22:50:43 +0330 Subject: [PATCH 05/26] price formating --- backend/product/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/product/serializers.py b/backend/product/serializers.py index a4726f9..935da30 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -13,12 +13,12 @@ class ProductChatSerializer(serializers.ModelSerializer): if dollor_price is None: raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) if obj.currency == 'toman': - return obj.price + toman_price = obj.price elif obj.currency == 'dollor': - return obj.price * dollor_price + toman_price = obj.price * dollor_price elif obj.currency == 'derham': - return obj.price * dollor_price * dollar_to_dirham - + toman_price = obj.price * dollor_price * dollar_to_dirham + return "{:,.0f} تومان".format(toman_price) class ProductSerializer(ProductChatSerializer): class Meta: model = ProductModel From abceb2a06617b74ffab9d8920d62f5d92a2f9a29 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 14 Jan 2025 23:24:29 +0330 Subject: [PATCH 06/26] add is_new field --- backend/product/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 935da30..277e28b 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -1,8 +1,11 @@ from .models import * from rest_framework import serializers +from django.utils import timezone +from datetime import timedelta class ProductChatSerializer(serializers.ModelSerializer): price = serializers.SerializerMethodField() + is_new = serializers.SerializerMethodField() class Meta: model = ProductModel fields = ['name', 'description', 'price', 'in_stock', 'discount', ] @@ -19,6 +22,9 @@ class ProductChatSerializer(serializers.ModelSerializer): elif obj.currency == 'derham': toman_price = obj.price * dollor_price * dollar_to_dirham return "{:,.0f} تومان".format(toman_price) + def get_is_new(self, obj): + return timezone.now() < obj.created_at + timedelta(days=7) + class ProductSerializer(ProductChatSerializer): class Meta: model = ProductModel From 360d71af8d656f81c897a3775216ead1542ab1fe Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 14 Jan 2025 23:57:53 +0330 Subject: [PATCH 07/26] 7 days for refresh token and 1 day for token and verify token --- backend/account/urls.py | 1 + backend/core/settings.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/account/urls.py b/backend/account/urls.py index c01ce3a..93ac5cb 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -4,6 +4,7 @@ from djoser.urls.jwt import views as djoser_jwt_views urlpatterns = [ path('profile', views.ProfileView.as_view()), + path('verify', djoser_jwt_views.TokenVerifyView.as_view(), name='jwt-verify'), path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'), path('address/create', views.CreateAddressView.as_view(), name='create-address'), path('address/edit/', views.EditAddressView.as_view(), name='edit-address'), diff --git a/backend/core/settings.py b/backend/core/settings.py index 05712e2..dcee877 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -201,7 +201,7 @@ REST_FRAMEWORK = { SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, } From 5dfe35e42d87e817766e4feb30726ff4b3a2c563 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 15 Jan 2025 00:20:35 +0330 Subject: [PATCH 08/26] 2 min token --- backend/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/core/settings.py b/backend/core/settings.py index dcee877..6336fd9 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -200,7 +200,7 @@ REST_FRAMEWORK = { } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=2), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, From f178754ba61183610885e20f72c4b8e43ddc3222 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 15 Jan 2025 01:26:06 +0330 Subject: [PATCH 09/26] add refresh token --- backend/core/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/core/urls.py b/backend/core/urls.py index d8e4a3e..2286b6f 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -16,7 +16,7 @@ urlpatterns = [ path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), - # path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('admin/', admin.site.urls), path('schema/', SpectacularAPIView.as_view(), name='schema'), # path('comment/', views.CommentView.as_view(), name='comment-list'), From 6e1fb1f072201667546b1c7a7b06befd31383925 Mon Sep 17 00:00:00 2001 From: marzban-dev Date: Thu, 16 Jan 2025 15:53:43 +0330 Subject: [PATCH 10/26] Update imports --- frontend/components/global/ToastContainer/ToastBox.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/components/global/ToastContainer/ToastBox.vue b/frontend/components/global/ToastContainer/ToastBox.vue index d274de6..d12d88e 100644 --- a/frontend/components/global/ToastContainer/ToastBox.vue +++ b/frontend/components/global/ToastContainer/ToastBox.vue @@ -2,7 +2,8 @@ // import -import type { ToastOptions } from "~/composables/useToast"; +import type { ToastOptions } from "~/composables/global/useToast"; +import { useToast } from "~/composables/global/useToast"; // type From 17493beffa5bf63c2dd0b7e365c6786cd4a005b6 Mon Sep 17 00:00:00 2001 From: marzban-dev Date: Thu, 16 Jan 2025 15:53:52 +0330 Subject: [PATCH 11/26] Updated --- frontend/components/global/Header.vue | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/components/global/Header.vue b/frontend/components/global/Header.vue index 07cda45..3341176 100644 --- a/frontend/components/global/Header.vue +++ b/frontend/components/global/Header.vue @@ -19,24 +19,24 @@ const { logout } = useAuth(); const nav_links = ref([ { title: "فروشگاه", - path: "#", + path: "#" }, { title: "دسته بندی ها", - path: "#", + path: "#" }, { title: "جستجو", - path: "#", + path: "#" }, { title: "ارتباط با ما", - path: "#", + path: "#" }, { title: "امکانات", - path: "#", - }, + path: "#" + } ]); @@ -53,7 +53,13 @@ const nav_links = ref([ -
KIR
+