diff --git a/backend/chat/models.py b/backend/chat/models.py index da9485b..ebbf5e4 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -1,6 +1,6 @@ from django.db import models from account.models import User -from product.models import ProductModel, DollorModel +from product.models import ProductModel from django.conf import settings import openai from time import sleep @@ -24,9 +24,7 @@ class ProductChatModel(models.Model): def save(self, *args, **kwargs): if not self.thread: client = openai.OpenAI(api_key=settings.OPENAI_API_KEY) - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - dollor_price = dollor_object.price - product_json = DynamicProductSerializer(instance=self.product, context={'dollor_price': dollor_price, 'view_type': 'chat'}).data + product_json = DynamicProductSerializer(instance=self.product, context={'view_type': 'chat'}).data try: thread = client.beta.threads.create( diff --git a/backend/core/__init__.py b/backend/core/__init__.py index e69de29..9e0d95f 100644 --- a/backend/core/__init__.py +++ b/backend/core/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/backend/core/celery.py b/backend/core/celery.py new file mode 100644 index 0000000..af3c387 --- /dev/null +++ b/backend/core/celery.py @@ -0,0 +1,8 @@ +import os +from celery import Celery +from django.conf import settings + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.production') +app = Celery('core') +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) \ No newline at end of file diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 38808a3..5c7c324 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -30,7 +30,7 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY") # Security and Debugging -SECRET_KEY = os.getenv("SECRET_KEY") or 'this is just for cron job dont judge me' +SECRET_KEY = os.getenv("SECRET_KEY") DEBUG = True BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -65,7 +65,8 @@ INSTALLED_APPS = [ "rest_framework.authtoken", "import_export", "django_jalali", - 'django_crontab', + 'django_celery_beat', + 'azbankgateways', # Custom Apps "product", "account", @@ -235,11 +236,19 @@ AWS_S3_OBJECT_PARAMETERS = { } # ============================================================================== -# django CRONJOBS +# bank gateway configs # ============================================================================== -CRONJOBS = [ - ('* * * * *', 'product.cron.update_product_prices', f'>> {BASE_DIR}/logfile.log 2>&1'), -] - - +AZ_IRANIAN_BANK_GATEWAYS = { + 'GATEWAYS': { + 'ZARINPAL': { + 'MERCHANT_CODE': 'Merchant-Code', + 'SANDBOX': True, + } + }, + 'IS_SAMPLE_FORM_ENABLE': True, + 'DEFAULT_BANK': 'ZARINPAL', + 'CURRENCY': 'IRR', + 'TRACKING_CODE_QUERY_PARAM': 'tc', + 'BANK_PRIORITIES': ['ZARINPAL'], +} \ No newline at end of file diff --git a/backend/core/settings/production.py b/backend/core/settings/production.py index 3ec44aa..a9e37fd 100644 --- a/backend/core/settings/production.py +++ b/backend/core/settings/production.py @@ -46,4 +46,23 @@ MEDIA_URL = 'https://c262408.parspack.net/' MEDIA_ROOT = '/app/media' STATIC_URL = '/shop_static/' -STATIC_ROOT = '/app/static' \ No newline at end of file +STATIC_ROOT = '/app/static' + + +# ============================================================================== +# django cerery +# ============================================================================== + +CELERY_BROKER_URL = "redis://redis:6379/0" +CELERY_RESULT_BACKEND = "redis://redis:6379/0" +CELERY_TIMEZONE = "UTC" +CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' + +from celery.schedules import crontab + +CELERY_BEAT_SCHEDULE = { + 'update-prices-every-minute': { + 'task': 'product.tasks.update_product_prices', + 'schedule': crontab(minute='*'), + }, +} \ No newline at end of file diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 045106b..3f4deba 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -135,6 +135,11 @@ UNFOLD = { "link": reverse_lazy("admin:product_dollormodel_changelist"), "badge": "utils.admin.dollor_price", }, + { + "title": _("کد تخفیف"), + "icon": "payments", + "link": reverse_lazy("admin:order_discountcode_changelist"), + }, ], }, @@ -260,7 +265,47 @@ UNFOLD = { ], }, - +{ + "title": _("تسک های سلری"), + "collapsible": True, + "items": [ + { + "title": _("Clocked"), + "icon": "hourglass_bottom", + "link": reverse_lazy( + "admin:django_celery_beat_clockedschedule_changelist" + ), + }, + { + "title": _("Crontabs"), + "icon": "update", + "link": reverse_lazy( + "admin:django_celery_beat_crontabschedule_changelist" + ), + }, + { + "title": _("Intervals"), + "icon": "arrow_range", + "link": reverse_lazy( + "admin:django_celery_beat_intervalschedule_changelist" + ), + }, + { + "title": _("Periodic tasks"), + "icon": "task", + "link": reverse_lazy( + "admin:django_celery_beat_periodictask_changelist" + ), + }, + { + "title": _("Solar events"), + "icon": "event", + "link": reverse_lazy( + "admin:django_celery_beat_solarschedule_changelist" + ), + }, + ], + }, ], }, diff --git a/backend/dockerfile b/backend/dockerfile index 09c155a..16ceef1 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -13,6 +13,5 @@ COPY . /app/ CMD ["sh", "-c", "python manage.py makemigrations && \ python manage.py migrate && \ - python manage.py crontab add && \ python manage.py collectstatic --no-input && \ gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3"] \ No newline at end of file diff --git a/backend/home/admin.py b/backend/home/admin.py index e64e483..b4c7429 100644 --- a/backend/home/admin.py +++ b/backend/home/admin.py @@ -92,4 +92,62 @@ class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin): ArrayField: { "widget": ArrayWidget, } - } \ No newline at end of file + } + + +# admin.py +from django.contrib import admin +from unfold.admin import ModelAdmin + +from django_celery_beat.models import ( + ClockedSchedule, + CrontabSchedule, + IntervalSchedule, + PeriodicTask, + SolarSchedule, +) +from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin +from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin +from django_celery_beat.admin import PeriodicTaskAdmin as BasePeriodicTaskAdmin +from django_celery_beat.admin import PeriodicTaskForm, TaskSelectWidget +from unfold.widgets import * +admin.site.unregister(PeriodicTask) +admin.site.unregister(IntervalSchedule) +admin.site.unregister(CrontabSchedule) +admin.site.unregister(SolarSchedule) +admin.site.unregister(ClockedSchedule) + + +class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget): + pass + + +class UnfoldPeriodicTaskForm(PeriodicTaskForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["task"].widget = UnfoldAdminTextInputWidget() + self.fields["regtask"].widget = UnfoldTaskSelectWidget() + + +@admin.register(PeriodicTask) +class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin): + form = UnfoldPeriodicTaskForm + + +@admin.register(IntervalSchedule) +class IntervalScheduleAdmin(ModelAdmin): + pass + + +@admin.register(CrontabSchedule) +class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin): + pass + + +@admin.register(SolarSchedule) +class SolarScheduleAdmin(ModelAdmin): + pass + +@admin.register(ClockedSchedule) +class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin): + pass \ No newline at end of file diff --git a/backend/home/views.py b/backend/home/views.py index 003a09a..28241e8 100644 --- a/backend/home/views.py +++ b/backend/home/views.py @@ -20,8 +20,6 @@ class HomeView(APIView): authentication_classes = [] def get(self, request): - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - dollor_price = dollor_object.price sliders = SliderModel.objects.all() slider_ser = SliderSerializer(instance=sliders, many=True, context={'request': request}) @@ -30,7 +28,7 @@ class HomeView(APIView): sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request}) products_to_show = ProductModel.objects.filter(show=True) - product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price, 'view_type': 'list'}) + product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'view_type': 'list'}) home_image = HomeImageModel.objects.all().first() home_image_ser = HomeImageSerializer(instance=home_image, context={'request': request}) diff --git a/backend/order/admin.py b/backend/order/admin.py index 3626c1a..0cc6e52 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -19,6 +19,12 @@ class OrderItemModelInline(StackedInline): +@admin.register(DiscountCode) +class DiscountCodeAdmin(ModelAdmin, ImportExportModelAdmin): + import_form_class = ImportForm + export_form_class = ExportForm + list_display = ['code', 'expiration_date', 'percent', 'quantity'] + @admin.register(OrderModel) class OrderAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/order/migrations/0012_rename_name_discountcode_code.py b/backend/order/migrations/0012_rename_name_discountcode_code.py new file mode 100644 index 0000000..6dda311 --- /dev/null +++ b/backend/order/migrations/0012_rename_name_discountcode_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-03-10 20:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0011_orderitemmodel_price'), + ] + + operations = [ + migrations.RenameField( + model_name='discountcode', + old_name='name', + new_name='code', + ), + ] diff --git a/backend/order/migrations/0013_ordermodel_cart_total.py b/backend/order/migrations/0013_ordermodel_cart_total.py new file mode 100644 index 0000000..86b90be --- /dev/null +++ b/backend/order/migrations/0013_ordermodel_cart_total.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-03-11 18:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0012_rename_name_discountcode_code'), + ] + + operations = [ + migrations.AddField( + model_name='ordermodel', + name='cart_total', + field=models.BigIntegerField(blank=True, null=True, verbose_name='کل سبد خرید'), + ), + ] diff --git a/backend/order/migrations/0014_alter_orderitemmodel_price.py b/backend/order/migrations/0014_alter_orderitemmodel_price.py new file mode 100644 index 0000000..2e001cd --- /dev/null +++ b/backend/order/migrations/0014_alter_orderitemmodel_price.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-03-11 19:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0013_ordermodel_cart_total'), + ] + + operations = [ + migrations.AlterField( + model_name='orderitemmodel', + name='price', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قیمت'), + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index 1251999..a37a45f 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -6,20 +6,31 @@ from .execptions import DiscountNotAvailableError from django_jalali.db import models as jmodels class DiscountCode(models.Model): - name = models.CharField(max_length=50, verbose_name='کد تخفیف') + code = models.CharField(max_length=50, verbose_name='کد تخفیف') percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد') quantity = models.PositiveIntegerField(verbose_name='تعداد') expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا') def __str__(self): - return self.name + return self.code class Meta: verbose_name = 'کد تخفیف' verbose_name_plural = 'کد های تخفیف' + def is_valid(self): return self.expiration_date > timezone.now() and self.quantity > 0 + def not_valid_reason(self): + if self.expiration_date > timezone.now() and self.quantity > 0: + return 'این کد معتبر میباشد' + elif not self.expiration_date > timezone.now(): + return 'تایم کد تخفیف تمام شده' + elif not self.quantity > 0: + return 'این کد تخفیف تمام شده است' + else: + print('log later bug') + class OrderModel(models.Model): @@ -39,6 +50,12 @@ class OrderModel(models.Model): 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="وضعیت سفارش") + discount = models.BigIntegerField(null=True, blank=True, verbose_name='کل تخقیف') + tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات') + final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی') + cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید') + + def __str__(self): return f'سفارش: {self.id}' @@ -47,9 +64,6 @@ class OrderModel(models.Model): verbose_name_plural = 'سفارشات' - # def total_without_tax(self): - # return sum(item.total() for item in self.items.all()) - def save(self, *args, **kwargs): try: @@ -63,15 +77,12 @@ class OrderModel(models.Model): super().save(*args, **kwargs) + 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 + # discount_percent = self.discount_code.percent + # return total_with_item_discount * ((100 - discount_percent) / 100) + pass def tax(self): @@ -82,22 +93,20 @@ class OrderModel(models.Model): pass # return self.total_with_discount() + self.tax() - def remove_order_item(self, item_pk, quantity): + + def final_price(self): pass - def add_order_item(self, item_pk, quantity): - status = '' - return status - - def clear_cart(self): + def submit_cart(self): pass + class OrderItemModel(models.Model): order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") - price = models.PositiveIntegerField(verbose_name='قیمت') + price = models.PositiveIntegerField(verbose_name='قیمت', blank=True, null=True) product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") class Meta: verbose_name = 'ایتم سبد خرید' diff --git a/backend/order/serializers.py b/backend/order/serializers.py index f796e5b..e0b3b2c 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -1,29 +1,105 @@ from rest_framework import serializers -from .models import OrderItemModel, OrderModel -from product.serializers import ProductVariantSerialzier +from .models import OrderItemModel, OrderModel, DiscountCode +from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer +from account.serializers import UserAddressSerializer +from product.models import ProductVariant + +class ProductVariantSerialzier(serializers.ModelSerializer): + product_attributes = AttributeValueSerialzier(many=True) + image = serializers.SerializerMethodField() + discount_amount = serializers.SerializerMethodField() + title = serializers.SerializerMethodField() + price = serializers.SerializerMethodField() + final_price = serializers.SerializerMethodField() + category = serializers.SerializerMethodField() + class Meta: + model = ProductVariant + fields = ['title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price'] + + def get_discount_amount(self, obj): + discount_amount = int(obj.price * (obj.discount / 100)) + return f'{discount_amount:,.0f} تومان' + + def get_final_price(self, obj): + final_price = obj.price - int(obj.price * (obj.discount / 100)) + return f'{final_price:,.0f} تومان' + + def get_price(self, obj): + return f'{obj.price:,.0f} تومان' + + def get_image(self, obj): + return self.context.get('request').build_absolute_uri(obj.images.all().first().image.url) + + def get_title(self, obj): + return obj.product.name + + def get_category(self, obj): + return obj.product.category.name + + +class DiscountCodeSerializer(serializers.ModelSerializer): + class Meta: + model = DiscountCode + fields = ('code', 'percent') + read_only_fields = ('percent',) + class OrderItemSerailzier(serializers.ModelSerializer): product = serializers.SerializerMethodField() class Meta: model = OrderItemModel - fields = "__all__" - read_only_fields = ('order', 'product') + exclude = ('price', 'order') + read_only_fields = ('order', 'product', ) def get_product(self, obj): return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data + + + class CartSerializer(serializers.ModelSerializer): items = OrderItemSerailzier(many=True) + cart_total = serializers.SerializerMethodField() + tax = serializers.SerializerMethodField() + final_price = serializers.SerializerMethodField() + discount_code = serializers.SerializerMethodField() class Meta: model = OrderModel - fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', 'items'] + fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price'] + def get_discount_code(self, obj): + if obj.discount_code: + return { + 'code': f'{obj.discount_code.code}', + 'percent': obj.discount_code.percent, + 'amount': f'{10000:,.0f} تومان' + } + else: + return None + + + + + + def get_tax(self, obj): + return f'{1000:,.0f} تومان' + + def get_cart_total(self, obj): + + return f'{10000:,.0f} تومان' + def get_final_price(self, obj): + + return f'{8000:,.0f} تومان' + + class OrderSerializer(serializers.ModelSerializer): count = serializers.SerializerMethodField() images = serializers.SerializerMethodField() + address = UserAddressSerializer() + items = OrderItemSerailzier(many=True) class Meta: model = OrderModel - fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', "images", "count", "id"] + fields = ['address', 'created_at', 'items', 'status', 'discount_code', "images", "count", "id"] def get_count(self, obj): return obj.items.all().count() diff --git a/backend/order/urls.py b/backend/order/urls.py index 325e1a5..d29e3b5 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -1,11 +1,13 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include -from .views import CartItemViews, CartView, OrderlistView +from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView urlpatterns = [ - path('list', OrderlistView.as_view(), name='order-list'), + path('all', OrderlistView.as_view(), name='order-list'), path('cart', CartView.as_view()), + path('cart/discount', ApplyDiscountView.as_view()), + path('cart/all', CartItemClear.as_view()), path('cart/item/', CartItemViews.as_view(), name='change-item-cart'), # path('payment', CartView.as_view()), # path('', CartView.as_view()), diff --git a/backend/order/views.py b/backend/order/views.py index 364287e..9c24245 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated from .serializers import * # from cart.models import from rest_framework import status -from .models import OrderItemModel, OrderModel +from .models import OrderItemModel, OrderModel, DiscountCode try: pass except DiscountNotAvailableError: @@ -24,6 +24,36 @@ pay +class ApplyDiscountView(APIView): + serializer_class = DiscountCodeSerializer + permission_classes = [IsAuthenticated] + def post(self, request): + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + discount_code = get_object_or_404(DiscountCode, code=request.data.get('code')) + + if not discount_code.is_valid(): + return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST) + cart_order.discount_code = discount_code + cart_order.save() + return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK) + + + +class CartItemClear(APIView): + permission_classes = [IsAuthenticated] + serializer_class = OrderItemSerailzier + def delete(self, request): + cart_order, created = OrderModel.objects.get_or_create( + user=request.user, + status='CART' + ) + cart_order.items.all().delete() + return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT) + + class CartItemViews(APIView): permission_classes = [IsAuthenticated] serializer_class = OrderItemSerailzier @@ -77,4 +107,65 @@ class OrderlistView(APIView): 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 + return Response(orders_ser.data, status=status.HTTP_200_OK) + + + +# from rest_framework.views import APIView +# from rest_framework.response import Response +# from rest_framework import status +# from azbankgateways import bankfactories, models as bank_models + +# class PaymentView(APIView): +# def post(self, request): +# amount = request.data.get('amount') +# user = request.user + + +# payment = Payment.objects.create(amount=amount, bank_type='ZARINPAL') + + +# factory = bankfactories.ZarinpalBankFactory() +# try: +# bank = factory.create( +# amount=amount, +# user=user, +# callback_url='http://.com/callback/', +# reference_model=payment, +# ) +# bank.ready() +# return Response({'gateway_url': bank.redirect_url}, status=status.HTTP_200_OK) +# except Exception as e: +# return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + + +# from django.views.decorators.csrf import csrf_exempt +# from rest_framework.decorators import api_view +# from rest_framework.response import Response +# from azbankgateways import bankfactories, models as bank_models + +# @csrf_exempt +# @api_view(['POST']) +# def callback_view(request): +# tracking_code = request.POST.get('tracking_code') +# payment_id = request.POST.get('payment_id') + +# payment = Payment.objects.get(id=payment_id) +# bank_type = payment.bank_type + + +# factory = bankfactories.BankFactory.get_bank(bank_type) +# try: +# result = factory.verify_transaction(tracking_code) +# if result.is_success: +# payment.status = 'Paid' +# payment.tracking_code = tracking_code +# payment.save() +# return Response({'status': 'Payment successful'}) +# else: +# payment.status = 'Failed' +# payment.save() +# return Response({'status': 'Payment failed'}) +# except Exception as e: +# return Response({'error': str(e)}) \ No newline at end of file diff --git a/backend/product/cron.py b/backend/product/cron.py deleted file mode 100644 index 883774f..0000000 --- a/backend/product/cron.py +++ /dev/null @@ -1,5 +0,0 @@ -from product.models import ProductVariant - -def update_product_prices(): - print('calling the update product prices from cron') - ProductVariant.update_all_prices() \ No newline at end of file diff --git a/backend/product/tasks.py b/backend/product/tasks.py new file mode 100644 index 0000000..4bba6c6 --- /dev/null +++ b/backend/product/tasks.py @@ -0,0 +1,8 @@ +from celery import shared_task +from product.models import ProductVariant + +@shared_task +def update_product_prices(): + print("\033[92m Calling update product prices from Celery\033[00m") + ProductVariant.update_all_prices() + print("\033[92m its working\033[00m") diff --git a/backend/product/views.py b/backend/product/views.py index 6606516..ea29436 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -54,9 +54,7 @@ class ProductView(APIView): authentication_classes = [] def get(self, request, pk): product = get_object_or_404(ProductModel, id=pk) - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - dollor_price = dollor_object.price - product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'instance'}) + product_ser = self.serializer_class(instance=product, many=False, context={'request': request, 'view_type': 'instance'}) return Response(product_ser.data, status=status.HTTP_200_OK) @@ -103,6 +101,7 @@ class AllProductsView(APIView): "Sort results by one of the following fields:\n" "`name`, `-name`, `price`, `-price`, `created_at`, `-created_at`." "\nPrefix with `-` for descending order." + "remove the price form sorting templory " ), required=False, type=OpenApiTypes.STR, @@ -171,17 +170,17 @@ class AllProductsView(APIView): 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_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__price__gte=price_gte) + if price_lte: + products = products.filter(variants__price__lte=price_lte) # Sorting sort_by = request.query_params.get('sort', None) - if sort_by in ['name', '-name', 'price', '-price', 'created_at', '-created_at']: + if sort_by in ['name', '-name', 'created_at', '-created_at']: products = products.order_by(sort_by) else: products = products.order_by('name') @@ -189,9 +188,7 @@ class AllProductsView(APIView): # Pagination paginator = self.pagination_class() paginated_products = paginator.paginate_queryset(products, request) - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - dollor_price = dollor_object.price - serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'list'}) + serializer = self.serializer_class(paginated_products, many=True, context={'request': request, 'view_type': 'list'}) return paginator.get_paginated_response(serializer.data) except MainCategoryModel.DoesNotExist: diff --git a/backend/requirements.txt b/backend/requirements.txt index a3a2e7b..d99ba6c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,19 +1,27 @@ aiohappyeyeballs==2.4.0 aiohttp==3.10.5 aiosignal==1.3.1 +amqp==5.3.1 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 +billiard==4.2.1 boto3==1.36.26 botocore==1.36.26 branca==0.8.1 +celery==5.4.0 certifi==2024.8.30 cffi==1.17.1 charset-normalizer==3.3.2 +click==8.1.8 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 colorama==0.4.6 +cron-descriptor==1.4.5 cryptography==44.0.1 defusedxml==0.8.0rc2 diff-match-patch==20230430 @@ -21,11 +29,10 @@ distro==1.9.0 Django==5.1.2 django-admin-interface==0.28.5 django-admin-persian-fonts==0.2 +django-celery-beat==2.7.0 django-cleanup==8.1.0 django-colorfield==0.11.0 django-cors-headers==4.4.0 -django-cron==0.6.0 -django-crontab==0.7.1 django-dbbackup==4.2.1 django-dirtyfields==1.9.3 django-filter==24.3 @@ -33,6 +40,7 @@ django-import-export==4.1.1 django-iranian-cities==1.0.2 django-jalali==7.3.0 django-storages==1.14.5 +django-timezone-field==7.1 django-unfold==0.48.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 @@ -65,6 +73,7 @@ jiter==0.8.2 jmespath==1.0.1 jsonschema==4.23.0 jsonschema-specifications==2024.10.1 +kombu==5.4.2 lxml==5.2.2 MarkupPy==1.14 MarkupSafe==3.0.2 @@ -78,6 +87,7 @@ openpyxl==3.1.2 packaging==24.2 pillow==10.4.0 platformdirs==4.2.2 +prompt_toolkit==3.0.50 propcache==0.2.0 psutil==6.0.0 psycopg2-binary==2.9.10 @@ -88,6 +98,7 @@ pydantic==2.10.6 pydantic_core==2.27.2 PyJWT==2.10.1 pyTelegramBotAPI==4.23.0 +python-crontab==3.2.0 python-dateutil==2.9.0.post0 python-decouple==3.8 python-dotenv==1.0.1 @@ -97,6 +108,7 @@ python3-openid==3.2.0 pytz==2024.2 pywebpush==2.0.3 PyYAML==6.0.2 +redis==5.2.1 referencing==0.35.1 requests==2.32.3 requests-file==2.1.0 @@ -119,6 +131,8 @@ typing_extensions==4.12.2 tzdata==2024.1 uritemplate==4.1.1 urllib3==2.2.3 +vine==5.1.0 +wcwidth==0.2.13 whitenoise==6.7.0 xlrd==2.0.1 xlwt==1.3.0 diff --git a/docker-compose.yml b/docker-compose.yml index 14dbfba..2d0c81c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: frontend: + container_name: shop_frontend build: context: ./frontend ports: @@ -10,6 +11,7 @@ services: - default django: + container_name: shop_backend build: context: ./backend ports: @@ -18,18 +20,13 @@ services: - db volumes: - ./backend:/app - - /root/vol/shop/media:/app/media - - /root/vol/shop/static:/app/static - command: - [ - "sh", - "-c", - "python manage.py migrate && python manage.py collectstatic --no-input && python manage.py crontab add && gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3", - ] + - media_data:/app/media + - media_data:/app/static networks: - default db: + container_name: shop_db image: postgres:16 environment: POSTGRES_DB: hshop @@ -43,6 +40,7 @@ services: - default db-backup: + container_name: shop_backup build: context: ./backup depends_on: @@ -61,6 +59,46 @@ services: networks: - default + + redis: + container_name: hshop_redis + image: redis:alpine + ports: + - "6379:6379" + networks: + - default + + celery_worker: + container_name: shop_celery_worker + build: + context: ./backend + command: celery -A core worker --loglevel=info + depends_on: + - django + - redis + volumes: + - ./backend:/app + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + networks: + - default + + celery_beat: + container_name: shop_celery_beat + build: + context: ./backend + command: celery -A core beat --loglevel=info + depends_on: + - django + - redis + volumes: + - ./backend:/app + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + networks: + - default + + volumes: postgres_data: media_data: diff --git a/frontend/app.vue b/frontend/app.vue index ad1e1ff..1665ab4 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -19,11 +19,11 @@ const closeModal = () => { - + + + + + diff --git a/frontend/components/articles/ArticlesList.vue b/frontend/components/articles/ArticlesList.vue index 3231515..0e832fa 100644 --- a/frontend/components/articles/ArticlesList.vue +++ b/frontend/components/articles/ArticlesList.vue @@ -11,10 +11,14 @@ type Props = { const props = defineProps(); const { articles } = toRefs(props); +// state + +const isMobile = useMediaQuery('(max-width: 1024px)'); +