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 e293140..d4e4460 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 = ( @@ -86,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/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/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/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 fc097df..6512684 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: @@ -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' @@ -134,4 +135,67 @@ 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(unique=True) + keys = models.JSONField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f'{self.user} push' + + def send_notif(self, title, body, icon): + payload = { + "title": 'فروشگاه هی ملز', + "body": body, + "icon": 'https://api.heymlz.com' + icon, + "image": 'https://api.heymlz.com' + icon, + } + print(payload) + 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": 'فروشگاه هی ملز', + "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", + 'aud': 'https://mamalizz-cooked.vercel.app' + } + ) + 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..69e81dd 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=User.objects.all().first(), + 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/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/base.py b/backend/core/settings/base.py index 9e47af9..8d4307b 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 @@ -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/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 0000000..3a7a907 Binary files /dev/null and b/backend/core/static/visibility.png differ diff --git a/backend/core/static/visibility_off.png b/backend/core/static/visibility_off.png new file mode 100644 index 0000000..229a843 Binary files /dev/null and b/backend/core/static/visibility_off.png differ diff --git a/backend/core/urls.py b/backend/core/urls.py index b5e2f05..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')), @@ -25,6 +27,8 @@ urlpatterns = [ path('chat/', include('chat.urls')), path('tickets/', include('ticket.urls')), path('blogs/', include('blog.urls')), + path('order/', include('order.urls')), + path('home/', include('home.urls')), path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] diff --git a/backend/core/views.py b/backend/core/views.py index 0a361be..d673079 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): + 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,94 @@ 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 +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, + "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 get(self, request): + # Log empty attempt (optional) + ip = request.META.get("REMOTE_ADDR") + print(f"Honeypot page accessed from IP: {ip}") + + return render(request, 'admin/fake_login.html', self.get_context(request)) + + def post(self, request): + username = request.POST.get("username") + password = request.POST.get("password") # Never actually used + ip = request.META.get("REMOTE_ADDR") + + 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/home/admin.py b/backend/home/admin.py index 57df957..e64e483 100644 --- a/backend/home/admin.py +++ b/backend/home/admin.py @@ -1,12 +1,16 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin +from unfold.admin import ModelAdmin as UnfoldModelAdmin 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.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) @@ -24,6 +28,41 @@ class ShowCaseSliderAdmin(ModelAdmin, ImportExportModelAdmin): } } +@admin.register(LearnVideoModel) +class LearnVideoAdmin(UnfoldModelAdmin): + list_display = ['title', 'section', 'display_viewd'] + search_fields = ['title', 'section', 'content_type__app_label'] + # autocomplete_fields = ['section'] + compressed_fields = False + 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='دیده شده') + def display_viewd(self, instance): + if instance.viewd: + svg = f'' + 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 438d955..3626c1a 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -1,14 +1,14 @@ from django.contrib import admin from .models import * -from unfold.admin import ModelAdmin, TabularInline +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(TabularInline): +class OrderItemModelInline(StackedInline): model = OrderItemModel extra = 0 max_num = 0 @@ -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/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/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/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 f7efb32..570351b 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -1,7 +1,9 @@ from django.db import models -from account.models import User, UserAddressModel -from product.models import ProductModel +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='کد تخفیف') @@ -21,6 +23,7 @@ class DiscountCode(models.Model): class OrderModel(models.Model): + objects = jmodels.jManager() STATUS_CHOICES = [ ('CART', 'در سبد خرید'), ('ADMIN_PENDING', 'در انتظار تایید'), @@ -32,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="وضعیت سفارش") @@ -43,13 +46,24 @@ 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 save(self, *args, **kwargs): + try: + push_object = PushSubscription.objects.get(user=self.user) + except: + print('object not found') + try: + 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) 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 +73,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,15 +89,16 @@ 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 = 'محصولات خریداری شده' + verbose_name = 'ایتم سبد خرید' + verbose_name_plural = 'ایتم های سبد خرید' def total(self): return self.quantity * self.product.get_toman_price() 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..b9337c0 --- /dev/null +++ b/backend/order/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from .models import OrderItemModel, OrderModel + + +class OrderItemSerailzier(serializers.ModelSerializer): + class Meta: + model = OrderItemModel + fields = "__all__" + 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/order/urls.py b/backend/order/urls.py new file mode 100644 index 0000000..696981c --- /dev/null +++ b/backend/order/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include +from .views import CartItemViews, CartView + +urlpatterns = [ + 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 91ea44a..924f1a3 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -1,3 +1,69 @@ 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) + 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': quantity}) + + if not created and order_item.quantity: + order_item.quantity = quantity + order_item.save() + 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) + + 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 CartView(APIView): + def get(self, request): + return Response({'detail': 'این بخش در حال توسعه می باشد تا اماده شدن این بخش به نقاشی خود ادامه دهید'}, status=status.HTTP_404_NOT_FOUND) diff --git a/backend/product/admin.py b/backend/product/admin.py index 43b905d..3cfe104 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -1,21 +1,20 @@ -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): 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 +104,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: 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/requirements.txt b/backend/requirements.txt index 37e397b..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 @@ -19,11 +19,14 @@ 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 +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 @@ -32,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 @@ -49,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 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 %} diff --git a/backend/templates/admin/fake_login.html b/backend/templates/admin/fake_login.html new file mode 100644 index 0000000..a965bfb --- /dev/null +++ b/backend/templates/admin/fake_login.html @@ -0,0 +1,137 @@ +{% extends 'unfold/layouts/skeleton.html' %} + +{% load i18n static %} + + + +{% 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 extrastyle %}{{ block.super }} + +{% endblock %} + +{% block title %} + پنل مدیریت هی ملز +{% endblock %} + +{% block base %} +
+
+
+

+ خوش امدید به + پنل مدیریت هی ملز +

+ + {% 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 %} +
+ +
+ + arrow_forward بازگشت به وبسایت + + + {% if not theme %} + {% include "unfold/helpers/theme_switch.html" %} + {% endif %} +
+
+ + + + +
+{% endblock %} diff --git a/backend/templates/video_change_form_after.html b/backend/templates/video_change_form_after.html new file mode 100644 index 0000000..fe1a60d --- /dev/null +++ b/backend/templates/video_change_form_after.html @@ -0,0 +1,173 @@ +{% 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 %} + + {% if original.title %} + +
+
+

+ {{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 %} +
+
+{% endif %} + {% if adminform.model_admin.change_form_outer_before_template %} + {% include adminform.model_admin.change_form_outer_before_template %} + {% endif %} + + {% if original.title %} + + +
+ {% if original and original.video %} +

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

+
+ +
+ {% else %} +

no video available.

+ {% endif %} +
+ {% 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 21fd491..5ae80c6 100644 --- a/backend/ticket/admin.py +++ b/backend/ticket/admin.py @@ -1,11 +1,18 @@ 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 from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField +from unfold.contrib.filters.admin import ( + ChoicesDropdownFilter, + MultipleChoicesDropdownFilter, +) +from utils.admin import ModelAdmin + + 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/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/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 75600c4..b59682b 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -1,20 +1,33 @@ 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', 'در حال پردازش'), ('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='وضعیت تیکت') - 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 @@ -26,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 diff --git a/backend/utils/admin.py b/backend/utils/admin.py index 61cabce..a86239c 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,reverse +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(reverse("admin:home_learnvideomodel_change", args=[learn_video.pk])) + except Exception as e: + messages.error(request, f"برای بخش {content_type} ویدیویی اپلود نشده است") + return redirect("admin:home_learnvideomodel_changelist") \ No newline at end of file