This commit is contained in:
Mamalizz
2025-02-18 23:42:53 +03:30
65 changed files with 1466 additions and 69 deletions
+2
View File
@@ -27,3 +27,5 @@ ACCESS_TOKEN_LIFETIME = 5000
REFRESH_TOKEN_LIFETIME = 5000
SMS_API_KEY = ''
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
+10 -1
View File
@@ -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 = (
@@ -87,3 +88,11 @@ class AddressAdmin(ModelAdmin, ImportExportModelAdmin):
def address_display(self, obj):
return obj.address[0:35] + '...'
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
@@ -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),
),
]
@@ -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='اپلودر اموزش'),
),
]
@@ -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)),
],
),
]
@@ -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),
),
]
+65 -1
View File
@@ -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'
@@ -135,3 +136,66 @@ class UserAddressModel(models.Model):
class Meta:
verbose_name = 'ادرس کاربر'
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)
+6
View File
@@ -24,3 +24,9 @@ class UserAddressSerializer(serializers.ModelSerializer):
if not user.is_authenticated:
raise serializers.ValidationError("You must be logged in to perform this action.")
return data
class PushSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = PushSubscription
fields = ('endpoint', 'keys')
+1
View File
@@ -12,4 +12,5 @@ urlpatterns = [
path('address/delete/<int:pk>', views.DeleteAddressView.as_view(), name='delete-address'),
path('address/list', views.GetUserAddressesView.as_view(), name='list-addresses'),
path('address/<int:pk>', views.GetIDUserAddressView.as_view(), name='get-ID-address'),
path('subscribe', views.SubscribeView.as_view(), name='subscibe')
]
+15 -1
View File
@@ -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
@@ -172,3 +172,17 @@ class GetIDUserAddressView(generics.RetrieveAPIView):
def get_queryset(self):
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)
+1 -2
View File
@@ -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):
+2 -2
View File
@@ -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):
+23 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+5 -1
View File
@@ -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/<int:pk>', 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'),
]
+93 -1
View File
@@ -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))
+41 -2
View File
@@ -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'<a href="/home/video/view/{instance.id}"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24"><path fill="green" d="M12 16q1.875 0 3.188-1.312T16.5 11.5t-1.312-3.187T12 7T8.813 8.313T7.5 11.5t1.313 3.188T12 16m0-1.8q-1.125 0-1.912-.788T9.3 11.5t.788-1.912T12 8.8t1.913.788t.787 1.912t-.787 1.913T12 14.2m0 4.8q-3.65 0-6.65-2.037T1 11.5q1.35-3.425 4.35-5.462T12 4t6.65 2.038T23 11.5q-1.35 3.425-4.35 5.463T12 19m0-2q2.825 0 5.188-1.487T20.8 11.5q-1.25-2.525-3.613-4.012T12 6T6.813 7.488T3.2 11.5q1.25 2.525 3.613 4.013T12 17"/></svg></a>'
else:
svg = f'<a href="/home/video/view/{instance.id}"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24"><path fill="#c30009" d="m19.1 21.9l-3.5-3.45q-.875.275-1.775.413T12 19q-3.35 0-6.125-1.8t-4.35-4.75q-.125-.225-.187-.462t-.063-.488t.063-.488t.187-.462q.55-.975 1.175-1.9T4.15 7L2.075 4.9Q1.8 4.625 1.8 4.213t.3-.713q.275-.275.7-.275t.7.275l17 17q.275.275.288.688t-.288.712q-.275.275-.7.275t-.7-.275M12 16q.275 0 .525-.025t.5-.1l-5.4-5.4q-.075.25-.1.5T7.5 11.5q0 1.875 1.313 3.188T12 16m0-12q3.35 0 6.138 1.813t4.362 4.762q.125.2.188.438t.062.487t-.05.488t-.175.437q-.475.925-1.062 1.75t-1.313 1.55q-.35.35-.825.325t-.825-.375l-2-2q-.175-.175-.225-.413t.025-.487q.1-.325.15-.625t.05-.65q0-1.875-1.312-3.187T12 7q-.35 0-.65.05t-.625.15q-.25.075-.5.025T9.8 7l-.825-.825q-.475-.475-.312-1.1t.787-.8q.625-.125 1.263-.2T12 4m1.975 5.65q.275.325.462.713t.238.812q.025.2-.15.275t-.325-.075l-2.05-2.05Q12 9.175 12.088 9t.287-.175q.475.05.875.263t.725.562"/></svg></a>'
return format_html(
svg
)
@admin.register(SliderModel)
class SliderAdmin(ModelAdmin, ImportExportModelAdmin):
@@ -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': 'ویدیوی های اموزشی',
},
),
]
@@ -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='ویدیو'),
),
]
@@ -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'),
),
]
@@ -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='مدل مرتبط'),
),
]
@@ -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),
),
]
@@ -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='تماشا شده'),
),
]
@@ -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='مدل مرتبط'),
),
]
+35 -1
View File
@@ -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='لینک')
@@ -46,3 +46,37 @@ class ShowCaseSlider(models.Model):
class Meta:
verbose_name = 'مدل نمایش کیس'
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 = 'ویدیوی های اموزشی'
+6
View File
@@ -0,0 +1,6 @@
from django.urls import path
from . import views
urlpatterns = [
path('video/view/<int:pk>', views.ChangeViewVideo.as_view(), name='product-chat-view'),
]
+10 -1
View File
@@ -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):
+4 -4
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
class DiscountNotAvailableError(Exception):
pass
@@ -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='محصول'),
),
]
@@ -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': 'ایتم های سبد خرید'},
),
]
@@ -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='تاریخ سفارش'),
),
]
@@ -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',
),
]
@@ -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',
),
]
@@ -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='تاریخ ثبت سفارش'),
),
]
+27 -12
View File
@@ -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})'
+14
View File
@@ -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']
+11
View File
@@ -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/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
path('payment', CartView.as_view()),
path('', CartView.as_view()),
]
+67 -1
View File
@@ -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)
+7 -8
View File
@@ -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 = ['']
@@ -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(),
),
]
@@ -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')},
),
]
@@ -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ها'),
),
]
@@ -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ها'),
),
]
@@ -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ها'),
),
]
@@ -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',
),
]
@@ -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')},
),
]
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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)
+10 -4
View File
@@ -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
+1 -1
View File
@@ -7,7 +7,7 @@
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
<h1 id="site-name"><a href="{% url 'admin:index' %}">مدیریت هی ملز</a></h1>
{% endblock %}
{% block extrahead %}
+137
View File
@@ -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 }}<link rel="stylesheet" type="text/css" href="{% static 'override.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'fonts.css' %}" />
{% endblock %}
{% block title %}
پنل مدیریت هی ملز
{% endblock %}
{% block base %}
<div dir="rtl" id="page" class="flex min-h-screen">
<div class="flex flex-grow items-center justify-center mx-auto px-4 relative">
<div class="w-full sm:w-96">
<h1 class="font-semibold mb-10">
<span class="block text-font-important-light dark:text-font-important-dark">خوش امدید به </span>
<span class="block text-primary-600 text-xl dark:text-primary-500">پنل مدیریت هی ملز</span>
</h1>
{% include "unfold/helpers/messages.html" %}
{% if form.errors or form.non_field_errors %}
<div class="flex flex-col gap-4 mb-8 *:mb-0">
{% 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 %}
</div>
{% endif %}
{% block login_before %}{% endblock %}
<form action="{{ app_path }}" method="post" id="login-form">
{% csrf_token %}
<div class="flex flex-col group mb-6 last:mb-4">
<label for="id_username" class="block text-font-important-light dark:text-font-important-dark font-semibold mb-2">
شماره تماس
<span class="text-red-600">*</span>
</label>
<input type="text" name="username" autofocus="" autocapitalize="none" autocomplete="username" maxlength="12" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full" required="" id="id_username">
</div>
<div class="flex flex-col group mb-6 last:mb-4">
<label for="id_password" class="block text-font-important-light dark:text-font-important-dark font-semibold mb-2">
رمز
<span class="text-red-600">*</span>
</label>
<input type="password" name="password" autocomplete="current-password" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full" required="" id="id_password">
</div>
<div class="submit-row">
<button type="submit" class="bg-primary-600 border border-transparent flex flex-row font-semibold group items-center justify-center py-2 rounded text-sm text-white w-full">
ورود
</button>
</div>
{% if password_reset_url %}
<div class="password-reset-link">
<a href="{{ password_reset_url }}" class="border border-base-200 font-medium hidden mt-4 px-3 py-2 rounded text-center text-sm text-base-500 transition-all w-full hover:bg-base-50 lg:block lg:w-auto dark:border-base-700 dark:text-font-default-dark dark:hover:text-base-200 dark:hover:bg-base-900">
{% translate 'Forgotten your password or username?' %}
</a>
</div>
{% endif %}
</form>
{% block login_after %}{% endblock %}
</div>
<div class="absolute flex flex-row items-center justify-between left-0 m-4 right-0 top-0">
<a href="https://heymlz.com" class="flex font-medium items-center text-sm text-primary-600 dark:text-primary-500">
<span class="material-symbols-outlined mr-2">arrow_forward</span> بازگشت به وبسایت
</a>
{% if not theme %}
{% include "unfold/helpers/theme_switch.html" %}
{% endif %}
</div>
</div>
<div class="bg-cover flex-grow hidden max-w-3xl xl:max-w-4xl xl:block" style="background-image: url('/shop_static/favicon.png')">
</div>
</div>
{% endblock %}
@@ -0,0 +1,173 @@
{% extends "admin/base_site.html" %}
{% load unfold %}
{% load i18n admin_urls static admin_modify %}
{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
{{ media }}
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="px-4 lg:px-8">
<div class="container mb-6 mx-auto -my-3 lg:mb-12">
<ul class="flex flex-wrap">
{% 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 %}
</ul>
</div>
</div>
{% 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 %}
<div id="content-main" class="flex flex-col gap-4">
{% block form_before %}{% endblock %}
{% if original.title %}
<div dir='rtl' class="bg-base-50 border border-base-200 border-dashed flex flex-col gap-4 p-4 rounded dark:bg-white/[.02] dark:border-base-700 lg:flex-row lg:justify-between w-full shrink-0 lg:items-center" style="justify-content: space-between;">
<div class="flex flex-col lg:flex-row lg:items-center">
<h2 class="font-semibold text-font-important-light text-base dark:text-font-important-dark flex items-center">
<span class="material-symbols-outlined md-18 mr-3 w-4.5 align-middle">{{original.icon}}</span>
<span class="align-middle" style="margin-right: 8px;">اموزش بخش {{original.content_type}}</span>
</h2>
</div>
<div class="flex lg:flex-row lg:items-center">
{% 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 %}
</div>
</div>
{% endif %}
{% if adminform.model_admin.change_form_outer_before_template %}
{% include adminform.model_admin.change_form_outer_before_template %}
{% endif %}
{% if original.title %}
<div class="border border-base-200 border-dashed mb-4 p-3 rounded dark:border-base-700 gap-1.5">
{% if original and original.video %}
<h3 style="display: flex; justify-content: center; align-items: center; width: 100%; padding: 5px 0;">
<a href="{{original.section_url}}" style="color: rgb(var(--color-primary-500)); margin-right: 5px;">
{{original.content_type}}
</a>
آموزش بخش
</h3>
<div style="display: flex;justify-content: center;align-items: center;width: 100%;padding: 25px 0px;">
<video controls style="border-radius: var(--border-radius,6px);width: 100%;max-width: 800px;">
<source src="{{ original.video.url }}" type="video/mp4">
</video>
</div>
{% else %}
<p>no video available.</p>
{% endif %}
</div>
{% endif %}
{% if request.user.video_uploader %}
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" {% if adminform.model_admin.warn_unsaved_form %}class="warn-unsaved-form"{% endif %} novalidate>
{% csrf_token %}
{% if adminform.model_admin.change_form_before_template %}
{% include adminform.model_admin.change_form_before_template %}
{% endif %}
{% block form_top %}{% endblock %}
<div>
{% if is_popup %}
<input type="hidden" name="{{ is_popup_var }}" value="1">
{% endif %}
{% if to_field %}
<input type="hidden" name="{{ to_field_var }}" value="{{ 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 %}
<script id="django-admin-form-add-constants" src="{% static 'admin/js/change_form.js' %}"{% if adminform and add %} data-model-name="{{ opts.model_name }}"{% endif %} async></script>
{% endblock %}
{% prepopulated_fields_js %}
</div>
</form>
{% endif %}
{% if adminform.model_admin.change_form_outer_after_template %}
{% include adminform.model_admin.change_form_outer_after_template %}
{% endif %}
{% block form_after %}{% endblock %}
</div>
{% endblock %}
+13 -5
View File
@@ -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}
@@ -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,
),
]
@@ -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='دسته بندی تیکت'),
),
]
@@ -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='دسته بندی تیکت'),
),
]
@@ -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='دسته بندی تیکت'),
),
]
@@ -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'),
),
]
@@ -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='اپدیت شده در'),
),
]
+18 -4
View File
@@ -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}"
+5 -3
View File
@@ -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__'
exclude = ('customer', )
read_only_fields = ('status', 'admin', )
+29 -1
View File
@@ -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()
@@ -16,3 +16,31 @@ def comment_count(request):
def new_ticket_count(request):
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")