diff --git a/backend/account/urls.py b/backend/account/urls.py index efdc99e..f3cb639 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -14,4 +14,5 @@ urlpatterns = [ path('address/', views.GetIDUserAddressView.as_view(), name='get-ID-address'), path('subscribe', views.SubscribeView.as_view(), name='subscibe'), path('attack/view/', views.ChangeViewAttack.as_view(), name='attack-view'), + path('logout', views.LogoutView.as_view(), name='logout'), ] \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index 64d6fac..2b52687 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -195,4 +195,27 @@ class ChangeViewAttack(View): attack = get_object_or_404(SecurityBreachAttemptModel, pk=pk) attack.viewd = not attack.viewd attack.save() - return redirect('admin:account_securitybreachattemptmodel_changelist') \ No newline at end of file + return redirect('admin:account_securitybreachattemptmodel_changelist') + + +from rest_framework import serializers +from rest_framework_simplejwt.tokens import RefreshToken + +class LogoutSerializer(serializers.Serializer): + refresh_token = serializers.CharField(help_text="Refresh token to be blacklisted") + +class LogoutView(APIView): + permission_classes = (IsAuthenticated,) + + @extend_schema( + request=LogoutSerializer, + responses={205: None, 400: "Bad request (invalid token or missing data)"}, + ) + def post(self, request): + try: + refresh_token = request.data["refresh_token"] + token = RefreshToken(refresh_token) + token.blacklist() + return Response(status=status.HTTP_205_RESET_CONTENT) + except Exception as e: + return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/backend/core/views.py b/backend/core/views.py index 6dbeb76..d57f36a 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -288,6 +288,5 @@ class FakeAdminLoginView(View): hacker, created = SecurityBreachAttemptModel.objects.get_or_create(ip_address=ip) hacker.trys += 1 hacker.save() - messages.error(request, "Please correct the error below.") - messages.error(request, "Please enter the correct شماره تماس and password for a staff account. Note that both fields may be case-sensitive.") + messages.error(request, "لطفا شماره تماس و گذرواژه را برای یک حساب کارمند وارد کنید. توجه داشته باشید که ممکن است هر دو به کوچکی و بزرگی حروف حساس باشند.") return render(request, 'admin/fake_login.html', self.get_context(request)) \ No newline at end of file diff --git a/backend/order/migrations/0021_ordermodel_discount_ordermodel_final_price_and_more.py b/backend/order/migrations/0021_ordermodel_discount_ordermodel_final_price_and_more.py new file mode 100644 index 0000000..c466457 --- /dev/null +++ b/backend/order/migrations/0021_ordermodel_discount_ordermodel_final_price_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.2 on 2025-03-17 13:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0020_rename_bank_record_ordermodel_bank_records'), + ] + + operations = [ + migrations.AddField( + model_name='ordermodel', + name='discount', + field=models.BigIntegerField(blank=True, null=True, verbose_name='کل تخقیف'), + ), + migrations.AddField( + model_name='ordermodel', + name='final_price', + field=models.BigIntegerField(blank=True, null=True, verbose_name='قیمت نهایی'), + ), + migrations.AddField( + model_name='ordermodel', + name='tax', + field=models.BigIntegerField(blank=True, null=True, verbose_name='مالیات'), + ), + migrations.AlterField( + model_name='ordermodel', + name='status', + field=models.CharField(choices=[('CART', 'در سبد خرید'), ('ADMIN_PENDING', 'در انتظار تایید'), ('PENDING', 'درحال پردازش'), ('POSTED', 'ارسال شده'), ('RECEIVED', 'تحویل شده'), ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده')], max_length=20, verbose_name='وضعیت سفارش'), + ), + ] diff --git a/backend/order/migrations/0022_alter_orderitemmodel_price.py b/backend/order/migrations/0022_alter_orderitemmodel_price.py new file mode 100644 index 0000000..7471b3c --- /dev/null +++ b/backend/order/migrations/0022_alter_orderitemmodel_price.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-03-17 14:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0021_ordermodel_discount_ordermodel_final_price_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='orderitemmodel', + name='price', + field=models.PositiveIntegerField(default=0, verbose_name='قیمت'), + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index aabb946..0e5f02f 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -59,7 +59,7 @@ class OrderModel(models.Model): bank_records = models.ManyToManyField(Bank, max_length=100, verbose_name='رکورد بانکی', null=True, blank=True) def __str__(self): - return f'سفارش: {self.id}' + return f'سفارش: {self.id + 1000}' class Meta: verbose_name = 'سفارش' @@ -80,27 +80,27 @@ class OrderModel(models.Model): - def discount(self): - # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) - # discount_percent = self.discount_code.percent - # return total_with_item_discount * ((100 - discount_percent) / 100) - pass + # def discount(self): + # # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) + # # discount_percent = self.discount_code.percent + # # return total_with_item_discount * ((100 - discount_percent) / 100) + # pass - def tax(self): - return self.total_without_tax() * 0.2 + # def tax(self): + # return self.total_without_tax() * 0.2 - def total(self): - pass + # def total(self): + # pass # return self.total_with_discount() + self.tax() - def final_price(self): - pass + # def final_price(self): + # pass - def submit_cart(self): - pass + # def submit_cart(self): + # pass @@ -108,7 +108,7 @@ class OrderModel(models.Model): class OrderItemModel(models.Model): order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") - price = models.PositiveIntegerField(verbose_name='قیمت', blank=True, null=True) + price = models.PositiveIntegerField(verbose_name='قیمت', default=0) product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") class Meta: verbose_name = 'ایتم سبد خرید' diff --git a/backend/order/permissons.py b/backend/order/permissons.py index 7b0ef86..af60575 100644 --- a/backend/order/permissons.py +++ b/backend/order/permissons.py @@ -1,7 +1,7 @@ from rest_framework.permissions import BasePermission class CanDeleteCartItemPermissions(BasePermission): - message = "شما دسترسی حذف این ایتم رو ندارید" + message = "شما دسترسی حذف این ایتم را ندارید" def has_object_permission(self, request, view, obj): if obj.order.user != request.user: @@ -12,4 +12,15 @@ class CanDeleteCartItemPermissions(BasePermission): self.message = "وضعیت سفارش سبد خرید نیست و آیتمی را نمی‌توانید حذف کنید." return False + return True + + +class GetOrderPermission(BasePermission): + message = "شما دسترسی به این سفارش را ندارید" + def has_object_permission(self, request, view, obj): + if obj.user != request.user: + return False + if obj.status != 'CART': + self.message = "سفارش در وضعیت سبد خرید است" + return False return True \ No newline at end of file diff --git a/backend/order/serializers.py b/backend/order/serializers.py index ce2edc3..f25a84f 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -46,13 +46,30 @@ class DiscountCodeSerializer(serializers.ModelSerializer): class OrderItemSerailzier(serializers.ModelSerializer): product = serializers.SerializerMethodField() + discount_amount = serializers.SerializerMethodField() + price = serializers.SerializerMethodField() + final_price = serializers.SerializerMethodField() + discount = serializers.SerializerMethodField() class Meta: model = OrderItemModel - exclude = ('price', 'order') - read_only_fields = ('order', 'product', ) + exclude = ('order',) + read_only_fields = ('order', 'product',) def get_product(self, obj): return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data + def get_discount_amount(self, obj): + discount_amount = int(obj.price * (obj.product.discount / 100)) + return f'{(discount_amount * obj.quantity):,.0f} تومان' + + def get_final_price(self, obj): + final_price = obj.price - int(obj.price * (obj.product.discount / 100)) + return f'{(final_price * obj.quantity):,.0f} تومان' + + def get_price(self, obj): + return f'{(obj.price * obj.quantity):,.0f} تومان' + + def get_discount(self, obj): + return obj.product.discount @@ -78,21 +95,17 @@ class CartSerializer(serializers.ModelSerializer): return None - - - def get_tax(self, obj): return f'{1000:,.0f} تومان' def get_cart_total(self, obj): - return f'{10000:,.0f} تومان' - def get_final_price(self, obj): + def get_final_price(self, obj): return f'{8000:,.0f} تومان' -class OrderSerializer(serializers.ModelSerializer): +class OrderListSerializer(serializers.ModelSerializer): count = serializers.SerializerMethodField() images = serializers.SerializerMethodField() order_id = serializers.SerializerMethodField() @@ -100,6 +113,35 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = OrderModel fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status'] + read_only_fields = ['count', 'images', 'order_id', 'verbose_status'] + def get_verbose_status(self, obj): + return obj.get_status_display() + + def get_count(self, obj): + return obj.items.all().count() + + def get_images(self, obj): + image_list = [ + self.context.get('request').build_absolute_uri(image.image.url) + if (image := item.product.images.all().first()) else None + for item in obj.items.all()[:3] + ] + return filter(lambda x: x is not None, image_list) + def get_order_id(self, obj): + return obj.id + 1000 + + +class OrderGetSerializer(serializers.ModelSerializer): + count = serializers.SerializerMethodField() + images = serializers.SerializerMethodField() + order_id = serializers.SerializerMethodField() + verbose_status = serializers.SerializerMethodField() + items = OrderItemSerailzier(many=True) + address = UserAddressSerializer() + discount_code = DiscountCodeSerializer() + class Meta: + model = OrderModel + fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount'] def get_verbose_status(self, obj): return obj.get_status_display() diff --git a/backend/order/urls.py b/backend/order/urls.py index c439005..969c612 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -1,7 +1,7 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include -from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView +from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView, OrderGetView from .views import PaymentView, callback_view urlpatterns = [ @@ -12,4 +12,5 @@ urlpatterns = [ path('cart/item/', CartItemViews.as_view(), name='change-item-cart'), path('payment', PaymentView.as_view(), name='payment'), path('callback', callback_view, name='callback-gateway'), + path('', OrderGetView.as_view(), name='order-get'), ] diff --git a/backend/order/views.py b/backend/order/views.py index a4a9309..d797810 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -8,11 +8,12 @@ from .serializers import * # from cart.models import from rest_framework import status from .models import OrderItemModel, OrderModel, DiscountCode -from .permissons import CanDeleteCartItemPermissions +from .permissons import CanDeleteCartItemPermissions, GetOrderPermission from azbankgateways import bankfactories, models as bank_models from azbankgateways.exceptions import AZBankGatewaysException from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from utils.pagination import StructurePagination +from order.models import OrderModel try: pass except DiscountNotAvailableError: @@ -45,7 +46,14 @@ class ApplyDiscountView(APIView): cart_order.save() return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK) - + def delete(self, request): + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + cart_order.discount_code = None + cart_order.save() + return Response({'detail': 'کد تخفیف با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT) class CartItemClear(APIView): permission_classes = [IsAuthenticated] @@ -108,7 +116,7 @@ class CartView(APIView): class OrderlistView(APIView): permission_classes = [IsAuthenticated] - serializer_class = OrderSerializer + serializer_class = OrderListSerializer pagination_class = StructurePagination @extend_schema( parameters=[ @@ -162,6 +170,20 @@ class OrderlistView(APIView): +class OrderGetView(APIView): + permission_classes = [IsAuthenticated, GetOrderPermission] + serializer_class = OrderGetSerializer + def get(self, request, pk): + order_object = get_object_or_404(OrderModel, pk=pk) + + + permission = GetOrderPermission() + if not permission.has_object_permission(request, self, order_object): + return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN) + + + order_ser = self.serializer_class(order_object, context={'request': request}) + return Response(order_ser.data, status=status.HTTP_200_OK) class PaymentView(APIView): permission_classes = [IsAuthenticated] diff --git a/backend/product/serializers.py b/backend/product/serializers.py index cd65826..135e7da 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -65,7 +65,12 @@ class ProductVariantSerialzier(serializers.ModelSerializer): request = self.context.get('request') if not request or not request.user.is_authenticated: return 0 - return 1 + cart_items = self.context.get('cart_items', []) + if cart_items: + for item in cart_items: + if item['product']['id'] == obj.id: + return item['quantity'] + return 0 diff --git a/backend/product/views.py b/backend/product/views.py index 79b1c60..ccc51ee 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -10,8 +10,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly from utils.pagination import StructurePagination from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from rest_framework.permissions import AllowAny - - +from order.serializers import OrderItemSerailzier +from order.models import OrderModel # class APIView(APIView): # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) @@ -54,7 +54,15 @@ class ProductView(APIView): # authentication_classes = [] def get(self, request, pk): product = get_object_or_404(ProductModel, id=pk) - product_ser = self.serializer_class(instance=product, many=False, context={'request': request, 'view_type': 'instance'}) + if request.user.is_authenticated: + cart_obj, _ = OrderModel.objects.get_or_create(user=request.user, status='CART') + cart_items = cart_obj.items.all() + cart_items_ser = OrderItemSerailzier(cart_items, many=True, context={'request': request}) + product_ser_context = {'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data} + else: + product_ser_context = {'request': request, 'view_type': 'instance'} + + product_ser = self.serializer_class(instance=product, many=False, context=product_ser_context) return Response(product_ser.data, status=status.HTTP_200_OK) diff --git a/backend/ticket/serializers.py b/backend/ticket/serializers.py index 6be7138..d99a8d2 100644 --- a/backend/ticket/serializers.py +++ b/backend/ticket/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from .models import Ticket, Message, Attachment from django.utils.timezone import localtime -from order.serializers import OrderSerializer +from order.serializers import OrderListSerializer from order.serializers import OrderModel class AttachmentSerializer(serializers.ModelSerializer): @@ -50,7 +50,7 @@ class TicketSerializer(serializers.ModelSerializer): messages = MessageAttachmentSerializer(many=True, read_only=True) message = MessageForTicketSerializer(write_only=True) order_id = serializers.PrimaryKeyRelatedField(queryset=OrderModel.objects.all(), write_only=True, source='order') - order = OrderSerializer(read_only=True) + order = OrderListSerializer(read_only=True) class Meta: model = Ticket exclude = ('customer', 'admin') @@ -79,7 +79,7 @@ class TicketSerializer(serializers.ModelSerializer): class TicketListSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField() ticket_category = serializers.SerializerMethodField() - order = OrderSerializer() + order = OrderListSerializer() class Meta: model = Ticket exclude = ('customer', 'admin', )