From 00b5ce5a6c3b87a6cd01f92e4b749b6db06d3b78 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 15 Feb 2025 00:04:51 +0330 Subject: [PATCH 01/22] ticket model and admin add ticket category --- backend/requirements.txt | 3 ++- backend/ticket/admin.py | 16 ++++++++++++---- .../migrations/0005_ticket_ticket_category.py | 19 +++++++++++++++++++ .../0006_alter_ticket_ticket_category.py | 18 ++++++++++++++++++ .../0007_alter_ticket_ticket_category.py | 18 ++++++++++++++++++ backend/ticket/models.py | 11 ++++++++++- 6 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 backend/ticket/migrations/0005_ticket_ticket_category.py create mode 100644 backend/ticket/migrations/0006_alter_ticket_ticket_category.py create mode 100644 backend/ticket/migrations/0007_alter_ticket_ticket_category.py diff --git a/backend/requirements.txt b/backend/requirements.txt index 37e397b..d8019ef 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,7 +19,8 @@ django-dbbackup==4.2.1 django-filter==24.3 django-import-export==4.1.1 django-iranian-cities==1.0.2 -django-unfold==0.46.0 +django-jalali==7.3.0 +django-unfold==0.48.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 djoser==2.3.1 diff --git a/backend/ticket/admin.py b/backend/ticket/admin.py index 21fd491..121c965 100644 --- a/backend/ticket/admin.py +++ b/backend/ticket/admin.py @@ -6,6 +6,13 @@ from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField +from unfold.contrib.filters.admin import ( + ChoicesDropdownFilter, + MultipleChoicesDropdownFilter, +) + + + class MessageInline(TabularInline): model = Message @@ -16,9 +23,9 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - search_fields = ['subject',] - list_filter = ['status'] - + search_fields = ['subject', 'messages__content'] + list_filter = [('status', ChoicesDropdownFilter), ('ticket_category', ChoicesDropdownFilter)] + list_filter_submit = True compressed_fields = True warn_unsaved_form = True @@ -27,7 +34,8 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } - list_display = ['subject', 'customer', 'admin', 'status', 'admin', 'status', 'created_at'] + readonly_fields = ('created_at', 'updated_at') + list_display = ['subject', 'ticket_category', 'customer', 'admin', 'status', 'created_at'] inlines = [MessageInline] radio_fields = {'status': admin.VERTICAL} diff --git a/backend/ticket/migrations/0005_ticket_ticket_category.py b/backend/ticket/migrations/0005_ticket_ticket_category.py new file mode 100644 index 0000000..9ab18fb --- /dev/null +++ b/backend/ticket/migrations/0005_ticket_ticket_category.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-02-13 21:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0004_alter_message_content_alter_message_created_at_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ticket', + name='ticket_category', + field=models.CharField(default='other', max_length=30, verbose_name='دسته بندی تیکت'), + preserve_default=False, + ), + ] diff --git a/backend/ticket/migrations/0006_alter_ticket_ticket_category.py b/backend/ticket/migrations/0006_alter_ticket_ticket_category.py new file mode 100644 index 0000000..21535ef --- /dev/null +++ b/backend/ticket/migrations/0006_alter_ticket_ticket_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-13 21:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0005_ticket_ticket_category'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='ticket_category', + field=models.CharField(choices=[('finance_and_accounting', 'مالی و حسابداری - Finance and Accounting'), ('user_profile', 'پروفایل کاربری - User Profile'), ('order_tracking', 'پیگیری سفارش - Order Tracking'), ('authentication', 'احراز هویت - Authentication'), ('product', 'محصول - Product'), ('bug_reporting', 'اعلام باگ و خطا در وبسایت - Bug and Error Reporting'), ('other', 'سایر - Other')], max_length=30, verbose_name='دسته بندی تیکت'), + ), + ] diff --git a/backend/ticket/migrations/0007_alter_ticket_ticket_category.py b/backend/ticket/migrations/0007_alter_ticket_ticket_category.py new file mode 100644 index 0000000..ab5d047 --- /dev/null +++ b/backend/ticket/migrations/0007_alter_ticket_ticket_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-13 21:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0006_alter_ticket_ticket_category'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='ticket_category', + field=models.CharField(choices=[('Finance and Accounting', 'مالی و حسابداری'), ('User Profile', 'پروفایل کاربری'), ('Order Tracking', 'پیگیری سفارش'), ('Authentication', 'احراز هویت'), ('Product', 'محصول'), ('Bug and Error Reporting', 'اعلام باگ و خطا در وبسایت'), ('Other', 'سایر')], max_length=30, verbose_name='دسته بندی تیکت'), + ), + ] diff --git a/backend/ticket/models.py b/backend/ticket/models.py index 75600c4..c6d7fcb 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -8,8 +8,17 @@ class Ticket(models.Model): ('resolved', 'حل شده'), ('closed', 'بسته'), ] - + CATEGORY_CHOICES = [ + ('finance_and_accounting', 'مالی و حسابداری'), + ('user_profile', 'پروفایل کاربری'), + ('order_tracking', 'پیگیری سفارش'), + ('authentication', 'احراز هویت'), + ('product', 'محصول'), + ('bug_and_error_reporting', 'اعلام باگ و خطا در وبسایت'), + ('other', 'سایر'), + ] subject = models.CharField(max_length=255, verbose_name='موضوع') + ticket_category = models.CharField(max_length=30, verbose_name='دسته بندی تیکت', choices=CATEGORY_CHOICES) customer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tickets", verbose_name='کاربر') admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_tickets", verbose_name='ادمین') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name='وضعیت تیکت') From 81d75d61e72efae0864a0f3f683b84ef70878ee7 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 15 Feb 2025 00:08:16 +0330 Subject: [PATCH 02/22] product varient change list bug fix order item change quantity and remove --- backend/core/urls.py | 1 + backend/order/execptions.py | 2 + .../0004_alter_orderitemmodel_product.py | 20 ++++++ backend/order/models.py | 19 +++--- backend/order/serializers.py | 9 +++ backend/order/urls.py | 8 +++ backend/order/views.py | 67 ++++++++++++++++++- backend/product/admin.py | 4 +- ...24_alter_attributevalue_unique_together.py | 17 +++++ ...lter_productvariant_attributes_and_more.py | 22 ++++++ .../0026_alter_productvariant_attributes.py | 18 +++++ .../0027_alter_productvariant_attributes.py | 18 +++++ .../0028_alter_productvariant_attributes.py | 18 +++++ ...attributevalue_unique_together_and_more.py | 21 ++++++ ...ductvariant_product_attributes_and_more.py | 28 ++++++++ backend/product/models.py | 6 +- 16 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 backend/order/execptions.py create mode 100644 backend/order/migrations/0004_alter_orderitemmodel_product.py create mode 100644 backend/order/serializers.py create mode 100644 backend/order/urls.py create mode 100644 backend/product/migrations/0024_alter_attributevalue_unique_together.py create mode 100644 backend/product/migrations/0025_alter_productvariant_attributes_and_more.py create mode 100644 backend/product/migrations/0026_alter_productvariant_attributes.py create mode 100644 backend/product/migrations/0027_alter_productvariant_attributes.py create mode 100644 backend/product/migrations/0028_alter_productvariant_attributes.py create mode 100644 backend/product/migrations/0029_alter_attributevalue_unique_together_and_more.py create mode 100644 backend/product/migrations/0030_rename_attributes_productvariant_product_attributes_and_more.py diff --git a/backend/core/urls.py b/backend/core/urls.py index b5e2f05..56a7db0 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ path('chat/', include('chat.urls')), path('tickets/', include('ticket.urls')), path('blogs/', include('blog.urls')), + path('order/', include('order.urls')), path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] diff --git a/backend/order/execptions.py b/backend/order/execptions.py new file mode 100644 index 0000000..c92b966 --- /dev/null +++ b/backend/order/execptions.py @@ -0,0 +1,2 @@ +class DiscountNotAvailableError(Exception): + pass \ No newline at end of file diff --git a/backend/order/migrations/0004_alter_orderitemmodel_product.py b/backend/order/migrations/0004_alter_orderitemmodel_product.py new file mode 100644 index 0000000..7008b7e --- /dev/null +++ b/backend/order/migrations/0004_alter_orderitemmodel_product.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0003_alter_orderitemmodel_product'), + ('product', '0023_alter_productimagemodel_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='orderitemmodel', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='product.productvariant', verbose_name='محصول'), + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index f7efb32..94b5396 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -1,8 +1,8 @@ from django.db import models from account.models import User, UserAddressModel -from product.models import ProductModel +from product.models import ProductModel, ProductVariant from django.utils import timezone - +from .execptions import DiscountNotAvailableError class DiscountCode(models.Model): name = models.CharField(max_length=50, verbose_name='کد تخفیف') percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد') @@ -43,13 +43,15 @@ class OrderModel(models.Model): verbose_name = 'سفارش' verbose_name_plural = 'سفارشات' - def total_without_tax(self): - return sum(item.total() for item in self.items.all()) + # def total_without_tax(self): + # return sum(item.total() for item in self.items.all()) def total_with_discount(self): total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) - if self.discount_code and self.discount_code.is_valid(): + if self.discount_code: + if not self.discount_code.is_valid(): + raise DiscountNotAvailableError('این کد تخفیف دیگر معتبر نیست') discount_percent = self.discount_code.percent return total_with_item_discount * ((100 - discount_percent) / 100) return total_with_item_discount @@ -59,7 +61,7 @@ class OrderModel(models.Model): return self.total_without_tax() * 0.2 def total(self): - return self.total_without_tax + self.tax() + return self.total_with_discount() + self.tax() def remove_order_item(self, item_pk, quantity): pass @@ -75,7 +77,7 @@ class OrderModel(models.Model): class OrderItemModel(models.Model): order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') quantity = models.SmallIntegerField(verbose_name="تعداد") - product = models.ForeignKey(ProductModel, on_delete=models.PROTECT, verbose_name="محصول") + product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") class Meta: verbose_name = 'محصول خریداری شده' verbose_name_plural = 'محصولات خریداری شده' @@ -85,5 +87,6 @@ class OrderItemModel(models.Model): def total_with_discount(self): return self.quantity * self.product.get_toman_price_after_discount() - + def __str__(self): + return f'ایتم سبد خرید محصول: {self.product} کاربر {self.order.user}' diff --git a/backend/order/serializers.py b/backend/order/serializers.py new file mode 100644 index 0000000..2a7d8e8 --- /dev/null +++ b/backend/order/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers +from .models import OrderItemModel + + +class OrderItemSerailzier(serializers.ModelSerializer): + class Meta: + model = OrderItemModel + fields = "__all__" + read_only_fields = ('order', 'product') \ No newline at end of file diff --git a/backend/order/urls.py b/backend/order/urls.py new file mode 100644 index 0000000..c506e46 --- /dev/null +++ b/backend/order/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include +from .views import CartItemViews + +urlpatterns = [ + path('cart/item/', CartItemViews.as_view(), name='add cart'), +] diff --git a/backend/order/views.py b/backend/order/views.py index 91ea44a..1f3451e 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -1,3 +1,68 @@ from django.shortcuts import render +from .execptions import DiscountNotAvailableError +from rest_framework.views import APIView, Response +from django.shortcuts import get_object_or_404 +from product.models import ProductVariant +from rest_framework.permissions import IsAuthenticated +from .serializers import OrderItemSerailzier +# from cart.models import +from rest_framework import status +from .models import OrderItemModel, OrderModel +try: + pass +except DiscountNotAvailableError: + pass -# Create your views here. +""" + +add post +remove delete +show get + +pay +""" + + + +class CartItemViews(APIView): + permission_classes = [IsAuthenticated] + serializer_class = OrderItemSerailzier + def post(self, request, pk): + product_variant = get_object_or_404(ProductVariant, pk=pk) + + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + order_item, created = OrderItemModel.objects.get_or_create( + order=cart_order, + product=product_variant, + defaults={'quantity': request.data.get('quantity', 1)} + ) + + if not created: + order_item.quantity = request.data.get('quantity', 1) + order_item.save() + + return Response({'detail': 'it did something'}, status=status.HTTP_201_CREATED) + def delete(self, request, pk): + product_variant = get_object_or_404(ProductVariant, pk=pk) + + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + # order_item, created = OrderItemModel.objects.get_or_create( + # order=cart_order, + # product=product_variant, + # defaults={'quantity': request.data.get('quantity', 1)} + # ) + order_item = get_object_or_404(OrderItemModel, order=cart_order, product=product_variant) + order_item.delete() + return Response({'detail': 'it did something related to delete'}, status=status.HTTP_204_NO_CONTENT) + + + +class CartViews(APIView): + def get(self, request): + pass diff --git a/backend/product/admin.py b/backend/product/admin.py index 43b905d..5ea98c7 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -15,7 +15,7 @@ from unfold.decorators import action, display class ProductVariantAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - autocomplete_fields = ['attributes'] + autocomplete_fields = ['product_attributes', 'images', 'in_pack_items'] warn_unsaved_form = True @@ -105,7 +105,7 @@ class ProductVariantInLine(StackedInline): show_change_link = True tab = True min_num = 1 - autocomplete_fields = ['attributes', 'in_pack_items', 'images'] + autocomplete_fields = ['product_attributes', 'in_pack_items', 'images'] # search_fields = [''] diff --git a/backend/product/migrations/0024_alter_attributevalue_unique_together.py b/backend/product/migrations/0024_alter_attributevalue_unique_together.py new file mode 100644 index 0000000..28379cb --- /dev/null +++ b/backend/product/migrations/0024_alter_attributevalue_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0023_alter_productimagemodel_options_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='attributevalue', + unique_together=set(), + ), + ] diff --git a/backend/product/migrations/0025_alter_productvariant_attributes_and_more.py b/backend/product/migrations/0025_alter_productvariant_attributes_and_more.py new file mode 100644 index 0000000..ef29eb5 --- /dev/null +++ b/backend/product/migrations/0025_alter_productvariant_attributes_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0024_alter_attributevalue_unique_together'), + ] + + operations = [ + migrations.AlterField( + model_name='productvariant', + name='attributes', + field=models.ManyToManyField(blank=True, related_name='variant', to='product.attributevalue', verbose_name='ویژگی\u200cها'), + ), + migrations.AlterUniqueTogether( + name='attributevalue', + unique_together={('attribute_type', 'value')}, + ), + ] diff --git a/backend/product/migrations/0026_alter_productvariant_attributes.py b/backend/product/migrations/0026_alter_productvariant_attributes.py new file mode 100644 index 0000000..d1bf5e7 --- /dev/null +++ b/backend/product/migrations/0026_alter_productvariant_attributes.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0025_alter_productvariant_attributes_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='productvariant', + name='attributes', + field=models.ManyToManyField(related_name='variant', to='product.attributevalue', verbose_name='ویژگی\u200cها'), + ), + ] diff --git a/backend/product/migrations/0027_alter_productvariant_attributes.py b/backend/product/migrations/0027_alter_productvariant_attributes.py new file mode 100644 index 0000000..f799514 --- /dev/null +++ b/backend/product/migrations/0027_alter_productvariant_attributes.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0026_alter_productvariant_attributes'), + ] + + operations = [ + migrations.AlterField( + model_name='productvariant', + name='attributes', + field=models.ManyToManyField(blank=True, related_name='variant', to='product.attributevalue', verbose_name='ویژگی\u200cها'), + ), + ] diff --git a/backend/product/migrations/0028_alter_productvariant_attributes.py b/backend/product/migrations/0028_alter_productvariant_attributes.py new file mode 100644 index 0000000..dcf388a --- /dev/null +++ b/backend/product/migrations/0028_alter_productvariant_attributes.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0027_alter_productvariant_attributes'), + ] + + operations = [ + migrations.AlterField( + model_name='productvariant', + name='attributes', + field=models.ManyToManyField(related_name='variant', to='product.attributevalue', verbose_name='ویژگی\u200cها'), + ), + ] diff --git a/backend/product/migrations/0029_alter_attributevalue_unique_together_and_more.py b/backend/product/migrations/0029_alter_attributevalue_unique_together_and_more.py new file mode 100644 index 0000000..438e9bc --- /dev/null +++ b/backend/product/migrations/0029_alter_attributevalue_unique_together_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0028_alter_productvariant_attributes'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='attributevalue', + unique_together=set(), + ), + migrations.RemoveField( + model_name='attributevalue', + name='attribute_type', + ), + ] diff --git a/backend/product/migrations/0030_rename_attributes_productvariant_product_attributes_and_more.py b/backend/product/migrations/0030_rename_attributes_productvariant_product_attributes_and_more.py new file mode 100644 index 0000000..decf2e1 --- /dev/null +++ b/backend/product/migrations/0030_rename_attributes_productvariant_product_attributes_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.2 on 2025-02-13 20:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0029_alter_attributevalue_unique_together_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='productvariant', + old_name='attributes', + new_name='product_attributes', + ), + migrations.AddField( + model_name='attributevalue', + name='attribute_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='product.attributetype'), + ), + migrations.AlterUniqueTogether( + name='attributevalue', + unique_together={('attribute_type', 'value')}, + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 1c65209..1cf00fa 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -201,7 +201,7 @@ class AttributeValue(models.Model): unique_together = ('attribute_type', 'value') def __str__(self): - return f"{self.attribute_type.name}: {self.value}" + return f"{self.attribute_type}: {self.value}" class ProductImageModel(models.Model): @@ -220,7 +220,7 @@ class ProductImageModel(models.Model): class ProductVariant(models.Model): product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول') - attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها', related_name='variant') + product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها', related_name='variant') in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود') price = models.PositiveIntegerField(default=0, verbose_name='قیمت') min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود') @@ -242,7 +242,7 @@ class ProductVariant(models.Model): verbose_name_plural = 'تنوع‌های محصول' def __str__(self): - return f"{self.product.name} - {', '.join(str(attr) for attr in self.attributes.all())}" + return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}" def get_toman_price(self, dollor_price=None): if not dollor_price: From d8f7be777261a8055283b4ba3867072481db3a3a Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 15 Feb 2025 00:43:32 +0330 Subject: [PATCH 03/22] order urls fix intendent and order item verbose name udpate --- backend/order/admin.py | 4 +-- backend/order/models.py | 6 ++--- backend/order/urls.py | 7 +++-- backend/order/views.py | 58 ++++++++++++++++++++--------------------- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/backend/order/admin.py b/backend/order/admin.py index 438d955..813088f 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin, TabularInline +from unfold.admin import ModelAdmin, TabularInline, StackedInline from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm @@ -8,7 +8,7 @@ from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField -class OrderItemModelInline(TabularInline): +class OrderItemModelInline(StackedInline): model = OrderItemModel extra = 0 max_num = 0 diff --git a/backend/order/models.py b/backend/order/models.py index 94b5396..9a5bc76 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -79,8 +79,8 @@ class OrderItemModel(models.Model): quantity = models.SmallIntegerField(verbose_name="تعداد") product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") class Meta: - verbose_name = 'محصول خریداری شده' - verbose_name_plural = 'محصولات خریداری شده' + verbose_name = 'ایتم سبد خرید' + verbose_name_plural = 'ایتم های سبد خرید' def total(self): return self.quantity * self.product.get_toman_price() @@ -88,5 +88,5 @@ class OrderItemModel(models.Model): def total_with_discount(self): return self.quantity * self.product.get_toman_price_after_discount() def __str__(self): - return f'ایتم سبد خرید محصول: {self.product} کاربر {self.order.user}' + return f'({self.product}) - ({self.order.user})' diff --git a/backend/order/urls.py b/backend/order/urls.py index c506e46..696981c 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -1,8 +1,11 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include -from .views import CartItemViews +from .views import CartItemViews, CartView urlpatterns = [ - path('cart/item/', CartItemViews.as_view(), name='add cart'), + path('cart', CartView.as_view()), + path('cart/item/', CartItemViews.as_view(), name='change-item-cart'), + path('payment', CartView.as_view()), + path('', CartView.as_view()), ] diff --git a/backend/order/views.py b/backend/order/views.py index 1f3451e..d29ced5 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -28,41 +28,41 @@ class CartItemViews(APIView): permission_classes = [IsAuthenticated] serializer_class = OrderItemSerailzier def post(self, request, pk): - product_variant = get_object_or_404(ProductVariant, pk=pk) + product_variant = get_object_or_404(ProductVariant, pk=pk) - cart_order, created = OrderModel.objects.get_or_create( - user=request.user, - status='CART' - ) - order_item, created = OrderItemModel.objects.get_or_create( - order=cart_order, - product=product_variant, - defaults={'quantity': request.data.get('quantity', 1)} - ) + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + order_item, created = OrderItemModel.objects.get_or_create( + order=cart_order, + product=product_variant, + defaults={'quantity': request.data.get('quantity', 1)} + ) - if not created: - order_item.quantity = request.data.get('quantity', 1) - order_item.save() + if not created: + order_item.quantity = request.data.get('quantity', 1) + order_item.save() - return Response({'detail': 'it did something'}, status=status.HTTP_201_CREATED) + return Response({'detail': f'تعداد ایتم {product_variant.product.name} به {request.data.get('quantity', 1)} تغییر کرد'}, status=status.HTTP_202_ACCEPTED) def delete(self, request, pk): - product_variant = get_object_or_404(ProductVariant, pk=pk) + product_variant = get_object_or_404(ProductVariant, pk=pk) - cart_order, created = OrderModel.objects.get_or_create( - user=request.user, - status='CART' - ) - # order_item, created = OrderItemModel.objects.get_or_create( - # order=cart_order, - # product=product_variant, - # defaults={'quantity': request.data.get('quantity', 1)} - # ) - order_item = get_object_or_404(OrderItemModel, order=cart_order, product=product_variant) - order_item.delete() - return Response({'detail': 'it did something related to delete'}, status=status.HTTP_204_NO_CONTENT) + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + # order_item, created = OrderItemModel.objects.get_or_create( + # order=cart_order, + # product=product_variant, + # defaults={'quantity': request.data.get('quantity', 1)} + # ) + order_item = get_object_or_404(OrderItemModel, order=cart_order, product=product_variant) + order_item.delete() + return Response({'detail': f'محصول {product_variant.product.name} از سبد خرید پاک شد'}, status=status.HTTP_204_NO_CONTENT) -class CartViews(APIView): +class CartView(APIView): def get(self, request): - pass + return Response({'detail': 'این بخش در حال توسعه می باشد تا اماده شدن این بخش به نقاشی خود ادامه دهید'}, status=status.HTTP_404_NOT_FOUND) From 81df30c806b464a9a01f72a72b07779098e97995 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 16 Feb 2025 21:42:29 +0330 Subject: [PATCH 04/22] video player and model and video guide routing and stuff --- backend/account/admin.py | 3 +- .../migrations/0011_user_video_uploader.py | 18 ++ .../0012_alter_user_video_uploader.py | 18 ++ backend/account/models.py | 1 + backend/blog/admin.py | 3 +- backend/chat/admin.py | 4 +- backend/core/settings/unfold_conf.py | 8 +- backend/core/static/visibility.png | Bin 0 -> 1545 bytes backend/core/static/visibility_off.png | Bin 0 -> 1584 bytes backend/core/urls.py | 1 + backend/home/admin.py | 37 +++- .../home/migrations/0006_learnvideomodel.py | 25 +++ ...on_alter_learnvideomodel_title_and_more.py | 29 +++ .../0008_learnvideomodel_content_type.py | 20 +++ ...0009_alter_learnvideomodel_content_type.py | 20 +++ .../migrations/0010_learnvideomodel_viewd.py | 18 ++ ...omodel_icon_alter_learnvideomodel_viewd.py | 24 +++ ...0012_alter_learnvideomodel_content_type.py | 20 +++ backend/home/models.py | 38 +++- backend/home/urls.py | 6 + backend/home/views.py | 11 +- backend/order/admin.py | 4 +- .../0005_alter_orderitemmodel_options.py | 17 ++ backend/product/admin.py | 11 +- .../templates/video_change_form_after.html | 166 ++++++++++++++++++ backend/ticket/admin.py | 4 +- .../0008_alter_ticket_ticket_category.py | 18 ++ backend/utils/admin.py | 32 +++- 28 files changed, 531 insertions(+), 25 deletions(-) create mode 100644 backend/account/migrations/0011_user_video_uploader.py create mode 100644 backend/account/migrations/0012_alter_user_video_uploader.py create mode 100644 backend/core/static/visibility.png create mode 100644 backend/core/static/visibility_off.png create mode 100644 backend/home/migrations/0006_learnvideomodel.py create mode 100644 backend/home/migrations/0007_learnvideomodel_section_alter_learnvideomodel_title_and_more.py create mode 100644 backend/home/migrations/0008_learnvideomodel_content_type.py create mode 100644 backend/home/migrations/0009_alter_learnvideomodel_content_type.py create mode 100644 backend/home/migrations/0010_learnvideomodel_viewd.py create mode 100644 backend/home/migrations/0011_learnvideomodel_icon_alter_learnvideomodel_viewd.py create mode 100644 backend/home/migrations/0012_alter_learnvideomodel_content_type.py create mode 100644 backend/home/urls.py create mode 100644 backend/order/migrations/0005_alter_orderitemmodel_options.py create mode 100644 backend/templates/video_change_form_after.html create mode 100644 backend/ticket/migrations/0008_alter_ticket_ticket_category.py diff --git a/backend/account/admin.py b/backend/account/admin.py index e293140..53c231d 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -9,7 +9,7 @@ from django.contrib.postgres.fields import ArrayField from django.contrib.auth.models import Group from unfold.forms import AdminPasswordChangeForm from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm - +from utils.admin import ModelAdmin class UserAddressInLine(TabularInline): model = UserAddressModel extra = 0 @@ -36,6 +36,7 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): fieldsets = ( ('اطلاعات شخصی', {'fields': ('first_name', 'last_name', 'profile_photo', 'password', 'gender', 'birth_date'),}), ('اطلاعات ارتباطی', {'fields': ('phone', 'email'),}), + ('دسترسی های وبسایت', {'fields': ('is_superuser', 'video_uploader'),}), ) empty_value_display = 'ثبت نشده' add_fieldsets = ( diff --git a/backend/account/migrations/0011_user_video_uploader.py b/backend/account/migrations/0011_user_video_uploader.py new file mode 100644 index 0000000..d8249f3 --- /dev/null +++ b/backend/account/migrations/0011_user_video_uploader.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-14 22:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0010_useraddressmodel_is_main'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='video_uploader', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/account/migrations/0012_alter_user_video_uploader.py b/backend/account/migrations/0012_alter_user_video_uploader.py new file mode 100644 index 0000000..425e808 --- /dev/null +++ b/backend/account/migrations/0012_alter_user_video_uploader.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-14 23:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0011_user_video_uploader'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='video_uploader', + field=models.BooleanField(default=False, help_text='اپلود کننده ی ویدیویی اموزشی پنل ادمین', verbose_name='اپلودر اموزش'), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index fc097df..0e5323b 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -50,6 +50,7 @@ class User(AbstractBaseUser, PermissionsMixin): date_joined = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبتنام') otp_hash = models.CharField(max_length=64, null=True, blank=True, verbose_name='کد یک بار مصرف') otp_expiry = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تمام شدن کد یک بار مصرف') + video_uploader = models.BooleanField(default=False, help_text='اپلود کننده ی ویدیویی اموزشی پنل ادمین', verbose_name='اپلودر اموزش') objects = UserManager() USERNAME_FIELD = 'phone' diff --git a/backend/blog/admin.py b/backend/blog/admin.py index fd74838..ea7b973 100644 --- a/backend/blog/admin.py +++ b/backend/blog/admin.py @@ -1,12 +1,11 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField - +from utils.admin import ModelAdmin @admin.register(BlogModel) class BlogModelAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/chat/admin.py b/backend/chat/admin.py index 8d83c8f..5532e04 100644 --- a/backend/chat/admin.py +++ b/backend/chat/admin.py @@ -1,12 +1,12 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin + from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField - +from utils.admin import ModelAdmin @admin.register(ProductChatModel) class ProductChatAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 3829318..934d808 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -30,7 +30,7 @@ UNFOLD = { }, - "BORDER_RADIUS": "8px", + "BORDER_RADIUS": "20px", "SHOW_HISTORY": True, "SHOW_VIEW_ON_SITE": True, "ENVIRONMENT": "core.settings.environment_callback", @@ -93,6 +93,12 @@ UNFOLD = { "icon": "dashboard", "link": reverse_lazy("admin:index"), }, + { + "title": _("آموزش استفاده از پنل"), + "icon": "school", + "link": reverse_lazy("admin:home_learnvideomodel_changelist"), + "badge": "utils.admin.new_learn_video_count", + }, { "title": _("سفارشات"), "icon": "shopping_cart", diff --git a/backend/core/static/visibility.png b/backend/core/static/visibility.png new file mode 100644 index 0000000000000000000000000000000000000000..3a7a90782270b20fecd1122ee93fca941c8d6357 GIT binary patch literal 1545 zcmbu<`#aMM00!{y*2c_nzu#&_$V%l%#B$q~9hX>c<)|g68s*Yd)1kg0*N`prpL-@J{F#=cdhNIKi zR`Q#bJ6my9_vS6_+Lb$NHR+&*Z5mf|nDMDPq1gf@ z0@A0amX;|6rjf5(+LTjf0%C=qC9=+61{c5W1WK{v>Wp?o+TCEtuZ0U}r%CG<7ks}$ zg>p$hvODQBWOFb`slfa>m7>5e@uIFDRg&5b^ z(7CGFvDbAi>kO5ZvEPvafY<3W$ek}zEe*^CXMD{ozWDv19!oR;%2ww}S2S&UuV&#{ z0wGi=npq8M&MCOFq8!fW7SbAREjXxrKzETFNPREOX!ly9&344enaFz8)db6-b&6qt z-}i~N`#T;jye~saD;brQSEkJ@Mzs-PfXeYMHa3>rtzRl$?C{BFYaNIpLq9q~|bV1*^>I;ajyd$(gle5;TT{@6^sm8LV((=%_>cyU=kQ}@V z2yruw?wrs_DN!{MrvPq8wx>lCU7;&AuH+yu!JhuUZKEHv>1F~wXMo2)K=tuHnAe8I zh4s@&apvy>fhyAFhBH&a1S9#euvY3VI)Cb+l3066p^Kl*e)IvqfJvtaVJ0d@_Pi!SyJSBguGl z)L{LPA;|+E(1hfgdqWo7RJi)*-n|Q&$zCFg3!0>j zVXxr;^W3v*M@g`zTi-y9ZX__`%f$VC3KnB8$7C48^A$&eJe&o5?y01e)Px+IPrcY? z0H|A~*o@?VT`(Uu08UW&-}EdC2;+5CbC)}YSp|Vac-{ky;=!qsw^yBRy&Pp6`MCl#oQYYu_m7R?r-yYgDtZ7GuEFoc7fV^wFpQT z^*M7`r=c*!OXkLo_Jw#;lbbAUUBFh}Kmvbwuz^is;4v2~j9d?sC9Jt?4 zQ9^@#Xnv&cB)My?+R7@@XR7autZ!g3)W)Cjk*wRCm{1My{L&j*W3AjNDerk|xBHFv z;^V(zCL+Q-VLHH{@$IwNq@mQk)$H#~XKFM?+%upA%EJVX-T@*ly$pj~o>#29HTiI- zT{;?Y51#gTI2Lm#1%v70FhB>6Lxei#ab8muWuovUDk91g-Ib{)Yi=BB6h~!)kFNRo z6_<6K9yXegwLpsnJk3^g-yimwqV@+eks8mEUAbH8xongsF^xk2R$;M(w9*^Zu;lS*RwF!UUY8pL!JjSoAJU(7JS(7ofxjrt=n2JvvN{;AwetloNv38XB poGD03P(VRoXw^$cjsKsU`&G(z(V{S)o^E|RK)`unnJ%P+e*n0W$#4Jw literal 0 HcmV?d00001 diff --git a/backend/core/static/visibility_off.png b/backend/core/static/visibility_off.png new file mode 100644 index 0000000000000000000000000000000000000000..229a8437e42445593e180ba5dad473ad8d4acfde GIT binary patch literal 1584 zcmV-02G9A4P)Px)>PbXFRCr$PoLiF9Fc3wZl`*UYs+f=21+a_xfGSuC!^*<6%Sve!TYYc09vRa4 zOk_*Cr(aTRotF4aRp2zW_vm~H0F#X%00e-^3QX=kJRkr}R$y}X;Q;|)vI3L44-W_c zlNFfUJ?sIWADyyqBOq*vU%HY4VEw%2q7T{hFWj-`3_OLAfO$)g&xpph8$7r1EyN?A)RW zZB0!Dh*We_y#uL&xQx05XqHU!G(Q09hyF35 zVIW`tcL-q!2nxWRLg>2vLIaopA(fA-MW*OpRxjC_A*~lu2@T~gAZv69OC|uFA*{Qu zB6h11F~QkNY8Mcu$N-@Fo(t+#9U%kvwAo5v@emV?mJrMykOX+I5M=yL5HG)fK7aHW zf?PWkz}lhX|AcItBJBqy3%=wAxxe(ogf9mOcV2J-V5~p46b3@JRRQfwW&P0NSV8>$ z`E1tFe4$nRBoGM!-VWIb2^J7+f97yx0^k*Y0t7nr8N9&+fd(nMVeqoGF= zf7Z4IGJbUIWi}@uR6|kK13;^VTx}u*%@e5c=O9|><9aY_i`h`J$p?;FPm7#@gdr-ZQ=rP34HwNtyO7Q}hF=-xF3#C^8kjlZbr&5N7PhXzSTI}5W-3mby$(je4^}*iQ zuK^Po0IZ2wGb}FvWKIwO%~GVJ5rfsdgY|^o0JQsW^SKIjdm%`xo9VsvJuwka zYYCxe4j>ZoRo*!V5z2*-CxMv8x#9t!@Yf?Q2!{i%w+=Id+y`rW87(^QJbTZ6?DNz|?Lz8Yf zP!j+H1bVGtcshA11g+jb8k`xx20`5~Dmu1!gDeKk?{ghaoOn}5sRHS+W2%DoCgQMC z69^eYad^m^<7(tIlwQw=9zbx3Z%V>@ARHwm4FZWX%y`J0(ERK?R|v|*AQM)pV{;Ha zA+$U_vKat&olvpl6^8tdJlSR?A_}vefaZX#jJ+HLbqni+n*vaZnIBzT z99b+)OY6;#tdV1j)2Dj!)@%qj4FI)nZ@ZZg0>DOZ7K8whxz7v;0icLqqag%#YS`Wo0sydx=m{YJ005*33W0q-C;)%}q?z_FA1l7xGXM|( zY7i{q2LMw1?Ej26?@?RD4**60xhHtU4*(7TtuBom&`L?;eFA_{xK@`1fL2Nx?-Kxw i!nL|A0JKulc%Ofo$IPA8{hg2i0000' + else: + svg = f'' + + return format_html( + svg + ) + + + @admin.register(SliderModel) class SliderAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/home/migrations/0006_learnvideomodel.py b/backend/home/migrations/0006_learnvideomodel.py new file mode 100644 index 0000000..9353a52 --- /dev/null +++ b/backend/home/migrations/0006_learnvideomodel.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.2 on 2025-02-14 21:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0005_alter_showcaseslider_description_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='LearnVideoModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=50)), + ('video', models.FileField(upload_to='learning_video/')), + ], + options={ + 'verbose_name': 'ویدیوی اموزشی', + 'verbose_name_plural': 'ویدیوی های اموزشی', + }, + ), + ] diff --git a/backend/home/migrations/0007_learnvideomodel_section_alter_learnvideomodel_title_and_more.py b/backend/home/migrations/0007_learnvideomodel_section_alter_learnvideomodel_title_and_more.py new file mode 100644 index 0000000..b7b3d98 --- /dev/null +++ b/backend/home/migrations/0007_learnvideomodel_section_alter_learnvideomodel_title_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.2 on 2025-02-14 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0006_learnvideomodel'), + ] + + operations = [ + migrations.AddField( + model_name='learnvideomodel', + name='section', + field=models.CharField(default='', max_length=40, verbose_name='بخش مربوطه'), + preserve_default=False, + ), + migrations.AlterField( + model_name='learnvideomodel', + name='title', + field=models.CharField(max_length=100, verbose_name='عنوان ویدیو'), + ), + migrations.AlterField( + model_name='learnvideomodel', + name='video', + field=models.FileField(upload_to='learning_video/', verbose_name='ویدیو'), + ), + ] diff --git a/backend/home/migrations/0008_learnvideomodel_content_type.py b/backend/home/migrations/0008_learnvideomodel_content_type.py new file mode 100644 index 0000000..ab5b2f3 --- /dev/null +++ b/backend/home/migrations/0008_learnvideomodel_content_type.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-02-14 23:12 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('home', '0007_learnvideomodel_section_alter_learnvideomodel_title_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='learnvideomodel', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Related Model'), + ), + ] diff --git a/backend/home/migrations/0009_alter_learnvideomodel_content_type.py b/backend/home/migrations/0009_alter_learnvideomodel_content_type.py new file mode 100644 index 0000000..7f1327e --- /dev/null +++ b/backend/home/migrations/0009_alter_learnvideomodel_content_type.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-02-14 23:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('home', '0008_learnvideomodel_content_type'), + ] + + operations = [ + migrations.AlterField( + model_name='learnvideomodel', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', unique=True, verbose_name='مدل مرتبط'), + ), + ] diff --git a/backend/home/migrations/0010_learnvideomodel_viewd.py b/backend/home/migrations/0010_learnvideomodel_viewd.py new file mode 100644 index 0000000..5b4b232 --- /dev/null +++ b/backend/home/migrations/0010_learnvideomodel_viewd.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-15 00:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0009_alter_learnvideomodel_content_type'), + ] + + operations = [ + migrations.AddField( + model_name='learnvideomodel', + name='viewd', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/home/migrations/0011_learnvideomodel_icon_alter_learnvideomodel_viewd.py b/backend/home/migrations/0011_learnvideomodel_icon_alter_learnvideomodel_viewd.py new file mode 100644 index 0000000..adf3697 --- /dev/null +++ b/backend/home/migrations/0011_learnvideomodel_icon_alter_learnvideomodel_viewd.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2025-02-15 07:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0010_learnvideomodel_viewd'), + ] + + operations = [ + migrations.AddField( + model_name='learnvideomodel', + name='icon', + field=models.CharField(default='', max_length=30, verbose_name='ایکون'), + preserve_default=False, + ), + migrations.AlterField( + model_name='learnvideomodel', + name='viewd', + field=models.BooleanField(default=False, verbose_name='تماشا شده'), + ), + ] diff --git a/backend/home/migrations/0012_alter_learnvideomodel_content_type.py b/backend/home/migrations/0012_alter_learnvideomodel_content_type.py new file mode 100644 index 0000000..47c18fc --- /dev/null +++ b/backend/home/migrations/0012_alter_learnvideomodel_content_type.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-02-16 17:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('home', '0011_learnvideomodel_icon_alter_learnvideomodel_viewd'), + ] + + operations = [ + migrations.AlterField( + model_name='learnvideomodel', + name='content_type', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='مدل مرتبط'), + ), + ] diff --git a/backend/home/models.py b/backend/home/models.py index d0d9a1e..857e0e0 100644 --- a/backend/home/models.py +++ b/backend/home/models.py @@ -1,6 +1,6 @@ from django.db import models from product.models import ProductModel - +from django.urls import reverse class SliderModel(models.Model): link = models.URLField(verbose_name='لینک') @@ -45,4 +45,38 @@ class ShowCaseSlider(models.Model): class Meta: verbose_name = 'مدل نمایش کیس' - verbose_name_plural = 'مدل نمایش کیس ها' \ No newline at end of file + verbose_name_plural = 'مدل نمایش کیس ها' + +from django.contrib.contenttypes.models import ContentType +class LearnVideoModel(models.Model): + title = models.CharField(max_length=100, verbose_name='عنوان ویدیو') + section = models.CharField(max_length=40, verbose_name='بخش مربوطه') + video = models.FileField(upload_to='learning_video/', verbose_name='ویدیو') + content_type = models.OneToOneField( + ContentType, + on_delete=models.CASCADE, + verbose_name='مدل مرتبط', blank=True, null=True, unique=True + ) + icon = models.CharField(max_length=30, verbose_name='ایکون') + viewd = models.BooleanField(default=False, verbose_name='تماشا شده') + @property + def section_url(self): + if not self.content_type: + return None + + app_label = self.content_type.app_label + model_name = self.content_type.model + + try: + return reverse(f'admin:{app_label}_{model_name}_changelist') + except Exception: + return None + + + + def __str__(self): + return self.title + + class Meta: + verbose_name = 'ویدیوی اموزشی' + verbose_name_plural = 'ویدیوی های اموزشی' \ No newline at end of file diff --git a/backend/home/urls.py b/backend/home/urls.py new file mode 100644 index 0000000..a406202 --- /dev/null +++ b/backend/home/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('video/view/', views.ChangeViewVideo.as_view(), name='product-chat-view'), +] \ No newline at end of file diff --git a/backend/home/views.py b/backend/home/views.py index 181d7ab..003a09a 100644 --- a/backend/home/views.py +++ b/backend/home/views.py @@ -1,10 +1,19 @@ -from django.shortcuts import render +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 .serializers import * from .models import * from rest_framework import status +from django.views import View + + +class ChangeViewVideo(View): + def get(self, request, pk): + videomodel = get_object_or_404(LearnVideoModel, pk=pk) + videomodel.viewd = not videomodel.viewd + videomodel.save() + return redirect('admin:home_learnvideomodel_changelist') class HomeView(APIView): diff --git a/backend/order/admin.py b/backend/order/admin.py index 813088f..bce84b0 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -1,12 +1,12 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin, TabularInline, StackedInline +from unfold.admin import TabularInline, StackedInline from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField - +from utils.admin import ModelAdmin class OrderItemModelInline(StackedInline): model = OrderItemModel diff --git a/backend/order/migrations/0005_alter_orderitemmodel_options.py b/backend/order/migrations/0005_alter_orderitemmodel_options.py new file mode 100644 index 0000000..308bddd --- /dev/null +++ b/backend/order/migrations/0005_alter_orderitemmodel_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-02-14 21:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0004_alter_orderitemmodel_product'), + ] + + operations = [ + migrations.AlterModelOptions( + name='orderitemmodel', + options={'verbose_name': 'ایتم سبد خرید', 'verbose_name_plural': 'ایتم های سبد خرید'}, + ), + ] diff --git a/backend/product/admin.py b/backend/product/admin.py index 5ea98c7..3cfe104 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -1,15 +1,14 @@ -from django.contrib import admin +from django.contrib import admin, messages from .models import * -from unfold.admin import ModelAdmin, TabularInline, StackedInline - +from unfold.admin import TabularInline, StackedInline +from home.models import LearnVideoModel from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField -from unfold.widgets import ( - UnfoldAdminColorInputWidget, -) +from unfold.widgets import UnfoldAdminColorInputWidget from unfold.decorators import action, display +from utils.admin import ModelAdmin @admin.register(ProductVariant) class ProductVariantAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/templates/video_change_form_after.html b/backend/templates/video_change_form_after.html new file mode 100644 index 0000000..54b4059 --- /dev/null +++ b/backend/templates/video_change_form_after.html @@ -0,0 +1,166 @@ +{% extends "admin/base_site.html" %} +{% load unfold %} +{% load i18n admin_urls static admin_modify %} + +{% block extrahead %}{{ block.super }} + + {{ media }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %} + +{% if not is_popup %} + {% block breadcrumbs %} +
+
+
    + {% url 'admin:index' as link %} + {% trans 'Home' as name %} + {% include 'unfold/helpers/breadcrumb_item.html' with link=link name=name %} + + {% url 'admin:app_list' app_label=opts.app_label as link %} + {% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.app_config.verbose_name %} + + {% if has_view_permission %} + {% url opts|admin_urlname:'changelist' as link %} + {% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.verbose_name_plural|capfirst %} + {% else %} + {% include 'unfold/helpers/breadcrumb_item.html' with link='' name=opts.verbose_name_plural|capfirst %} + {% endif %} + + {% if add %} + {% blocktranslate trimmed with name=opts.verbose_name asvar breadcrumb_name %} + Add {{ name }} + {% endblocktranslate %} + + {% include 'unfold/helpers/breadcrumb_item.html' with link='' name=breadcrumb_name %} + {% else %} + {% include 'unfold/helpers/breadcrumb_item.html' with link='' name=original|truncatewords:'18' %} + {% endif %} +
+
+
+ {% endblock %} +{% endif %} + +{% block nav-global %}{% spaceless %} + {% if change and not is_popup %} + {% block object-tools-items %} + {% change_form_object_tools %} + {% endblock %} + {% endif %} +{% endspaceless %}{% endblock %} + +{% block nav-global-side %} + {% if has_add_permission %} + {% include "unfold/helpers/add_link.html" %} + {% endif %} +{% endblock %} + +{% block content %} +
+ {% block form_before %}{% endblock %} +
+
+

+ {{original.icon}} + اموزش بخش {{original.content_type}} +

+
+ +
+ {% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %} + {% component "unfold/components/button.html" with href=original.section_url %} + نمایش بخش مربوطه + {% endcomponent %} + {% endcomponent %} +
+
+ {% if adminform.model_admin.change_form_outer_before_template %} + {% include adminform.model_admin.change_form_outer_before_template %} + {% endif %} + +
+ {% if original and original.video %} +

+ + + {{original.content_type}} + + آموزش بخش +

+
+ +
+ {% else %} +

no video available.

+ {% endif %} +
+ + + + {% if request.user.video_uploader %} +
+ {% csrf_token %} + + {% if adminform.model_admin.change_form_before_template %} + {% include adminform.model_admin.change_form_before_template %} + {% endif %} + + {% block form_top %}{% endblock %} + +
+ {% if is_popup %} + + {% endif %} + + {% if to_field %} + + {% endif %} + + {% include "unfold/helpers/messages/errornote.html" with errors=errors %} + {% include "unfold/helpers/messages/error.html" with errors=adminform.form.non_field_errors %} + + {% block field_sets %} + {% for fieldset in adminform %} + {% if "tab" not in fieldset.classes %} + {% include 'admin/includes/fieldset.html' %} + {% endif %} + {% endfor %} + + {% include "unfold/helpers/fieldsets_tabs.html" %} + {% endblock %} + + {% block after_field_sets %}{% endblock %} + + {% block inline_field_sets %} + {% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} + {% endfor %} + {% endblock %} + + {% block after_related_objects %}{% endblock %} + + {% if adminform.model_admin.change_form_after_template %} + {% include adminform.model_admin.change_form_after_template %} + {% endif %} + + {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} + + {% block admin_change_form_document_ready %} + + {% endblock %} + + {% prepopulated_fields_js %} +
+
+ {% endif %} + {% if adminform.model_admin.change_form_outer_after_template %} + {% include adminform.model_admin.change_form_outer_after_template %} + {% endif %} + + {% block form_after %}{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/backend/ticket/admin.py b/backend/ticket/admin.py index 121c965..5ae80c6 100644 --- a/backend/ticket/admin.py +++ b/backend/ticket/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin, TabularInline +from unfold.admin import TabularInline from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm @@ -10,7 +10,7 @@ from unfold.contrib.filters.admin import ( ChoicesDropdownFilter, MultipleChoicesDropdownFilter, ) - +from utils.admin import ModelAdmin diff --git a/backend/ticket/migrations/0008_alter_ticket_ticket_category.py b/backend/ticket/migrations/0008_alter_ticket_ticket_category.py new file mode 100644 index 0000000..e700fbb --- /dev/null +++ b/backend/ticket/migrations/0008_alter_ticket_ticket_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-14 21:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0007_alter_ticket_ticket_category'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='ticket_category', + field=models.CharField(choices=[('finance_and_accounting', 'مالی و حسابداری'), ('user_profile', 'پروفایل کاربری'), ('order_tracking', 'پیگیری سفارش'), ('authentication', 'احراز هویت'), ('product', 'محصول'), ('bug_and_error_reporting', 'اعلام باگ و خطا در وبسایت'), ('other', 'سایر')], max_length=30, verbose_name='دسته بندی تیکت'), + ), + ] diff --git a/backend/utils/admin.py b/backend/utils/admin.py index 61cabce..67786f3 100644 --- a/backend/utils/admin.py +++ b/backend/utils/admin.py @@ -1,7 +1,7 @@ from order.models import OrderModel from product.models import DollorModel, CommentModel from ticket.models import Ticket - +from home.models import LearnVideoModel def admin_pending_count(request): pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count() @@ -15,4 +15,32 @@ def comment_count(request): return CommentModel.objects.filter(review_status='not_reviwed').count() def new_ticket_count(request): - return Ticket.objects.filter(status__in=['open', 'in_progress']).count() \ No newline at end of file + return Ticket.objects.filter(status__in=['open', 'in_progress']).count() + +def new_learn_video_count(request): + return LearnVideoModel.objects.filter(viewd=False).count() + + +from django.contrib import admin, messages +from unfold.admin import ModelAdmin +from home.models import LearnVideoModel +from import_export.admin import ImportExportModelAdmin +from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm +from unfold.decorators import action, display +from django.shortcuts import redirect +from django.contrib.contenttypes.models import ContentType + + +class ModelAdmin(ModelAdmin): + actions_list = ['redirect_to_learn'] + @action(description=f"چگونگی استفاده این بخش") + def redirect_to_learn(self, request): + content_type = ContentType.objects.get_for_model(self.model) + try: + learn_video = LearnVideoModel.objects.get( + content_type=content_type, + ) + return redirect(f'/admin/home/learnvideomodel/{learn_video.id}/change/') + except Exception as e: + messages.error(request, f"برای بخش {content_type} ویدیویی اپلود نشده است") + return redirect(f'/admin/home/learnvideomodel/') \ No newline at end of file From 4fb1d2bdb98b734986a8fd05c54224d3d03a05f6 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 16 Feb 2025 22:24:53 +0330 Subject: [PATCH 05/22] learn viedo delete and add permison --- backend/home/admin.py | 14 ++++++++++++-- backend/order/views.py | 27 ++++++++++++++------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/backend/home/admin.py b/backend/home/admin.py index 818b2cd..e64e483 100644 --- a/backend/home/admin.py +++ b/backend/home/admin.py @@ -10,6 +10,9 @@ from unfold.decorators import action, display from django.utils.html import format_html from unfold.decorators import display from utils.admin import ModelAdmin +from unfold.contrib.filters.admin import ChoicesDropdownFilter + + @admin.register(ShowCaseSlider) class ShowCaseSliderAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm @@ -28,16 +31,23 @@ class ShowCaseSliderAdmin(ModelAdmin, ImportExportModelAdmin): @admin.register(LearnVideoModel) class LearnVideoAdmin(UnfoldModelAdmin): list_display = ['title', 'section', 'display_viewd'] - search_fields = ['title', 'section'] + search_fields = ['title', 'section', 'content_type__app_label'] + # autocomplete_fields = ['section'] compressed_fields = False - list_filter = ['section'] + list_filter = ['viewd', 'section'] warn_unsaved_form = True + list_filter_submit = True change_form_template = 'video_change_form_after.html' formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } + def has_delete_permission(self, request, obj=None): + return request.user.video_uploader + + def has_add_permission(self, request, obj=None): + return request.user.video_uploader @display(description='دیده شده') diff --git a/backend/order/views.py b/backend/order/views.py index d29ced5..924f1a3 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -29,22 +29,23 @@ class CartItemViews(APIView): serializer_class = OrderItemSerailzier def post(self, request, pk): product_variant = get_object_or_404(ProductVariant, pk=pk) + response = 'محصول با موفقیت به سبد خرید اضافه شد' + quantity = request.data.get('quantity', 1) + if product_variant.in_stock < quantity: + quantity = product_variant.in_stock + response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد' - cart_order, created = OrderModel.objects.get_or_create( - user=request.user, - status='CART' - ) - order_item, created = OrderItemModel.objects.get_or_create( - order=cart_order, - product=product_variant, - defaults={'quantity': request.data.get('quantity', 1)} - ) + cart_order, created = OrderModel.objects.get_or_create(user=request.user, status='CART') + order_item, created = OrderItemModel.objects.get_or_create(order=cart_order, product=product_variant, defaults={'quantity': quantity}) - if not created: - order_item.quantity = request.data.get('quantity', 1) + if not created and order_item.quantity: + order_item.quantity = quantity order_item.save() - - return Response({'detail': f'تعداد ایتم {product_variant.product.name} به {request.data.get('quantity', 1)} تغییر کرد'}, status=status.HTTP_202_ACCEPTED) + if not order_item.quantity: + order_item.delete() + return Response({'detail': response, 'count': quantity}, status=status.HTTP_202_ACCEPTED) + + def delete(self, request, pk): product_variant = get_object_or_404(ProductVariant, pk=pk) From ad022cdf6a361ff61efbd1c98f44b2d69c474c95 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:11:50 +0330 Subject: [PATCH 06/22] push notificantion test --- backend/.env.local | 4 +- backend/account/admin.py | 10 ++- .../migrations/0013_pushsubscription.py | 25 +++++++ backend/account/models.py | 65 ++++++++++++++++++- backend/account/serializers.py | 8 ++- backend/account/urls.py | 1 + backend/account/views.py | 18 ++++- backend/core/settings/base.py | 2 +- backend/order/models.py | 12 +++- 9 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 backend/account/migrations/0013_pushsubscription.py diff --git a/backend/.env.local b/backend/.env.local index 064d665..657f88f 100644 --- a/backend/.env.local +++ b/backend/.env.local @@ -26,4 +26,6 @@ SITE_HEADER = 'فروشگاه هی ملز' ACCESS_TOKEN_LIFETIME = 5000 REFRESH_TOKEN_LIFETIME = 5000 -SMS_API_KEY = '' \ No newline at end of file +SMS_API_KEY = '' + +VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4' \ No newline at end of file diff --git a/backend/account/admin.py b/backend/account/admin.py index 53c231d..d4e4460 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -87,4 +87,12 @@ class AddressAdmin(ModelAdmin, ImportExportModelAdmin): } def address_display(self, obj): return obj.address[0:35] + '...' - address_display.short_description = 'ادرس' \ No newline at end of file + address_display.short_description = 'ادرس' + + +@admin.register(PushSubscription) +class PushSubscription(ModelAdmin, ImportExportModelAdmin): + import_form_class = ImportForm + export_form_class = ExportForm + compressed_fields = True + warn_unsaved_form = True \ No newline at end of file diff --git a/backend/account/migrations/0013_pushsubscription.py b/backend/account/migrations/0013_pushsubscription.py new file mode 100644 index 0000000..0d069e0 --- /dev/null +++ b/backend/account/migrations/0013_pushsubscription.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.2 on 2025-02-16 20:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0012_alter_user_video_uploader'), + ] + + operations = [ + migrations.CreateModel( + name='PushSubscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('endpoint', models.TextField()), + ('keys', models.JSONField()), + ('created_at', models.DateField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 0e5323b..910673a 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -7,7 +7,7 @@ from django.utils import timezone from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken import hashlib from django.contrib import admin - +from django.conf import settings class UserManager(BaseUserManager): def create_user(self, phone, password=None): if not phone: @@ -135,4 +135,65 @@ class UserAddressModel(models.Model): class Meta: verbose_name = 'ادرس کاربر' - verbose_name_plural = 'ادرس های کاربر' \ No newline at end of file + verbose_name_plural = 'ادرس های کاربر' + + +import os +import json +from pywebpush import webpush, WebPushException + +class PushSubscription(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + endpoint = models.TextField() + keys = models.JSONField() + created_at = models.DateField(auto_now_add=True) + + def __str__(self): + return f'{self.user} push' + + def send_notif(self, title, body): + payload = { + "title": title, + "body": body, + "icon": '' + } + + try: + webpush( + subscription_info={ + "endpoint": self.endpoint, + "keys": self.keys + }, + data=json.dumps(payload), + vapid_private_key=settings.VAPID_PRIVATE_KEY, + vapid_claims={ + "sub": "mailto:admin@example.com" + } + ) + except WebPushException as ex: + print("Failed to send notification:", ex) + + @classmethod + def send_group_notification(cls, user, title, body): + payload = { + "title": title, + "body": body, + "icon": '' + } + + subscriptions = PushSubscription.objects.filter(user=user) + for sub in subscriptions: + try: + webpush( + subscription_info={ + "endpoint": sub.endpoint, + "keys": sub.keys + }, + data=json.dumps(payload), + vapid_private_key=settings.VAPID_PRIVATE_KEY, + vapid_claims={ + "sub": "mailto:admin@example.com" + } + ) + except WebPushException as ex: + print(f"Failed to send notification to {sub.user}:", ex) \ No newline at end of file diff --git a/backend/account/serializers.py b/backend/account/serializers.py index 5c17c8f..9927dd4 100644 --- a/backend/account/serializers.py +++ b/backend/account/serializers.py @@ -23,4 +23,10 @@ class UserAddressSerializer(serializers.ModelSerializer): user = self.context['request'].user if not user.is_authenticated: raise serializers.ValidationError("You must be logged in to perform this action.") - return data \ No newline at end of file + return data + + +class PushSubscriptionSerializer(serializers.ModelSerializer): + class Meta: + model = PushSubscription + fields = ('endpoint', 'keys') diff --git a/backend/account/urls.py b/backend/account/urls.py index 4b70939..c5ebbf4 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -12,4 +12,5 @@ urlpatterns = [ path('address/delete/', views.DeleteAddressView.as_view(), name='delete-address'), path('address/list', views.GetUserAddressesView.as_view(), name='list-addresses'), path('address/', views.GetIDUserAddressView.as_view(), name='get-ID-address'), + path('subscribe', views.SubscribeView.as_view(), name='subscibe') ] \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index d48aa2a..29aac75 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -2,7 +2,7 @@ from django.shortcuts import render from rest_framework.views import APIView from rest_framework import generics, permissions, status from rest_framework.response import Response -from .serializers import ProfileSerializer, UserAddressSerializer, CustomTokenObtainPairSerializer +from .serializers import * from .models import UserAddressModel, User from rest_framework.permissions import IsAuthenticated, AllowAny from drf_spectacular.utils import extend_schema, OpenApiParameter @@ -171,4 +171,18 @@ class GetIDUserAddressView(generics.RetrieveAPIView): permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - return UserAddressModel.objects.filter(user=self.request.user) \ No newline at end of file + return UserAddressModel.objects.filter(user=self.request.user) + + + +class SubscribeView(APIView): + serializer_class = PushSubscriptionSerializer + def post(self, request): + push_ser = self.serializer_class(data=request.data) + if push_ser.is_valid(): + PushSubscription.objects.update_or_create( + user=request.user, + defaults=(push_ser.validated_data) + ) + return Response(status=status.HTTP_201_CREATED) + return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 9e47af9..237019d 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -29,7 +29,7 @@ DEBUG = True BASE_DIR = Path(__file__).resolve().parent.parent.parent - +VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY') # Application definition diff --git a/backend/order/models.py b/backend/order/models.py index 9a5bc76..a3f3617 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -45,7 +45,17 @@ class OrderModel(models.Model): # def total_without_tax(self): # return sum(item.total() for item in self.items.all()) - + def save(self, *args, **kwargs): + if self.status == 'POSTED': + try: + push_object = PushSubscription.objects.get(user=self.user) + except: + print('object not found') + try: + push_object.send_notif('سفارش شما به پست شده تغییر کرد', 'سفارش شما به پست شده تغییر کرد') + except: + print('didnt send') + super().save(*args, **kwargs) def total_with_discount(self): total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) From 3973a2af42daac8ab5e7a1240920d6113c391a80 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:15:10 +0330 Subject: [PATCH 07/22] requeiments for web push --- backend/requirements.txt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index d8019ef..51240cc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,7 +8,7 @@ attrs==24.2.0 certifi==2024.8.30 cffi==1.17.1 charset-normalizer==3.3.2 -cryptography==43.0.3 +cryptography==44.0.1 defusedxml==0.8.0rc2 diff-match-patch==20230430 distro==1.9.0 @@ -24,7 +24,9 @@ django-unfold==0.48.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 djoser==2.3.1 +dnspython==2.7.0 drf-spectacular==0.27.2 +email_validator==2.2.0 factory_boy==3.3.1 Faker==28.4.1 frozenlist==1.4.1 @@ -33,6 +35,7 @@ ghasedak_sms==1.0.3 ghasedakpack==0.1.13 gnupg==2.3.1 h11==0.14.0 +http_ece==1.2.1 httpagentparser==1.9.5 httpcore==1.0.5 httpx==0.27.2 @@ -50,16 +53,18 @@ openai==1.58.1 pillow==10.4.0 psutil==6.0.0 psycopg2-binary==2.9.10 +py-vapid==1.9.2 pycparser==2.22 -pydantic==2.10.4 +pydantic==2.10.6 pydantic_core==2.27.2 -PyJWT==2.9.0 +PyJWT==2.10.1 pyTelegramBotAPI==4.23.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-telegram-bot==21.6 python3-openid==3.2.0 pytz==2024.2 +pywebpush==2.0.3 PyYAML==6.0.2 referencing==0.35.1 requests==2.32.3 From 2ab584e77fb6ab4eaa1ecc788e7991ff1c8be9a4 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:18:46 +0330 Subject: [PATCH 08/22] add unique for endpoint and date time filed and added aud for model --- ...er_pushsubscription_created_at_and_more.py | 23 +++++++++++++++++++ backend/account/models.py | 7 +++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 backend/account/migrations/0014_alter_pushsubscription_created_at_and_more.py diff --git a/backend/account/migrations/0014_alter_pushsubscription_created_at_and_more.py b/backend/account/migrations/0014_alter_pushsubscription_created_at_and_more.py new file mode 100644 index 0000000..118d513 --- /dev/null +++ b/backend/account/migrations/0014_alter_pushsubscription_created_at_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2025-02-16 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0013_pushsubscription'), + ] + + operations = [ + migrations.AlterField( + model_name='pushsubscription', + name='created_at', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='pushsubscription', + name='endpoint', + field=models.TextField(unique=True), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 910673a..c1b59b5 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -144,9 +144,9 @@ from pywebpush import webpush, WebPushException class PushSubscription(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - endpoint = models.TextField() + endpoint = models.TextField(unique=True) keys = models.JSONField() - created_at = models.DateField(auto_now_add=True) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f'{self.user} push' @@ -192,7 +192,8 @@ class PushSubscription(models.Model): data=json.dumps(payload), vapid_private_key=settings.VAPID_PRIVATE_KEY, vapid_claims={ - "sub": "mailto:admin@example.com" + "sub": "mailto:admin@example.com", + 'aud': 'https://mamalizz-cooked.vercel.app' } ) except WebPushException as ex: From b4dd12716d62e56f349d8f637ed93cb3b9fc1b1b Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:24:26 +0330 Subject: [PATCH 09/22] add random user --- backend/account/views.py | 2 +- backend/templates/video_change_form_after.html | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/account/views.py b/backend/account/views.py index 29aac75..69e81dd 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -181,7 +181,7 @@ class SubscribeView(APIView): push_ser = self.serializer_class(data=request.data) if push_ser.is_valid(): PushSubscription.objects.update_or_create( - user=request.user, + user=User.objects.all().first(), defaults=(push_ser.validated_data) ) return Response(status=status.HTTP_201_CREATED) diff --git a/backend/templates/video_change_form_after.html b/backend/templates/video_change_form_after.html index 54b4059..95283bb 100644 --- a/backend/templates/video_change_form_after.html +++ b/backend/templates/video_change_form_after.html @@ -60,6 +60,9 @@ {% block content %}
{% block form_before %}{% endblock %} + + {% if original.title %} +

@@ -76,6 +79,7 @@ {% endcomponent %}

+{% endif %} {% if adminform.model_admin.change_form_outer_before_template %} {% include adminform.model_admin.change_form_outer_before_template %} {% endif %} From 81051831bca7169372e92c4ddd39ee3ea35d51d9 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:26:11 +0330 Subject: [PATCH 10/22] video add new remove commponent --- backend/templates/video_change_form_after.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/templates/video_change_form_after.html b/backend/templates/video_change_form_after.html index 95283bb..fe1a60d 100644 --- a/backend/templates/video_change_form_after.html +++ b/backend/templates/video_change_form_after.html @@ -84,6 +84,9 @@ {% include adminform.model_admin.change_form_outer_before_template %} {% endif %} + {% if original.title %} + +
{% if original and original.video %}

@@ -102,7 +105,7 @@

no video available.

{% endif %}

- + {% endif %} {% if request.user.video_uploader %} From d6390118cec842156403aed336d1bce8aa776efd Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:30:28 +0330 Subject: [PATCH 11/22] debug push sub --- backend/order/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/order/models.py b/backend/order/models.py index a3f3617..d41f476 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -1,8 +1,10 @@ from django.db import models -from account.models import User, UserAddressModel +from account.models import User, UserAddressModel, PushSubscription from product.models import ProductModel, ProductVariant from django.utils import timezone from .execptions import DiscountNotAvailableError + + class DiscountCode(models.Model): name = models.CharField(max_length=50, verbose_name='کد تخفیف') percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد') From e118f5c7d284c6e36689b0ee65ad195e11112b44 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:34:21 +0330 Subject: [PATCH 12/22] for all status --- backend/order/models.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/order/models.py b/backend/order/models.py index d41f476..86b2ae5 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -48,15 +48,14 @@ class OrderModel(models.Model): # def total_without_tax(self): # return sum(item.total() for item in self.items.all()) def save(self, *args, **kwargs): - if self.status == 'POSTED': - try: - push_object = PushSubscription.objects.get(user=self.user) - except: - print('object not found') - try: - push_object.send_notif('سفارش شما به پست شده تغییر کرد', 'سفارش شما به پست شده تغییر کرد') - except: - print('didnt send') + try: + push_object = PushSubscription.objects.get(user=self.user) + except: + print('object not found') + try: + push_object.send_notif(f'سفارش شما به {self.status} تغییر کرد', f'سفارش شما به {self.status} تغییر کرد') + except: + print('didnt send') super().save(*args, **kwargs) def total_with_discount(self): From 2c4988d2c62e1b7a08e23c07965acbc0641395db Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:52:29 +0330 Subject: [PATCH 13/22] image for notif --- backend/account/models.py | 4 ++-- backend/order/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/account/models.py b/backend/account/models.py index c1b59b5..ceb9366 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -151,11 +151,11 @@ class PushSubscription(models.Model): def __str__(self): return f'{self.user} push' - def send_notif(self, title, body): + def send_notif(self, title, body, icon=''): payload = { "title": title, "body": body, - "icon": '' + "icon": icon } try: diff --git a/backend/order/models.py b/backend/order/models.py index 86b2ae5..27bc19f 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -1,6 +1,6 @@ from django.db import models from account.models import User, UserAddressModel, PushSubscription -from product.models import ProductModel, ProductVariant +from product.models import ProductModel, ProductVariant, ProductImageModel from django.utils import timezone from .execptions import DiscountNotAvailableError @@ -53,7 +53,7 @@ class OrderModel(models.Model): except: print('object not found') try: - push_object.send_notif(f'سفارش شما به {self.status} تغییر کرد', f'سفارش شما به {self.status} تغییر کرد') + push_object.send_notif(f'سفارش شما به {self.status} تغییر کرد', f'سفارش شما به {self.status} تغییر کرد', ProductImageModel.objects.all().first().image.url) except: print('didnt send') super().save(*args, **kwargs) From e1c7204bd2a3a651bf3737d111fb7d0ce7002cab Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 00:59:15 +0330 Subject: [PATCH 14/22] icon url --- backend/account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/account/models.py b/backend/account/models.py index ceb9366..a9c39dd 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -155,7 +155,7 @@ class PushSubscription(models.Model): payload = { "title": title, "body": body, - "icon": icon + "icon": 'https://api.heymlz.com' + icon } try: From 72932e6272033a458d12bdc6832b58916e7b5ce0 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 01:02:23 +0330 Subject: [PATCH 15/22] a --- backend/account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/account/models.py b/backend/account/models.py index a9c39dd..246f225 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -151,7 +151,7 @@ class PushSubscription(models.Model): def __str__(self): return f'{self.user} push' - def send_notif(self, title, body, icon=''): + def send_notif(self, title, body, icon): payload = { "title": title, "body": body, From a25c27f65f3e41ec5bd766c86660a37bb7db8920 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 19:27:55 +0330 Subject: [PATCH 16/22] fix display of order notif --- backend/order/models.py | 2 +- backend/templates/admin/base_site.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/order/models.py b/backend/order/models.py index 27bc19f..01cf4d1 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -53,7 +53,7 @@ class OrderModel(models.Model): except: print('object not found') try: - push_object.send_notif(f'سفارش شما به {self.status} تغییر کرد', f'سفارش شما به {self.status} تغییر کرد', ProductImageModel.objects.all().first().image.url) + push_object.send_notif(f'سفارش شما به {self.get_status_display()} تغییر کرد', f'سفارش شما به {self.get_status_display()} تغییر کرد', ProductImageModel.objects.all().first().image.url) except: print('didnt send') super().save(*args, **kwargs) diff --git a/backend/templates/admin/base_site.html b/backend/templates/admin/base_site.html index d7593ad..9f2851d 100644 --- a/backend/templates/admin/base_site.html +++ b/backend/templates/admin/base_site.html @@ -7,7 +7,7 @@ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} -

{{ site_header|default:_('Django administration') }}

+

مدیریت هی ملز

{% endblock %} {% block extrahead %} From c46f2cb762f22e1c3175d6283f54347c5f0fd844 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 19:37:25 +0330 Subject: [PATCH 17/22] title pwa notif --- backend/account/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/account/models.py b/backend/account/models.py index 246f225..7d055ec 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -153,11 +153,11 @@ class PushSubscription(models.Model): def send_notif(self, title, body, icon): payload = { - "title": title, + "title": 'فروشگاه هی ملز', "body": body, "icon": 'https://api.heymlz.com' + icon } - + print(payload) try: webpush( subscription_info={ @@ -176,7 +176,7 @@ class PushSubscription(models.Model): @classmethod def send_group_notification(cls, user, title, body): payload = { - "title": title, + "title": 'فروشگاه هی ملز', "body": body, "icon": '' } From 0fa7c0494649452fe3ef5e508842ea65a36fe98b Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 21:19:08 +0330 Subject: [PATCH 18/22] test notif image --- backend/account/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/account/models.py b/backend/account/models.py index 7d055ec..6512684 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -155,7 +155,8 @@ class PushSubscription(models.Model): payload = { "title": 'فروشگاه هی ملز', "body": body, - "icon": 'https://api.heymlz.com' + icon + "icon": 'https://api.heymlz.com' + icon, + "image": 'https://api.heymlz.com' + icon, } print(payload) try: From 96cfcf1fcb143452a600aa26308a08a48997d407 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Mon, 17 Feb 2025 22:40:44 +0330 Subject: [PATCH 19/22] persion datetime and serializer ticket udpate --- backend/core/settings/base.py | 22 ++++++++++++++ .../0006_alter_ordermodel_created_at.py | 19 ++++++++++++ ...ename_created_at_ordermodel_created_at1.py | 18 ++++++++++++ ...ename_created_at1_ordermodel_created_at.py | 18 ++++++++++++ .../0009_alter_ordermodel_created_at.py | 19 ++++++++++++ backend/order/models.py | 5 ++-- backend/order/serializers.py | 9 ++++-- .../ticket/migrations/0009_ticket_order.py | 20 +++++++++++++ ...ted_at_alter_ticket_created_at_and_more.py | 29 +++++++++++++++++++ backend/ticket/models.py | 11 +++++-- backend/ticket/serializers.py | 8 +++-- 11 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 backend/order/migrations/0006_alter_ordermodel_created_at.py create mode 100644 backend/order/migrations/0007_rename_created_at_ordermodel_created_at1.py create mode 100644 backend/order/migrations/0008_rename_created_at1_ordermodel_created_at.py create mode 100644 backend/order/migrations/0009_alter_ordermodel_created_at.py create mode 100644 backend/ticket/migrations/0009_ticket_order.py create mode 100644 backend/ticket/migrations/0010_alter_message_created_at_alter_ticket_created_at_and_more.py diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 237019d..8d4307b 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'rest_framework_simplejwt.token_blacklist', 'rest_framework.authtoken', 'import_export', + "django_jalali", # custom apps 'product', 'account', @@ -67,8 +68,29 @@ INSTALLED_APPS = [ 'order', 'home', 'blog', + ] +JALALI_SETTINGS = { + # JavaScript static files for the admin Jalali date widget + "ADMIN_JS_STATIC_FILES": [ + "admin/jquery.ui.datepicker.jalali/scripts/jquery-1.10.2.min.js", + "admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.core.js", + "admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.datepicker-cc.js", + "admin/jquery.ui.datepicker.jalali/scripts/calendar.js", + "admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.datepicker-cc-fa.js", + "admin/main.js", + ], + # CSS static files for the admin Jalali date widget + "ADMIN_CSS_STATIC_FILES": { + "all": [ + "admin/jquery.ui.datepicker.jalali/themes/base/jquery-ui.min.css", + "admin/css/main.css", + ] + }, +} + + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', "whitenoise.middleware.WhiteNoiseMiddleware", diff --git a/backend/order/migrations/0006_alter_ordermodel_created_at.py b/backend/order/migrations/0006_alter_ordermodel_created_at.py new file mode 100644 index 0000000..a615cbe --- /dev/null +++ b/backend/order/migrations/0006_alter_ordermodel_created_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-02-17 18:15 + +import django_jalali.db.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0005_alter_orderitemmodel_options'), + ] + + operations = [ + migrations.AlterField( + model_name='ordermodel', + name='created_at', + field=django_jalali.db.models.jDateField(blank=True, null=True, verbose_name='تاریخ سفارش'), + ), + ] diff --git a/backend/order/migrations/0007_rename_created_at_ordermodel_created_at1.py b/backend/order/migrations/0007_rename_created_at_ordermodel_created_at1.py new file mode 100644 index 0000000..8d5beb3 --- /dev/null +++ b/backend/order/migrations/0007_rename_created_at_ordermodel_created_at1.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-17 18:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0006_alter_ordermodel_created_at'), + ] + + operations = [ + migrations.RenameField( + model_name='ordermodel', + old_name='created_at', + new_name='created_at1', + ), + ] diff --git a/backend/order/migrations/0008_rename_created_at1_ordermodel_created_at.py b/backend/order/migrations/0008_rename_created_at1_ordermodel_created_at.py new file mode 100644 index 0000000..afa9db2 --- /dev/null +++ b/backend/order/migrations/0008_rename_created_at1_ordermodel_created_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-17 18:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0007_rename_created_at_ordermodel_created_at1'), + ] + + operations = [ + migrations.RenameField( + model_name='ordermodel', + old_name='created_at1', + new_name='created_at', + ), + ] diff --git a/backend/order/migrations/0009_alter_ordermodel_created_at.py b/backend/order/migrations/0009_alter_ordermodel_created_at.py new file mode 100644 index 0000000..cfe3434 --- /dev/null +++ b/backend/order/migrations/0009_alter_ordermodel_created_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-02-17 18:46 + +import django_jalali.db.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0008_rename_created_at1_ordermodel_created_at'), + ] + + operations = [ + migrations.AlterField( + model_name='ordermodel', + name='created_at', + field=django_jalali.db.models.jDateField(blank=True, null=True, verbose_name='تاریخ ثبت سفارش'), + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index 01cf4d1..570351b 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -3,7 +3,7 @@ from account.models import User, UserAddressModel, PushSubscription from product.models import ProductModel, ProductVariant, ProductImageModel from django.utils import timezone from .execptions import DiscountNotAvailableError - +from django_jalali.db import models as jmodels class DiscountCode(models.Model): name = models.CharField(max_length=50, verbose_name='کد تخفیف') @@ -23,6 +23,7 @@ class DiscountCode(models.Model): class OrderModel(models.Model): + objects = jmodels.jManager() STATUS_CHOICES = [ ('CART', 'در سبد خرید'), ('ADMIN_PENDING', 'در انتظار تایید'), @@ -34,7 +35,7 @@ class OrderModel(models.Model): ] user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر') address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس') - created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ سفارش") + created_at = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش") is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت") discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="وضعیت سفارش") diff --git a/backend/order/serializers.py b/backend/order/serializers.py index 2a7d8e8..b9337c0 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -1,9 +1,14 @@ from rest_framework import serializers -from .models import OrderItemModel +from .models import OrderItemModel, OrderModel class OrderItemSerailzier(serializers.ModelSerializer): class Meta: model = OrderItemModel fields = "__all__" - read_only_fields = ('order', 'product') \ No newline at end of file + read_only_fields = ('order', 'product') + +class OrderModelSerializer(serializers.ModelSerializer): + class Meta: + model = OrderModel + fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code'] \ No newline at end of file diff --git a/backend/ticket/migrations/0009_ticket_order.py b/backend/ticket/migrations/0009_ticket_order.py new file mode 100644 index 0000000..47bd507 --- /dev/null +++ b/backend/ticket/migrations/0009_ticket_order.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-02-17 18:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0006_alter_ordermodel_created_at'), + ('ticket', '0008_alter_ticket_ticket_category'), + ] + + operations = [ + migrations.AddField( + model_name='ticket', + name='order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='order.ordermodel'), + ), + ] diff --git a/backend/ticket/migrations/0010_alter_message_created_at_alter_ticket_created_at_and_more.py b/backend/ticket/migrations/0010_alter_message_created_at_alter_ticket_created_at_and_more.py new file mode 100644 index 0000000..a680ddf --- /dev/null +++ b/backend/ticket/migrations/0010_alter_message_created_at_alter_ticket_created_at_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.2 on 2025-02-17 18:46 + +import django_jalali.db.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0009_ticket_order'), + ] + + operations = [ + migrations.AlterField( + model_name='message', + name='created_at', + field=django_jalali.db.models.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در'), + ), + migrations.AlterField( + model_name='ticket', + name='created_at', + field=django_jalali.db.models.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در'), + ), + migrations.AlterField( + model_name='ticket', + name='updated_at', + field=django_jalali.db.models.jDateTimeField(auto_now=True, verbose_name='اپدیت شده در'), + ), + ] diff --git a/backend/ticket/models.py b/backend/ticket/models.py index c6d7fcb..b59682b 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -1,7 +1,10 @@ from django.db import models from account.models import User +from order.models import OrderModel +from django_jalali.db import models as jmodels class Ticket(models.Model): + objects = jmodels.jManager() STATUS_CHOICES = [ ('open', 'باز'), ('in_progress', 'در حال پردازش'), @@ -22,8 +25,9 @@ class Ticket(models.Model): customer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tickets", verbose_name='کاربر') admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_tickets", verbose_name='ادمین') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name='وضعیت تیکت') - created_at = models.DateTimeField(auto_now_add=True, verbose_name='ساخته شده در') - updated_at = models.DateTimeField(auto_now=True, verbose_name='اپدیت شده در') + created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در') + updated_at = jmodels.jDateTimeField(auto_now=True, verbose_name='اپدیت شده در') + order = models.ForeignKey(OrderModel ,blank=True, null=True, on_delete=models.SET_NULL) def __str__(self): return self.subject @@ -35,10 +39,11 @@ class Ticket(models.Model): class Message(models.Model): + objects = jmodels.jManager() ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="messages", verbose_name='تیکت') sender = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='ارسال کننده') content = models.TextField(verbose_name='محتوای پیام') - created_at = models.DateTimeField(auto_now_add=True, verbose_name='ساخته شده در') + created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در') def __str__(self): return f"Message by {self.sender.full_name} on {self.ticket.subject}" diff --git a/backend/ticket/serializers.py b/backend/ticket/serializers.py index d9afdcb..362d18e 100644 --- a/backend/ticket/serializers.py +++ b/backend/ticket/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from .models import Ticket, Message - +from django.utils.timezone import localtime +from account.serializers import ProfileSerializer class MessageSerializer(serializers.ModelSerializer): class Meta: model = Message @@ -8,7 +9,8 @@ class MessageSerializer(serializers.ModelSerializer): class TicketSerializer(serializers.ModelSerializer): messages = MessageSerializer(many=True, read_only=True) - + admin = ProfileSerializer(read_only=True) class Meta: model = Ticket - fields = '__all__' \ No newline at end of file + exclude = ('customer', ) + read_only_fields = ('status', 'admin', ) \ No newline at end of file From 7617431f2bd40735e6b6f40c96516d878a833df8 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 18 Feb 2025 21:42:14 +0330 Subject: [PATCH 20/22] debug product serializer --- backend/core/urls.py | 4 +- backend/core/views.py | 55 ++++++++++++++++++++++- backend/order/admin.py | 2 +- backend/product/serializers.py | 2 +- backend/templates/admin/fake_login.html | 60 +++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 backend/templates/admin/fake_login.html diff --git a/backend/core/urls.py b/backend/core/urls.py index 59fbd0f..b461d9c 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -7,6 +7,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView,TokenRefreshView from product import views from account.views import CustomTokenObtainPairView from home.views import HomeView +from .views import FakeAdminLoginView urlpatterns = [ @@ -17,7 +18,8 @@ urlpatterns = [ path('home', HomeView.as_view()), path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), - path('admin/', admin.site.urls), + path('admin/', FakeAdminLoginView.as_view()), # Fake admin + path('secret-admin/', admin.site.urls), # Real admin path('schema/', SpectacularAPIView.as_view(), name='schema'), # path('comment/', views.CommentView.as_view(), name='comment-list'), path('products/', include('product.urls')), diff --git a/backend/core/views.py b/backend/core/views.py index 0a361be..229b4a5 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -10,12 +10,13 @@ from unfold.views import UnfoldModelAdminViewMixin from order.models import OrderModel from ticket.models import Ticket - +import json def dashboard_callback(request, context): + print(context) pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count() open_tickets_count = Ticket.objects.filter(status__in=['open', 'in_progress']).count() context.update(random_data()) @@ -192,3 +193,55 @@ def random_data(): ], } return response + + + + + + + + + + + + + +# views.py +from django.contrib.auth.views import LoginView +from django.contrib.admin.sites import site as admin_site +from django.contrib import messages +from django.shortcuts import redirect +from django.views.generic import RedirectView, TemplateView +from unfold.views import UnfoldModelAdminViewMixin +class FakeAdminLoginView(LoginView): + template_name = "admin/fake_login.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + print(self.get_form()) + context.update({ + **admin_site.each_context(self.request), + "title": "Log in", + "site_header": admin_site.site_header, + "form": self.get_form(), + }) + return context + + def form_valid(self, form): + # Log the honeypot attempt + username = form.cleaned_data.get("username") + ip = self.request.META.get("REMOTE_ADDR") + print(f"Honeypot triggered! IP: {ip}, Username: {username}") + + # Add error message and redirect back to fake login + messages.error(self.request, "Please enter a correct شماره تماس and password. Note that both fields may be case-sensitive.") + return redirect('/admin') + + def form_invalid(self, form): + # Log the honeypot attempt + username = form.cleaned_data.get("username") + ip = self.request.META.get("REMOTE_ADDR") + print(f"Honeypot triggered! IP: {ip}, Username: {username}") + + # Add error message and redirect back to fake login + messages.error(self.request, "Please enter a correct شماره تماس and password. Note that both fields may be case-sensitive.") + return redirect('/admin') \ No newline at end of file diff --git a/backend/order/admin.py b/backend/order/admin.py index bce84b0..3626c1a 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -28,7 +28,7 @@ class OrderAdmin(ModelAdmin, ImportExportModelAdmin): list_filter = ['is_paid', 'status'] list_display = ['user', 'is_paid', 'status', 'discount_code', 'address'] - + readonly_fields = ('created_at',) compressed_fields = True warn_unsaved_form = True diff --git a/backend/product/serializers.py b/backend/product/serializers.py index f3bc036..3794822 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -44,7 +44,7 @@ class ProductImageSerailizer(serializers.ModelSerializer): class ProductVariantSerialzier(serializers.ModelSerializer): - attributes = AttributeValueSerialzier(many=True) + product_attributes = AttributeValueSerialzier(many=True) price = serializers.SerializerMethodField() in_pack_items = InPackItemsSerialzier(many=True) images = ProductImageSerailizer(many=True) diff --git a/backend/templates/admin/fake_login.html b/backend/templates/admin/fake_login.html new file mode 100644 index 0000000..cb1fb89 --- /dev/null +++ b/backend/templates/admin/fake_login.html @@ -0,0 +1,60 @@ +{% extends "admin/login.html" %} +{% load i18n static %} + +{% block content %} +
+
+
+ {% include "unfold/helpers/logo.html" %} +
+ + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} + asdfadsf + +
+ + + required + > +
+ + +
+ + +
+ + + +
+
+
+{% endblock %} \ No newline at end of file From 4e825711d38c44c45f76d1b5cd5d9882e5187e51 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 18 Feb 2025 22:23:45 +0330 Subject: [PATCH 21/22] hoeny pad admin --- backend/core/views.py | 93 ++++++++---- backend/templates/admin/fake_login.html | 190 +++++++++++++++++------- 2 files changed, 203 insertions(+), 80 deletions(-) diff --git a/backend/core/views.py b/backend/core/views.py index 229b4a5..d673079 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -16,7 +16,7 @@ import json def dashboard_callback(request, context): - print(context) + pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count() open_tickets_count = Ticket.objects.filter(status__in=['open', 'in_progress']).count() context.update(random_data()) @@ -213,35 +213,74 @@ from django.contrib import messages from django.shortcuts import redirect from django.views.generic import RedirectView, TemplateView from unfold.views import UnfoldModelAdminViewMixin -class FakeAdminLoginView(LoginView): - template_name = "admin/fake_login.html" - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - print(self.get_form()) - context.update({ - **admin_site.each_context(self.request), - "title": "Log in", +from django.views import View +from django.shortcuts import render, redirect, get_object_or_404 + +# class FakeAdminLoginView(View): +# template_name = "admin/fake_login.html" +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) + +# context.update({ +# **admin_site.each_context(self.request), +# "title": "Log in", +# "site_header": admin_site.site_header, +# "form": self.get_form(), +# }) +# return context + +# def post(self): +# # Log the honeypot attempt +# username = form.cleaned_data.get("username") +# ip = self.request.META.get("REMOTE_ADDR") +# print(f"Honeypot triggered! IP: {ip}, Username: {username}") + +# # Add error message and redirect back to fake login +# messages.error(self.request, "Please enter a correct شماره تماس and password. Note that both fields may be case-sensitive.") +# return redirect('/admin') + + + +from django.views import View +from django.contrib import messages +from django.shortcuts import render, redirect +from django.contrib.admin.sites import site as admin_site + +class FakeAdminLoginView(View): + def get_unfold_context(self, request): + """Replicates Unfold's template context""" + return { + "site_title": admin_site.site_title, "site_header": admin_site.site_header, - "form": self.get_form(), - }) + "unfold": { + "title": admin_site.site_title, + "navigation": [], + "theme": "light", # Match your Unfold config + "color_scheme": "light", # Match your Unfold config + } + } + + def get_context(self, request): + """Combine Unfold context with default admin context""" + context = admin_site.each_context(request) + context.update(self.get_unfold_context(request)) return context - def form_valid(self, form): - # Log the honeypot attempt - username = form.cleaned_data.get("username") - ip = self.request.META.get("REMOTE_ADDR") - print(f"Honeypot triggered! IP: {ip}, Username: {username}") + def get(self, request): + # Log empty attempt (optional) + ip = request.META.get("REMOTE_ADDR") + print(f"Honeypot page accessed from IP: {ip}") - # Add error message and redirect back to fake login - messages.error(self.request, "Please enter a correct شماره تماس and password. Note that both fields may be case-sensitive.") - return redirect('/admin') + return render(request, 'admin/fake_login.html', self.get_context(request)) - def form_invalid(self, form): - # Log the honeypot attempt - username = form.cleaned_data.get("username") - ip = self.request.META.get("REMOTE_ADDR") - print(f"Honeypot triggered! IP: {ip}, Username: {username}") + def post(self, request): + username = request.POST.get("username") + password = request.POST.get("password") # Never actually used + ip = request.META.get("REMOTE_ADDR") - # Add error message and redirect back to fake login - messages.error(self.request, "Please enter a correct شماره تماس and password. Note that both fields may be case-sensitive.") - return redirect('/admin') \ No newline at end of file + print(f"Honeypot triggered! IP: {ip}, Username: {username}") + 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.") + + # Redirect back to fake login page with context + return render(request, 'admin/fake_login.html', self.get_context(request)) \ No newline at end of file diff --git a/backend/templates/admin/fake_login.html b/backend/templates/admin/fake_login.html index cb1fb89..0c28127 100644 --- a/backend/templates/admin/fake_login.html +++ b/backend/templates/admin/fake_login.html @@ -1,60 +1,144 @@ -{% extends "admin/login.html" %} +{% extends 'unfold/layouts/skeleton.html' %} + {% load i18n static %} -{% block content %} -
-
-
- {% include "unfold/helpers/logo.html" %} +{% block extrastyle %} + {{ block.super }} + {{ form.media }} +{% endblock %} + +{% block bodyclass %}{{ block.super }}bg-base-50 login dark:bg-base-900{% endblock %} + +{% block usertools %}{% endblock %} + +{% block nav-global %}{% endblock %} + +{% block nav-sidebar %}{% endblock %} + +{% block content_title %}{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block title %} + {{ title }} | {{ site_title }} +{% endblock %} + +{% block base %} +
+
+
+

+ {% trans 'Welcome back to' %} + {{ site_title|default:_('Django site admin') }} +

+ + {% include "unfold/helpers/messages.html" %} + + {% if form.errors or form.non_field_errors %} +
+ {% include "unfold/helpers/messages/errornote.html" with errors=form.errors %} + + {% include "unfold/helpers/messages/error.html" with errors=form.non_field_errors %} + + {% if user.is_authenticated %} + {% blocktranslate trimmed asvar message %} + You are authenticated as {{ username }}, but are not authorized to + access this page. Would you like to login to a different account? + {% endblocktranslate %} + + {% include "unfold/helpers/messages/error.html" with error=message %} + {% endif %} +
+ {% endif %} + + {% block login_before %}{% endblock %} + +
+ {% csrf_token %} + +
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + + + +
+ + + + + + + +
+ +
+ + {% if password_reset_url %} + + {% endif %} +
+ + {% block login_after %}{% endblock %}
- {% if messages %} -
- {% for message in messages %} - {{ message }} - {% endfor %} -
- {% endif %} +
+ {% if site_url %} + + arrow_back {% trans 'Return to site' %} + + {% endif %} -
- {% csrf_token %} - asdfadsf - -
- - - required - > -
- - -
- - -
- - - -
+ {% if not theme %} + {% include "unfold/helpers/theme_switch.html" %} + {% endif %} +
+ + {% if image %} + + {% endif %}
-{% endblock %} \ No newline at end of file +{% endblock %} From db02d3d3bfa57f3ee2a30636254f44640e2e900f Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 18 Feb 2025 22:48:44 +0330 Subject: [PATCH 22/22] honey pad view fixed dynamic link for video link --- backend/templates/admin/fake_login.html | 41 ++++++++++--------------- backend/utils/admin.py | 6 ++-- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/backend/templates/admin/fake_login.html b/backend/templates/admin/fake_login.html index 0c28127..a965bfb 100644 --- a/backend/templates/admin/fake_login.html +++ b/backend/templates/admin/fake_login.html @@ -2,10 +2,7 @@ {% load i18n static %} -{% block extrastyle %} - {{ block.super }} - {{ form.media }} -{% endblock %} + {% block bodyclass %}{{ block.super }}bg-base-50 login dark:bg-base-900{% endblock %} @@ -19,17 +16,21 @@ {% block breadcrumbs %}{% endblock %} +{% block extrastyle %}{{ block.super }} + +{% endblock %} + {% block title %} - {{ title }} | {{ site_title }} + پنل مدیریت هی ملز {% endblock %} {% block base %} -
+

- {% trans 'Welcome back to' %} - {{ site_title|default:_('Django site admin') }} + خوش امدید به + پنل مدیریت هی ملز

{% include "unfold/helpers/messages.html" %} @@ -67,12 +68,6 @@ - - - - - -
@@ -80,7 +75,7 @@
- {% if site_url %} - - arrow_back {% trans 'Return to site' %} + + arrow_forward بازگشت به وبسایت - {% endif %} {% if not theme %} {% include "unfold/helpers/theme_switch.html" %} @@ -136,9 +129,9 @@
- {% if image %} - {% endblock %} diff --git a/backend/utils/admin.py b/backend/utils/admin.py index 67786f3..a86239c 100644 --- a/backend/utils/admin.py +++ b/backend/utils/admin.py @@ -27,7 +27,7 @@ from home.models import LearnVideoModel from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.decorators import action, display -from django.shortcuts import redirect +from django.shortcuts import redirect,reverse from django.contrib.contenttypes.models import ContentType @@ -40,7 +40,7 @@ class ModelAdmin(ModelAdmin): learn_video = LearnVideoModel.objects.get( content_type=content_type, ) - return redirect(f'/admin/home/learnvideomodel/{learn_video.id}/change/') + return redirect(reverse("admin:home_learnvideomodel_change", args=[learn_video.pk])) except Exception as e: messages.error(request, f"برای بخش {content_type} ویدیویی اپلود نشده است") - return redirect(f'/admin/home/learnvideomodel/') \ No newline at end of file + return redirect("admin:home_learnvideomodel_changelist") \ No newline at end of file