merge
This commit is contained in:
@@ -27,3 +27,5 @@ ACCESS_TOKEN_LIFETIME = 5000
|
||||
REFRESH_TOKEN_LIFETIME = 5000
|
||||
|
||||
SMS_API_KEY = ''
|
||||
|
||||
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
]
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
@@ -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': 'ویدیوی های اموزشی',
|
||||
},
|
||||
),
|
||||
]
|
||||
+29
@@ -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
@@ -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 = 'ویدیوی های اموزشی'
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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})'
|
||||
|
||||
|
||||
@@ -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']
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
+28
@@ -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')},
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
),
|
||||
]
|
||||
+29
@@ -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='اپدیت شده در'),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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")
|
||||
Reference in New Issue
Block a user