This commit is contained in:
Mamalizz
2025-02-27 20:51:12 +03:30
28 changed files with 573 additions and 239 deletions
+2
View File
@@ -29,3 +29,5 @@ REFRESH_TOKEN_LIFETIME = 5000
SMS_API_KEY = ''
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
OPENAI_API_KEY = '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'
+179 -147
View File
@@ -1,5 +1,4 @@
from dotenv import load_dotenv
# from http.cookiejar import debug
from pathlib import Path
from datetime import timedelta
import os
@@ -7,15 +6,21 @@ from django.templatetags.static import static
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
# Load environment variables
load_dotenv(".env.local")
# ==============================================================================
# General Configuration
# ==============================================================================
DOMAIN = os.getenv("DOMAIN")
API_DOMAIN = os.getenv("API_DOMAIN")
OPENAI_API_KEY = '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'
# TODO update telegram bot token
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
# TODO update email bullshit
# API Keys and Tokens
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
VAPID_PRIVATE_KEY = os.getenv("VAPID_PRIVATE_KEY")
# Email Configuration
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND")
EMAIL_HOST = os.getenv("EMAIL_HOST")
EMAIL_PORT = 587
@@ -24,17 +29,16 @@ EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
# Security and Debugging
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = True
BASE_DIR = Path(__file__).resolve().parent.parent.parent
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY')
# Application definition
# ==============================================================================
# Application Definition
# ==============================================================================
INSTALLED_APPS = [
# unfold theme
# Unfold Theme
"unfold",
"unfold.contrib.filters",
"unfold.contrib.forms",
@@ -42,37 +46,159 @@ INSTALLED_APPS = [
"unfold.contrib.import_export",
"unfold.contrib.guardian",
"unfold.contrib.simple_history",
# django
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# thired party apps
'corsheaders',
'rest_framework',
'drf_spectacular',
'django_cleanup.apps.CleanupConfig',
'django_filters',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'rest_framework.authtoken',
'import_export',
# Django Core
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-Party Apps
"storages",
"corsheaders",
"rest_framework",
"drf_spectacular",
"django_cleanup.apps.CleanupConfig",
"django_filters",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"rest_framework.authtoken",
"import_export",
"django_jalali",
# custom apps
'product',
'account',
'ticket',
'chat',
'order',
'home',
'blog',
# Custom Apps
"product",
"account",
"ticket",
"chat",
"order",
"home",
"blog",
]
# ==============================================================================
# Middleware Configuration
# ==============================================================================
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "core.urls"
# ==============================================================================
# Template Configuration
# ==============================================================================
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "core.wsgi.application"
# ==============================================================================
# Authentication and Password Validation
# ==============================================================================
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
LANGUAGE_CODE = "fa"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# ==============================================================================
# Static Files Configuration
# ==============================================================================
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "custom_static"),
BASE_DIR / "core" / "static",
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "account.User"
# ==============================================================================
# REST Framework Configuration
# ==============================================================================
REST_FRAMEWORK = {
# "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
# "DEFAULT_PERMISSION_CLASSES": [
# "rest_framework.permissions.IsAuthenticated",
# ],
}
# ==============================================================================
# Simple JWT Configuration
# ==============================================================================
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
}
# ==============================================================================
# Spectacular (API Documentation) Configuration
# ==============================================================================
SPECTACULAR_SETTINGS = {
"TITLE": os.getenv("SITE_TITLE"),
"DESCRIPTION": os.getenv("SITE_TITLE"),
"VERSION": "2.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"SWAGGER_UI_SETTINGS": {
"persistAuthorization": True,
},
}
# ==============================================================================
# Persian Datetime Configuration
# ==============================================================================
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",
@@ -81,7 +207,6 @@ JALALI_SETTINGS = {
"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",
@@ -91,113 +216,20 @@ JALALI_SETTINGS = {
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'fa'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'custom_static'),
BASE_DIR / "core" / "static"
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'account.User'
REST_FRAMEWORK = {
# 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ],
# ==============================================================================
# AWS S3 setting for production
# ==============================================================================
AWS_ACCESS_KEY_ID = 'mtiSN2JWjWgyfr2u'
AWS_SECRET_ACCESS_KEY = 'ZGmOM6ekLJEswJS1kOLp49B8DQ3GT0HZ'
AWS_STORAGE_BUCKET_NAME = 'c262408'
AWS_S3_ENDPOINT_URL = 'https://parspack.net'
AWS_S3_REGION_NAME = 'default'
AWS_S3_SIGNATURE_VERSION = 's3'
AWS_S3_ADDRESSING_STYLE = 'virtual'
AWS_QUERYSTRING_AUTH = False
AWS_DEFAULT_ACL = None
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
'ACL': 'public-read',
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
SPECTACULAR_SETTINGS = {
'TITLE': os.getenv("SITE_TITLE"),
'DESCRIPTION': os.getenv("SITE_TITLE"),
'VERSION': '2.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'COMPONENT_SPLIT_REQUEST': True
}
def environment_callback(request):
return ["نسخه ی توسعه", "success"]
+12 -1
View File
@@ -1,5 +1,7 @@
from .base import *
from .unfold_conf import *
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN]
CSRF_TRUSTED_ORIGINS = [
f"https://{DOMAIN}",
@@ -31,7 +33,16 @@ DATABASES = {
}
}
MEDIA_URL = '/shop_media/'
STORAGES = {
"default": {
"BACKEND": 'core.storages.MediaStorage',
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
MEDIA_URL = 'https://c262408.parspack.net/'
MEDIA_ROOT = '/app/media'
STATIC_URL = '/shop_static/'
+3
View File
@@ -265,3 +265,6 @@ UNFOLD = {
],
},
}
def environment_callback(request):
return ["نسخه ی توسعه", "success"]
+6
View File
@@ -0,0 +1,6 @@
from storages.backends.s3boto3 import S3Boto3Storage
class MediaStorage(S3Boto3Storage):
location = 'media'
default_acl = 'public-read'
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-26 17:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0012_alter_learnvideomodel_content_type'),
]
operations = [
migrations.AlterField(
model_name='showcaseslider',
name='description',
field=models.CharField(max_length=150, verbose_name='توضیحات'),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 5.1.2 on 2025-02-26 19:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0013_alter_showcaseslider_description'),
]
operations = [
migrations.AddField(
model_name='homeimagemodel',
name='video1',
field=models.FileField(null=True, upload_to='diff_section/', verbose_name='ویدیو اول'),
),
migrations.AddField(
model_name='homeimagemodel',
name='video2',
field=models.FileField(null=True, upload_to='diff_section/', verbose_name='ویدیو دوم'),
),
migrations.AlterField(
model_name='homeimagemodel',
name='image1',
field=models.ImageField(upload_to='diff_section/', verbose_name='عکس اول'),
),
migrations.AlterField(
model_name='homeimagemodel',
name='image2',
field=models.ImageField(upload_to='diff_section/', verbose_name='عکس دوم'),
),
]
+7 -3
View File
@@ -18,18 +18,22 @@ class SliderModel(models.Model):
class HomeImageModel(models.Model):
image1 = models.ImageField(upload_to='diff_image/', verbose_name='عکس اول')
image2 = models.ImageField(upload_to='diff_image/', verbose_name='عکس دوم')
image1 = models.ImageField(upload_to='diff_section/', verbose_name='عکس اول')
image2 = models.ImageField(upload_to='diff_section/', verbose_name='عکس دوم')
title1 = models.CharField(max_length=50, verbose_name='عنوان عکس اول')
title2 = models.CharField(max_length=50, verbose_name='عنوان عکس دوم')
description1 = models.TextField(verbose_name='توضیحات عکس اول')
description2 = models.TextField(verbose_name='توضیحات عکس دوم')
link1 = models.URLField(verbose_name='لینک عکس اول')
link2 = models.URLField(verbose_name='لینک عکس دوم')
video1 = models.FileField(verbose_name='ویدیو اول', upload_to='diff_section/', null=True)
video2 = models.FileField(verbose_name='ویدیو دوم', upload_to='diff_section/', null=True)
unique = (('unique', 'unique'),)
unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique', verbose_name='یونیک فیلد')
def __str__(self):
return f'{self.title1} - {self.title2}'
class Meta:
verbose_name = 'مدل عکس تفاوت خانه'
verbose_name_plural = 'مدل عکس تفاوت خانه'
@@ -37,7 +41,7 @@ class HomeImageModel(models.Model):
class ShowCaseSlider(models.Model):
title = models.CharField(max_length=30, verbose_name='عنوان')
description = models.CharField(max_length=150, verbose_name='عنوان')
description = models.CharField(max_length=150, verbose_name='توضیحات')
link = models.URLField(verbose_name='لینک')
image = models.ImageField(upload_to='show_case/', verbose_name='عکس')
def __str__(self):
+11 -9
View File
@@ -59,21 +59,23 @@ class OrderModel(models.Model):
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:
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
def discount(self):
pass
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
# 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
def tax(self):
return self.total_without_tax() * 0.2
def total(self):
return self.total_with_discount() + self.tax()
pass
# return self.total_with_discount() + self.tax()
def remove_order_item(self, item_pk, quantity):
pass
+13 -1
View File
@@ -8,8 +8,20 @@ class OrderItemSerailzier(serializers.ModelSerializer):
fields = "__all__"
read_only_fields = ('order', 'product')
class OrderModelSerializer(serializers.ModelSerializer):
class CartSerializer(serializers.ModelSerializer):
items = OrderItemSerailzier(many=True)
class Meta:
model = OrderModel
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', 'items']
class OrderSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
class Meta:
model = OrderModel
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', "images", "count", "id"]
def get_count(self, obj):
return obj.items.all().count()
def get_images(self, obj):
return ["a" , "b" , "c"]
+2 -1
View File
@@ -1,9 +1,10 @@
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from .views import CartItemViews, CartView
from .views import CartItemViews, CartView, OrderlistView
urlpatterns = [
path('list', OrderlistView.as_view(), name='order-list'),
path('cart', CartView.as_view()),
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
# path('payment', CartView.as_view()),
+12 -7
View File
@@ -4,7 +4,7 @@ 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, OrderModelSerializer
from .serializers import *
# from cart.models import
from rest_framework import status
from .models import OrderItemModel, OrderModel
@@ -53,11 +53,6 @@ class CartItemViews(APIView):
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)
@@ -66,9 +61,19 @@ class CartItemViews(APIView):
class CartView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderModelSerializer
serializer_class = CartSerializer
def get(self, request):
user = request.user
cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART')
cart_ser = self.serializer_class(instance=cart_instance, context={'request': request})
return Response(cart_ser.data, status=status.HTTP_200_OK)
class OrderlistView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderSerializer
def get(self, request):
user = request.user
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
orders_ser = self.serializer_class(instance=orders, many=True, context={'request': request})
return Response(orders_ser.data, status=status.HTTP_200_OK)
+7 -7
View File
@@ -170,14 +170,14 @@ class AllProductsView(APIView):
if search_query:
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
# Price filters
price_gte = request.query_params.get('price_gte', None)
price_lte = request.query_params.get('price_lte', None)
# # Price filters
# price_gte = request.query_params.get('price_gte', None)
# price_lte = request.query_params.get('price_lte', None)
if price_gte:
products = products.filter(variants__min_price__gte=price_gte)
if price_lte:
products = products.filter(variants__min_price__lte=price_lte)
# if price_gte:
# products = products.filter(variants__min_price__gte=price_gte)
# if price_lte:
# products = products.filter(variants__min_price__lte=price_lte)
# Sorting
sort_by = request.query_params.get('sort', None)
+32
View File
@@ -5,22 +5,33 @@ annotated-types==0.7.0
anyio==4.6.0
asgiref==3.8.1
attrs==24.2.0
az-iranian-bank-gateways==2.0.5
beautifulsoup4==4.12.3
boto3==1.36.26
botocore==1.36.26
branca==0.8.1
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.3.2
colorama==0.4.6
cryptography==44.0.1
defusedxml==0.8.0rc2
diff-match-patch==20230430
distro==1.9.0
Django==5.1.2
django-admin-interface==0.28.5
django-admin-persian-fonts==0.2
django-cleanup==8.1.0
django-colorfield==0.11.0
django-cors-headers==4.4.0
django-cron==0.6.0
django-dbbackup==4.2.1
django-dirtyfields==1.9.3
django-filter==24.3
django-import-export==4.1.1
django-iranian-cities==1.0.2
django-jalali==7.3.0
django-storages==1.14.5
django-unfold==0.48.0
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
@@ -28,6 +39,7 @@ djoser==2.3.1
dnspython==2.7.0
drf-spectacular==0.27.2
email_validator==2.2.0
et-xmlfile==1.1.0
factory_boy==3.3.1
Faker==28.4.1
folium==0.19.4
@@ -43,29 +55,40 @@ httpcore==1.0.5
httpx==0.27.2
idna==3.10
inflection==0.5.1
isodate==0.6.1
jalali_core==1.0.0
jdatetime==5.0.0
Jinja2==3.1.5
jiter==0.8.2
jmespath==1.0.1
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
lxml==5.2.2
MarkupPy==1.14
MarkupSafe==3.0.2
maxminddb==2.6.2
multidict==6.1.0
numpy==2.2.3
oauthlib==3.2.2
odfpy==1.4.1
openai==1.58.1
openpyxl==3.1.2
pillow==10.4.0
platformdirs==4.2.2
propcache==0.2.0
psutil==6.0.0
psycopg2-binary==2.9.10
py-vapid==1.9.2
pycparser==2.22
pycryptodome==3.20.0
pydantic==2.10.6
pydantic_core==2.27.2
PyJWT==2.10.1
pyTelegramBotAPI==4.23.0
python-dateutil==2.9.0.post0
python-decouple==3.8
python-dotenv==1.0.1
python-slugify==8.0.4
python-telegram-bot==21.6
python3-openid==3.2.0
pytz==2024.2
@@ -73,20 +96,29 @@ pywebpush==2.0.3
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
requests-file==2.1.0
requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
rpds-py==0.20.0
s3transfer==0.11.2
setuptools==75.1.0
six==1.16.0
sniffio==1.3.1
social-auth-app-django==5.4.2
social-auth-core==4.5.4
soupsieve==2.5
sqlparse==0.5.1
tablib==3.5.0
telebot==0.0.5
text-unidecode==1.3
tqdm==4.67.1
typing_extensions==4.12.2
tzdata==2024.1
uritemplate==4.1.1
urllib3==2.2.3
whitenoise==6.7.0
xlrd==2.0.1
xlwt==1.3.0
xyzservices==2025.1.0
yarl==1.11.1
zeep==4.2.1
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2025-02-26 17:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ticket', '0014_attachment_uploaded_by'),
]
operations = [
migrations.RemoveField(
model_name='ticket',
name='attachments',
),
migrations.RemoveField(
model_name='ticket',
name='content',
),
]
-2
View File
@@ -37,7 +37,6 @@ class Ticket(models.Model):
('other', 'سایر'),
]
subject = models.CharField(max_length=255, verbose_name='موضوع')
content = models.TextField(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='ادمین')
@@ -45,7 +44,6 @@ class Ticket(models.Model):
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)
attachments = models.ManyToManyField(Attachment, related_name='tickets', blank=True)
def __str__(self):
return self.subject
+39 -24
View File
@@ -3,36 +3,13 @@ from .models import Ticket, Message, Attachment
from django.utils.timezone import localtime
from account.serializers import ProfileSerializer
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = '__all__'
class TicketSerializer(serializers.ModelSerializer):
messages = MessageSerializer(many=True, read_only=True)
admin = ProfileSerializer(read_only=True)
class Meta:
model = Ticket
exclude = ('customer', )
read_only_fields = ('status', 'admin', )
class TicketListSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
exclude = ('customer', 'admin', 'order', 'content')
read_only_fields = ('status',)
class AttachmentSerializer(serializers.ModelSerializer):
file = serializers.FileField(write_only=True)
link = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = ['id', 'name', 'file','link' , 'created_at', 'size']
fields = ['id', 'name', 'file', 'link' , 'created_at', 'size']
read_only_fields = ('size', 'name', )
def get_link(self, obj):
@@ -40,3 +17,41 @@ class AttachmentSerializer(serializers.ModelSerializer):
if request is not None:
return request.build_absolute_uri(obj.file.url)
return obj.file.url
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
exclude = ('sender', )
extra_kwargs = {'ticket': {'write_only': True}}
class MessageForTicketSerializer(serializers.ModelSerializer):
class Meta:
model = Message
exclude = ('sender', 'ticket')
class TicketSerializer(serializers.ModelSerializer):
messages = MessageSerializer(many=True, read_only=True)
message = MessageForTicketSerializer(write_only=True)
class Meta:
model = Ticket
exclude = ('customer', 'admin')
read_only_fields = ('status',)
def create(self, validated_data):
message = validated_data.pop('message', None)
ticket = super().create(validated_data)
print(f'fck this shit{ticket.pk}')
message['ticket'] = ticket.pk
message_obj = MessageSerializer(data=message)
if message_obj.is_valid():
message_obj.save(sender=ticket.customer)
return ticket
else:
raise Exception('bullshit data for message')
class TicketListSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
exclude = ('customer', 'admin', )
read_only_fields = ('status',)
+22 -8
View File
@@ -40,16 +40,30 @@ class AttachmentUploadView(APIView):
class TicketCreateView(generics.CreateAPIView):
queryset = Ticket.objects.all()
# class TicketCreateView(generics.CreateAPIView):
# queryset = Ticket.objects.all()
# serializer_class = TicketSerializer
# permission_classes = [permissions.IsAuthenticated]
# def perform_create(self, serializer):
# message = serializer.validated_data.get('message')
# ticket = serializer.validated_data.get('ticket')
# serializer.save(customer=self.request.user)
class TicketCreateView(APIView):
serializer_class = TicketSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
serializer.save(customer=self.request.user)
def post(self, request):
new_ticket_ser = self.serializer_class(data=request.data)
message = request.data.get('message', None)
print(message)
if new_ticket_ser.is_valid():
new_ticket_ser.save(customer=request.user)
return Response(new_ticket_ser.data, status=status.HTTP_201_CREATED)
else:
print(new_ticket_ser.error_messages)
return Response(new_ticket_ser.errors)
class TicketListView(APIView):
serializer_class = TicketListSerializer
permission_classes = [permissions.IsAuthenticated]
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
. /etc/profile
echo "Cron environment:" >> /var/log/cron/cron.log
env >> /var/log/cron/cron.log
echo "Starting backup at $(date)" >> /var/log/cron/cron.log
export PGPASSWORD=$PG_PASSWORD
TELEGRAM_API="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendDocument"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_heymlz_shop${TIMESTAMP}.sql"
ZIP_FILE="backup_heymlz_shop${TIMESTAMP}.zip"
pg_dump -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DATABASE -w > /backups/$BACKUP_FILE
if [ $? -ne 0 ]; then
echo "backup failed" >> /var/log/cron/cron.log
exit 1
fi
zip -j /backups/$ZIP_FILE /backups/$BACKUP_FILE
curl -F chat_id=$TELEGRAM_CHAT_ID \
-F document=@/backups/$ZIP_FILE \
$TELEGRAM_API
curl -F chat_id=$TELEGRAM_CHAT_ID2 \
-F document=@/backups/$ZIP_FILE \
$TELEGRAM_API
ls -t /backups/*.zip | tail -n +4 | xargs rm -f
ls -t /backups/*.sql | tail -n +4 | xargs rm -f
echo "backup completed at $(date)" >> /var/log/cron/cron.log
+1
View File
@@ -0,0 +1 @@
0 3 * * * /app/backup.sh >> /var/log/cron/cron.log 2>&1
+15
View File
@@ -0,0 +1,15 @@
FROM alpine:3.19
RUN apk add --no-cache postgresql-client curl zip tzdata
RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
RUN mkdir -p /var/log/cron /backups
WORKDIR /app
COPY backup.sh .
COPY crontab /etc/crontabs/root
RUN chmod +x backup.sh && \
chmod 0644 /etc/crontabs/root && \
touch /var/log/cron/cron.log
CMD ["busybox", "crond", "-f", "-L", "/var/log/cron/cron.log"]
+19 -2
View File
@@ -42,11 +42,28 @@ services:
networks:
- default
db-backup:
build:
context: ./backup
depends_on:
- db
environment:
- PG_HOST=db
- PG_PORT=5432
- PG_DATABASE=hshop
- PG_USER=byeto
- PG_PASSWORD=vuhbyq-cypMu0-sirbon
- TELEGRAM_BOT_TOKEN=7068288679:AAGecMnyt9A6R78OQu8nQeISMK1LepX718g
- TELEGRAM_CHAT_ID=1198382521
- TELEGRAM_CHAT_ID2=5115366609
volumes:
- backups:/backups
networks:
- default
volumes:
postgres_data:
media_data:
backups:
networks:
default:
+6 -3
View File
@@ -3,18 +3,21 @@
// types
type Props = {
selectable?: boolean,
selected?: boolean;
}
// props
defineProps<Props>();
withDefaults(defineProps<Props>(), {
selectable: false
});
</script>
<template>
<div
class="size-[25px] rounded-full transition-all ring-2 ring-offset-4 shadow-black/30 shadow-inner"
:class="selected ? 'ring-blue-500' : 'ring-transparent'"
class="size-[25px] rounded-full transition-all shadow-black/30 shadow-inner"
:class="[selectable ? 'ring-2 ring-offset-4 ' : '', selected ? 'ring-blue-500' : 'ring-transparent']"
/>
</template>
+26 -5
View File
@@ -1,12 +1,23 @@
<script lang="ts" setup>
// import
import type { LoadingOverlayProvideType } from "~/pages/index.vue";
// provide / inject
const { showLoadingOverlay } = inject("loadingOverlay") as LoadingOverlayProvideType;
// state
const { $gsap: gsap } = useNuxtApp();
const shouldRenderLoadingOverlay = ref(true);
// lifecycle
onMounted(() => {
watch(() => showLoadingOverlay.value, (value) => {
if (!value) {
const timeline = gsap.timeline();
timeline
@@ -16,18 +27,28 @@ onMounted(() => {
.to("#loading-overlay", {
scale: 0.8,
opacity: 0,
delay: 5
delay: 3
})
.to("#loading-overlay", {
opacity: 0,
y: "20%"
y: "20%",
onComplete: () => {
shouldRenderLoadingOverlay.value = false;
}
});
}
}, {
once: true
});
</script>
<template>
<div id="loading-overlay" class="fixed inset-0 size-full z-9999 flex-center bg-black">
<div
v-if="shouldRenderLoadingOverlay"
id="loading-overlay"
class="fixed inset-0 size-full z-9999 flex-center bg-black"
>
<img id="loading-overlay-image" src="/video/loading-2.gif" class="opacity-0 scale-70 absolute z-20" alt="" />
<div
id="loading-overlay-gradient"
@@ -41,7 +62,7 @@ onMounted(() => {
#loading-overlay-image {
animation-name: loading-overlay-image-animation;
animation-duration: 1s;
animation-delay: 0.75s;
animation-delay: 0.35s;
animation-fill-mode: forwards;
}
@@ -92,7 +92,7 @@ const onSwiper = (swiper: SwiperClass) => {
brand="برند محصول"
:title="product.name"
:picture="product.variants[0].images[0].image"
:colors="product.variants.map(v => v.color)"
:colors="product.colors"
:price="product.variants[0].price"
:rate="product.rating"
:dark-layer="true"
+7 -1
View File
@@ -5,6 +5,11 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData";
import type { LoadingOverlayProvideType } from "~/pages/index.vue";
// provide / inject
const { changeLoadingOverlay } = inject("loadingOverlay") as LoadingOverlayProvideType;
// state
@@ -31,6 +36,7 @@ let gsapTimeline: gsap.core.Timeline;
// methods
const onSwiper = (swiper: SwiperClass) => {
changeLoadingOverlay(false);
swiper_instance.value = swiper;
};
@@ -105,7 +111,7 @@ onMounted(() => {
padding: "0px 80px"
}, {
padding: "0px 40px"
}, "=")
}, "=");
ScrollTrigger.create({
trigger: "#header-slider-container",
@@ -101,6 +101,7 @@ watch(() => selectedVariant.value, (newValue) => {
v-for="color in product!.colors"
:key="color"
@click="selectedColor = color"
selectable
:selected="selectedColor === color "
:style="{backgroundColor: color}"
class="cursor-pointer"
+23 -1
View File
@@ -4,10 +4,32 @@
import useHomeData from "~/composables/api/home/useHomeData";
// type
export type LoadingOverlayProvideType = {
showLoadingOverlay: Ref<boolean>,
changeLoadingOverlay: (value: boolean) => void,
}
// state
const { suspense } = useHomeData();
const showLoadingOverlay = ref(true);
// method
const changeLoadingOverlay = (value: boolean) => {
showLoadingOverlay.value = value;
};
// provide / inject
provide("loadingOverlay", {
showLoadingOverlay,
changeLoadingOverlay
});
// ssr
const response = await suspense();
@@ -23,7 +45,7 @@ if (response.isError) {
<template>
<div class="w-full">
<!-- <LoadingOverlay />-->
<LoadingOverlay />
<Hero />
<Preview />
<ProductsShowcase />