diff --git a/backend/.env.local b/backend/.env.local index 657f88f..e0f9789 100644 --- a/backend/.env.local +++ b/backend/.env.local @@ -28,4 +28,6 @@ REFRESH_TOKEN_LIFETIME = 5000 SMS_API_KEY = '' -VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4' \ No newline at end of file +VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4' + +OPENAI_API_KEY = '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' \ No newline at end of file diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 695947c..ee792e4 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -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"] - - diff --git a/backend/core/settings/production.py b/backend/core/settings/production.py index 4ce04ef..3ec44aa 100644 --- a/backend/core/settings/production.py +++ b/backend/core/settings/production.py @@ -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/' diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index a4ad30f..045106b 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -264,4 +264,7 @@ UNFOLD = { ], }, -} \ No newline at end of file +} + +def environment_callback(request): + return ["نسخه ی توسعه", "success"] \ No newline at end of file diff --git a/backend/core/storages.py b/backend/core/storages.py new file mode 100644 index 0000000..d9cea23 --- /dev/null +++ b/backend/core/storages.py @@ -0,0 +1,6 @@ +from storages.backends.s3boto3 import S3Boto3Storage + + +class MediaStorage(S3Boto3Storage): + location = 'media' + default_acl = 'public-read' \ No newline at end of file diff --git a/backend/home/migrations/0013_alter_showcaseslider_description.py b/backend/home/migrations/0013_alter_showcaseslider_description.py new file mode 100644 index 0000000..6f6e597 --- /dev/null +++ b/backend/home/migrations/0013_alter_showcaseslider_description.py @@ -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='توضیحات'), + ), + ] diff --git a/backend/home/migrations/0014_homeimagemodel_video1_homeimagemodel_video2_and_more.py b/backend/home/migrations/0014_homeimagemodel_video1_homeimagemodel_video2_and_more.py new file mode 100644 index 0000000..f3b0e88 --- /dev/null +++ b/backend/home/migrations/0014_homeimagemodel_video1_homeimagemodel_video2_and_more.py @@ -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='عکس دوم'), + ), + ] diff --git a/backend/home/models.py b/backend/home/models.py index 857e0e0..d59f71a 100644 --- a/backend/home/models.py +++ b/backend/home/models.py @@ -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): diff --git a/backend/order/models.py b/backend/order/models.py index 570351b..3f37b5b 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -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 diff --git a/backend/order/serializers.py b/backend/order/serializers.py index 093c002..5950648 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -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'] \ No newline at end of file + 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"] \ No newline at end of file diff --git a/backend/order/urls.py b/backend/order/urls.py index a4d4dbd..325e1a5 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -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/', CartItemViews.as_view(), name='change-item-cart'), # path('payment', CartView.as_view()), diff --git a/backend/order/views.py b/backend/order/views.py index 7f5971c..bb21f5a 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -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) \ No newline at end of file diff --git a/backend/product/views.py b/backend/product/views.py index 68e06e8..6606516 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -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) diff --git a/backend/requirements.txt b/backend/requirements.txt index ca6be94..83cdbca 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/backend/ticket/migrations/0015_remove_ticket_attachments_remove_ticket_content.py b/backend/ticket/migrations/0015_remove_ticket_attachments_remove_ticket_content.py new file mode 100644 index 0000000..f5de4af --- /dev/null +++ b/backend/ticket/migrations/0015_remove_ticket_attachments_remove_ticket_content.py @@ -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', + ), + ] diff --git a/backend/ticket/models.py b/backend/ticket/models.py index 8ab3b90..6a40c05 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -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 diff --git a/backend/ticket/serializers.py b/backend/ticket/serializers.py index bd7611f..ba2cc9c 100644 --- a/backend/ticket/serializers.py +++ b/backend/ticket/serializers.py @@ -3,40 +3,55 @@ 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): request = self.context.get('request') if request is not None: return request.build_absolute_uri(obj.file.url) - return obj.file.url \ No newline at end of file + 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',) + + diff --git a/backend/ticket/views.py b/backend/ticket/views.py index 292b4f7..556d6b8 100644 --- a/backend/ticket/views.py +++ b/backend/ticket/views.py @@ -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] diff --git a/backup/backup.sh b/backup/backup.sh new file mode 100644 index 0000000..7d3221e --- /dev/null +++ b/backup/backup.sh @@ -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 \ No newline at end of file diff --git a/backup/crontab b/backup/crontab new file mode 100644 index 0000000..2341de2 --- /dev/null +++ b/backup/crontab @@ -0,0 +1 @@ +0 3 * * * /app/backup.sh >> /var/log/cron/cron.log 2>&1 \ No newline at end of file diff --git a/backup/dockerfile b/backup/dockerfile new file mode 100644 index 0000000..08b66d8 --- /dev/null +++ b/backup/dockerfile @@ -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"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6a21034..eded502 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/frontend/components/global/ColorCircle.vue b/frontend/components/global/ColorCircle.vue index 3db00be..0d3c2df 100644 --- a/frontend/components/global/ColorCircle.vue +++ b/frontend/components/global/ColorCircle.vue @@ -3,18 +3,21 @@ // types type Props = { + selectable?: boolean, selected?: boolean; } // props -defineProps(); +withDefaults(defineProps(), { + selectable: false +}); \ No newline at end of file diff --git a/frontend/components/global/LoadingOverlay.vue b/frontend/components/global/LoadingOverlay.vue index 6329eca..7b22a94 100644 --- a/frontend/components/global/LoadingOverlay.vue +++ b/frontend/components/global/LoadingOverlay.vue @@ -1,33 +1,54 @@