diff --git a/backend/account/migrations/0008_alter_user_otp_expiry_alter_user_otp_hash.py b/backend/account/migrations/0008_alter_user_otp_expiry_alter_user_otp_hash.py new file mode 100644 index 0000000..a6f8696 --- /dev/null +++ b/backend/account/migrations/0008_alter_user_otp_expiry_alter_user_otp_hash.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2024-12-31 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0007_remove_user_otp_user_otp_hash_alter_user_otp_expiry'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='otp_expiry', + field=models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تمام شدن کد یک بار مصرف'), + ), + migrations.AlterField( + model_name='user', + name='otp_hash', + field=models.CharField(blank=True, max_length=64, null=True, verbose_name='کد یک بار مصرف'), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 3f01069..68d5ff9 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -85,9 +85,8 @@ class User(AbstractBaseUser, PermissionsMixin): tokens = OutstandingToken.objects.filter(user=self) for token in tokens: BlacklistedToken.objects.get_or_create(token=token) - print('done') except Exception as e: - print(f"ridi dadash") + print(f"block list error: {e}") def __str__(self): diff --git a/backend/account/views.py b/backend/account/views.py index ad9ceba..4a1798b 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -13,6 +13,7 @@ import ghasedak_sms class SendOTPView(APIView): permission_classes = [AllowAny] @extend_schema( + tags=["Authentication"], request={ "application/json": { "type": "object", @@ -68,9 +69,9 @@ class SendOTPView(APIView): class CustomTokenObtainPairView(TokenObtainPairView): serializer_class = CustomTokenObtainPairSerializer - # @extend_schema( - # tags=["Authentication"] - # ) + @extend_schema( + tags=["Authentication"] + ) def post(self, request, *args, **kwargs): phone = request.data.get("phone") otp = request.data.get("otp") diff --git a/backend/entertainment/__init__.py b/backend/chat/__init__.py similarity index 100% rename from backend/entertainment/__init__.py rename to backend/chat/__init__.py diff --git a/backend/chat/admin.py b/backend/chat/admin.py new file mode 100644 index 0000000..63ad8f7 --- /dev/null +++ b/backend/chat/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import * +from unfold.admin import ModelAdmin + + +@admin.register(ProductChatModel) +class ProductChatAdmin(ModelAdmin): + pass \ No newline at end of file diff --git a/backend/chat/apps.py b/backend/chat/apps.py new file mode 100644 index 0000000..2fe899a --- /dev/null +++ b/backend/chat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'chat' diff --git a/backend/chat/migrations/0001_initial.py b/backend/chat/migrations/0001_initial.py new file mode 100644 index 0000000..54a0a75 --- /dev/null +++ b/backend/chat/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.2 on 2025-01-01 17:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('product', '0005_categorymodel'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ProductChatModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('thread', models.CharField(blank=True, max_length=400, null=True)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product.productmodel')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'product')}, + }, + ), + ] diff --git a/backend/entertainment/migrations/__init__.py b/backend/chat/migrations/__init__.py similarity index 100% rename from backend/entertainment/migrations/__init__.py rename to backend/chat/migrations/__init__.py diff --git a/backend/chat/models.py b/backend/chat/models.py new file mode 100644 index 0000000..4c25da9 --- /dev/null +++ b/backend/chat/models.py @@ -0,0 +1,60 @@ +from django.db import models +from account.models import User +from product.models import ProductModel +from django.conf import settings +import openai +from time import sleep +from product.serializers import ProductChatSerializer + + +ASSISTANT_ID = 'asst_1wOnCKncEHkOfp0FjOIz4Xkp' +class ProductChatModel(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + product = models.ForeignKey(ProductModel, on_delete=models.CASCADE) + thread = models.CharField(max_length=400, blank=True, null=True) + + class Meta: + unique_together = ['user', 'product'] + + def __str__(self): + return f'{self.user} - {self.product}' + + def save(self, *args, **kwargs): + if not self.thread: + client = openai.OpenAI(api_key=settings.OPENAI_API_KEY) + product_json = ProductChatSerializer(instance=self.product).data + print(product_json) + try: + + thread = client.beta.threads.create( + messages=[ + { + "role": "user", + #TODO update first message + "content": f"this is the start of the chat greet the user this chat is about the product with given detail: {product_json}", + } + ] + ) + + + run = client.beta.threads.runs.create( + thread_id=thread.id, + assistant_id=ASSISTANT_ID + ) + + + while run.status != "completed": + run = client.beta.threads.runs.retrieve( + thread_id=thread.id, + run_id=run.id + ) + sleep(1) + + + self.thread = thread.id + + except Exception as e: + print(f'error in chat class: {e}') + raise ValueError(f"Error creating OpenAI thread: {e}") + + super().save(*args, **kwargs) \ No newline at end of file diff --git a/backend/chat/serializers.py b/backend/chat/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/entertainment/tests.py b/backend/chat/tests.py similarity index 100% rename from backend/entertainment/tests.py rename to backend/chat/tests.py diff --git a/backend/chat/urls.py b/backend/chat/urls.py new file mode 100644 index 0000000..31c977a --- /dev/null +++ b/backend/chat/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('product/', views.ProductChatView.as_view(), name='product-chat-view'), +] \ No newline at end of file diff --git a/backend/chat/views.py b/backend/chat/views.py new file mode 100644 index 0000000..9792f46 --- /dev/null +++ b/backend/chat/views.py @@ -0,0 +1,162 @@ +from django.core.paginator import Paginator +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework import status +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from django.db.models import Q +from time import sleep +import json +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiParameter, + OpenApiExample, +) +from rest_framework import serializers +from rest_framework.pagination import LimitOffsetPagination + +from .models import ProductModel, ProductChatModel +from utils.pagination import StructurePagination +import openai +from django.conf import settings + +ASSISTANT_ID = 'asst_1wOnCKncEHkOfp0FjOIz4Xkp' + + +# Serializer for the `new_message` field in POST requests +class NewMessageSerializer(serializers.Serializer): + new_message = serializers.CharField( + required=True, + help_text="The message content to send to the chat." + ) + + +# Custom Pagination Class +class StructurePagination(LimitOffsetPagination): + default_limit = 10 + max_limit = 100 + + +# Documentation for the ProductChatView +@extend_schema_view( + get=extend_schema( + summary="Retrieve messages for a product chat", + parameters=[ + OpenApiParameter( + name="limit", + description="Number of results to return per page.", + required=False, + type=int, + default=10, + ), + OpenApiParameter( + name="offset", + description="The starting position of the results.", + required=False, + type=int, + default=0, + ), + ], + responses={ + 200: OpenApiExample( + "Chat messages retrieved", + value={ + "messages": [ + {"sender": "user", "content": "Hello!"}, + {"sender": "assistant", "content": "How can I help you?"} + ] + } + ), + }, + ), + post=extend_schema( + summary="Send a new message in the product chat", + request=NewMessageSerializer, + responses={ + 200: OpenApiExample( + "Message sent successfully", + value={ + "messages": [ + {"sender": "user", "content": "Hello!"}, + {"sender": "assistant", "content": "How can I help you?"} + ] + } + ), + 400: OpenApiExample( + "Error example", + value={"detail": "پیام جدید نمی‌تواند خالی باشد"}, + ), + }, + ), +) +class ProductChatView(APIView): + permission_classes = [IsAuthenticated] + pagination_class = StructurePagination + + def get(self, request, pk): + """ + Retrieve all messages for a product chat. + """ + user = request.user + product = get_object_or_404(ProductModel, id=pk) + chat, created = ProductChatModel.objects.get_or_create(user=user, product=product) + + client = openai.OpenAI(api_key=settings.OPENAI_API_KEY) + message_response = client.beta.threads.messages.list(thread_id=chat.thread) + + # Format messages + formatted_messages = [] + counter = 1 + for message in message_response.data: + for content in message.content: + if content.type == "text" and 'this is the start of the chat greet the user this chat is about the product with given detail:' not in content.text.value: + sender = 'user' if message.role == 'user' else 'ai' + formatted_messages.append({'sender': sender, 'content': content.text.value, 'id': counter}) + counter += 1 + + paginator = StructurePagination() + paginated_messages = paginator.paginate_queryset(formatted_messages, request) + return paginator.get_paginated_response(paginated_messages) + + def post(self, request, pk): + """ + Send a new message in the product chat. + """ + user = request.user + product = get_object_or_404(ProductModel, id=pk) + chat, created = ProductChatModel.objects.get_or_create(user=user, product=product) + + if created: + return Response({'detail': 'چت این کاربر با این محصول هنوز ساخته نشده'}, status=status.HTTP_400_BAD_REQUEST) + + new_message = request.data.get('new_message', '').strip() + if not new_message: + return Response({'detail': 'پیام جدید نمی‌تواند خالی باشد'}, status=status.HTTP_400_BAD_REQUEST) + + client = openai.OpenAI(api_key=settings.OPENAI_API_KEY) + + # Send the user message + client.beta.threads.messages.create( + thread_id=chat.thread, + role="user", + content=new_message + ) + + # Start the assistant's run + run = client.beta.threads.runs.create(thread_id=chat.thread, assistant_id=ASSISTANT_ID) + while run.status != "completed": + run = client.beta.threads.runs.retrieve(thread_id=chat.thread, run_id=run.id) + sleep(1) + + # Fetch the updated messages + message_response = client.beta.threads.messages.list(thread_id=chat.thread) + formatted_messages = [] + for message in message_response.data: + for content in message.content: + if content.type == "text" and 'this is the start of the chat greet the user this chat is about the product with given detail:' not in content.text.value: + sender = 'user' if message.role == 'user' else 'ai' + formatted_messages.append({'sender': sender, 'content': content.text.value}) + + formatted_messages = formatted_messages[:2] + return Response(formatted_messages, status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py index 75226ed..375486c 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -13,7 +13,7 @@ load_dotenv(".env.local") 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") @@ -95,8 +95,8 @@ INSTALLED_APPS = [ # custom apps 'product', 'account', - 'entertainment', 'ticket', + 'chat', ] MIDDLEWARE = [ @@ -285,11 +285,11 @@ UNFOLD = { # esm category model ro lower case bezar inja amir - # { - # "title": _("دسته بندی"), - # "icon": "category", - # "link": reverse_lazy("admin:product_ _changelist"), - # }, + { + "title": _("دسته بندی"), + "icon": "category", + "link": reverse_lazy("admin:product_categorymodel_changelist"), + }, { "title": _("نظرات"), "icon": "chat", @@ -306,7 +306,7 @@ UNFOLD = { "items": [ { - "title": _("users"), + "title": _("کاربران"), "icon": "person", "link": reverse_lazy("admin:account_user_changelist"), }, @@ -314,6 +314,21 @@ UNFOLD = { ], }, + { + "title": _("بخش هوش مصنوعی"), + "separator": True, + "collapsible": True, + "items": [ + + { + "title": _("چت محصول"), + "icon": "chat", + "link": reverse_lazy("admin:chat_productchatmodel_changelist"), + }, + + ], + }, + diff --git a/backend/core/urls.py b/backend/core/urls.py index 7976fc5..d8e4a3e 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -16,12 +16,13 @@ urlpatterns = [ path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), - path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + # path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('admin/', admin.site.urls), path('schema/', SpectacularAPIView.as_view(), name='schema'), # path('comment/', views.CommentView.as_view(), name='comment-list'), path('products/', include('product.urls')), path('accounts/', include('account.urls')), + path('chat/', include('chat.urls')), path('tickets/', include('ticket.urls')), path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] diff --git a/backend/entertainment/admin.py b/backend/entertainment/admin.py deleted file mode 100644 index b9ddb12..0000000 --- a/backend/entertainment/admin.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.contrib import admin -from import_export.admin import ImportExportActionModelAdmin -from unfold.admin import ModelAdmin -from .models import * - - -@admin.register(Dare) -class Dare(ModelAdmin): - list_display = ['lang1', 'is_for_adults'] - list_filter = ['is_for_adults'] - - -@admin.register(Truth) -class Truth(ModelAdmin): - list_display = ['lang1', 'is_for_adults'] - list_filter = ['is_for_adults'] - - -@admin.register(Would_you_rather) -class Would_you_rather(ModelAdmin): - list_display = ['lang1', 'is_for_adults'] - list_filter = ['is_for_adults'] - - -@admin.register(challenge) -class Challenge(ModelAdmin): - list_display = ['type'] - -@admin.register(abjad) -class abjad(ModelAdmin): - list_display = ['word', 'difficulty_type', 'answer'] - list_filter = ['difficulty_type'] - - - -@admin.register(MusicModel) -class MusicAdmin(ModelAdmin): - list_display = ['name', 'message_id', 'singer', 'category', 'trand'] - -@admin.register(MovieModel) -class MovieAdmin(ModelAdmin): - list_display = ['name', 'message_id', 'category', 'receommended'] - -@admin.register(MovieCategory) -class MovieCategoryAdmin(ModelAdmin): - pass - -@admin.register(MusicCategory) -class MusicCategoryAdmin(ModelAdmin): - pass \ No newline at end of file diff --git a/backend/entertainment/apps.py b/backend/entertainment/apps.py deleted file mode 100644 index b7f4c42..0000000 --- a/backend/entertainment/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class EntertainmentConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'entertainment' - verbose_name = 'بخش سرگرمی ها' \ No newline at end of file diff --git a/backend/entertainment/migrations/0001_initial.py b/backend/entertainment/migrations/0001_initial.py deleted file mode 100644 index 3c86881..0000000 --- a/backend/entertainment/migrations/0001_initial.py +++ /dev/null @@ -1,141 +0,0 @@ -# Generated by Django 5.1.2 on 2024-12-13 17:49 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='abjad', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('word', models.TextField(verbose_name='صورت ی سوال')), - ('image', models.ImageField(blank=True, null=True, upload_to='media/', verbose_name='عکس بازی افتابه')), - ('difficulty_type', models.CharField(choices=[('hard', 'سخت'), ('normal', 'متوسط'), ('easy', 'اسون')], max_length=13, verbose_name='سختی')), - ('answer', models.CharField(max_length=30, verbose_name='جواب')), - ('option2', models.CharField(blank=True, max_length=30, null=True, verbose_name='گزینه ی اشتباه')), - ('option3', models.CharField(blank=True, max_length=30, null=True, verbose_name='گزینه ی اشتباه')), - ('option4', models.CharField(blank=True, max_length=30, null=True, verbose_name='گزینه ی اشتباه')), - ], - options={ - 'verbose_name': 'سوال ابجد', - 'verbose_name_plural': 'سوالات ابجد', - }, - ), - migrations.CreateModel( - name='challenge', - fields=[ - ('type', models.CharField(choices=[('map', 'نقشه ی گنج'), ('prize', 'جوایز')], max_length=30, primary_key=True, serialize=False, verbose_name='نوع چالش')), - ('image', models.ImageField(upload_to='media/', verbose_name='عکس')), - ('link', models.URLField(verbose_name='لینک')), - ('text', models.TextField(verbose_name='متن توضیحات')), - ('button_text', models.CharField(max_length=40, verbose_name='متن دکمه')), - ], - options={ - 'verbose_name': 'چالش', - 'verbose_name_plural': 'چالش ها', - }, - ), - migrations.CreateModel( - name='Dare', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('lang1', models.CharField(max_length=200, verbose_name='فارسی')), - ('is_for_adults', models.BooleanField(verbose_name='+18 سوال')), - ], - options={ - 'verbose_name': 'شجاعت', - 'verbose_name_plural': 'شجاعت ها', - }, - ), - migrations.CreateModel( - name='MovieCategory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40, verbose_name='نام دسته بندی')), - ], - options={ - 'verbose_name': 'دسته بندی قیلم', - 'verbose_name_plural': 'دسته بندی فیلم ها', - }, - ), - migrations.CreateModel( - name='UploadParent', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=300, verbose_name='نام')), - ('message_id', models.CharField(max_length=40, verbose_name='ای دی پیام تلگرام')), - ], - ), - migrations.CreateModel( - name='MusicCategory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40, verbose_name='نام دسته بندی')), - ], - options={ - 'verbose_name': 'دسته بندی موزیک', - 'verbose_name_plural': 'دسته بندی موزیک ها', - }, - ), - migrations.CreateModel( - name='Truth', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('lang1', models.CharField(max_length=200, verbose_name='فارسی')), - ('is_for_adults', models.BooleanField(verbose_name='+18 سوال')), - ], - options={ - 'verbose_name': 'حقیقت', - 'verbose_name_plural': 'حقیقت ها', - }, - ), - migrations.CreateModel( - name='Would_you_rather', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('lang1', models.CharField(max_length=200, verbose_name='فارسی')), - ('is_for_adults', models.BooleanField(verbose_name='+18 سوال')), - ], - options={ - 'verbose_name': 'ترجیح میدی', - 'verbose_name_plural': 'ترجیح میدی ها', - }, - ), - migrations.CreateModel( - name='MovieModel', - fields=[ - ('uploadparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='entertainment.uploadparent')), - ('description', models.CharField(blank=True, max_length=4000, null=True, verbose_name='توضیحات فیلم')), - ('receommended', models.BooleanField(default=False, verbose_name='پیشنهادی')), - ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='entertainment.moviecategory', verbose_name='دسته بندی')), - ], - options={ - 'verbose_name': 'مدل فیلم', - 'verbose_name_plural': 'مدل فیلم ها', - }, - bases=('entertainment.uploadparent',), - ), - migrations.CreateModel( - name='MusicModel', - fields=[ - ('uploadparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='entertainment.uploadparent')), - ('lyric', models.CharField(blank=True, max_length=4000, null=True, verbose_name='متن اهنگ')), - ('singer', models.CharField(blank=True, max_length=300, null=True, verbose_name='خواننده')), - ('trand', models.BooleanField(default=False, verbose_name='ترند')), - ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='entertainment.musiccategory', verbose_name='دسته بندی')), - ], - options={ - 'verbose_name': 'مدل اهنگ', - 'verbose_name_plural': 'مدل اهنگ ها', - }, - bases=('entertainment.uploadparent',), - ), - ] diff --git a/backend/entertainment/models.py b/backend/entertainment/models.py deleted file mode 100644 index d575fc2..0000000 --- a/backend/entertainment/models.py +++ /dev/null @@ -1,142 +0,0 @@ -from django.db import models - - -class BaseModel(models.Model): - lang1 = models.CharField(max_length=200, verbose_name='فارسی') - is_for_adults = models.BooleanField(verbose_name='+18 سوال') - - class Meta: - abstract = True - - def __str__(self): - return self.lang1 - - async def get(self, *attrs): - if len(attrs) == 1: - return getattr(self, attrs[0], None) - else: - return tuple(getattr(self, attr, None) for attr in attrs) - - -class Dare(BaseModel): - class Meta: - verbose_name = 'شجاعت' - verbose_name_plural = 'شجاعت ها' - - -class Truth(BaseModel): - class Meta: - verbose_name = 'حقیقت' - verbose_name_plural = "حقیقت ها" - - -class Would_you_rather(BaseModel): - class Meta: - verbose_name = 'ترجیح میدی' - verbose_name_plural = "ترجیح میدی ها" - - -class challenge(models.Model): - challenge_type = ( - ('map', 'نقشه ی گنج'), - ('prize', 'جوایز') - ) - type = models.CharField(max_length=30, choices=challenge_type, verbose_name='نوع چالش', primary_key=True) - image = models.ImageField(upload_to='media/', verbose_name='عکس') - link = models.URLField(verbose_name='لینک') - text = models.TextField(verbose_name='متن توضیحات') - button_text = models.CharField(max_length=40, verbose_name='متن دکمه') - - def __str__(self): - return f'{self.type} - {self.text[0:50]}' - class Meta: - verbose_name = 'چالش' - verbose_name_plural = 'چالش ها' - -class abjad(models.Model): - word = models.TextField(verbose_name='صورت ی سوال') - difficulty = ( - ('hard', 'سخت'), - ('normal', 'متوسط'), - ('easy', 'اسون') - ) - image = models.ImageField(upload_to='media/', verbose_name='عکس بازی افتابه', blank=True, null=True) - difficulty_type = models.CharField(max_length=13, choices=difficulty, verbose_name='سختی') - answer = models.CharField(max_length=30, verbose_name='جواب') - option2 = models.CharField(max_length=30, verbose_name='گزینه ی اشتباه', null=True, blank=True) - option3 = models.CharField(max_length=30, verbose_name='گزینه ی اشتباه', null=True, blank=True) - option4 = models.CharField(max_length=30, verbose_name='گزینه ی اشتباه', null=True, blank=True) - def __str__(self): - return f'{self.word}' - class Meta: - verbose_name = 'سوال ابجد' - verbose_name_plural = 'سوالات ابجد' - async def get(self, *attrs): - if len(attrs) == 1: - return getattr(self, attrs[0], None) - else: - return tuple(getattr(self, attr, None) for attr in attrs) - -GAME_DATA = { - Dare: { - 'button': 'dare', - 'game_name': 'شجاعت' - }, - Truth: { - 'button': 'truth', - 'game_name': 'حقیقت' - }, - Would_you_rather: { - 'button': 'wyr', - 'game_name': 'ترجیح میدی' - } -} - - - -from django.db import models - - -class MusicCategory(models.Model): - name = models.CharField(max_length=40, verbose_name='نام دسته بندی') - def __str__(self): - return self.name - - class Meta: - verbose_name = 'دسته بندی موزیک' - verbose_name_plural = 'دسته بندی موزیک ها' - -class MovieCategory(models.Model): - name = models.CharField(max_length=40, verbose_name='نام دسته بندی') - def __str__(self): - return self.name - - class Meta: - verbose_name = 'دسته بندی قیلم' - verbose_name_plural = 'دسته بندی فیلم ها' - - -class UploadParent(models.Model): - name = models.CharField(max_length=300, verbose_name='نام') - message_id = models.CharField(max_length=40, verbose_name='ای دی پیام تلگرام') - def __str__(self): - return self.name - -class MusicModel(UploadParent): - lyric = models.CharField(verbose_name='متن اهنگ', max_length=4000, blank=True, null=True) - singer = models.CharField(max_length=300, verbose_name='خواننده', blank=True, null=True) - category = models.ForeignKey(MusicCategory, on_delete=models.CASCADE, verbose_name='دسته بندی', blank=True, null=True) - trand = models.BooleanField(default=False, verbose_name='ترند') - class Meta: - verbose_name = 'مدل اهنگ' - verbose_name_plural = 'مدل اهنگ ها' - - -class MovieModel(UploadParent): - description = models.CharField(max_length=4000,verbose_name='توضیحات فیلم', blank=True, null=True) - category = models.ForeignKey(MovieCategory, on_delete=models.CASCADE, verbose_name='دسته بندی', blank=True, null=True) - receommended = models.BooleanField(default=False, verbose_name='پیشنهادی') - class Meta: - verbose_name = 'مدل فیلم' - verbose_name_plural = 'مدل فیلم ها' - diff --git a/backend/entertainment/views.py b/backend/entertainment/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/backend/entertainment/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/backend/media/product_images/DSC00236.JPG b/backend/media/product_images/DSC00236.JPG new file mode 100644 index 0000000..bda18e8 Binary files /dev/null and b/backend/media/product_images/DSC00236.JPG differ diff --git a/backend/product/admin.py b/backend/product/admin.py index b94d761..4356486 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -7,6 +7,9 @@ from unfold.admin import ModelAdmin class ProductModelAdmin(ModelAdmin): pass +@admin.register(CategoryModel) +class CategoryModelAdmin(ModelAdmin): + pass @admin.register(CommentModel) diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 7abf8ee..44d88ec 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -5,7 +5,10 @@ class ProductSerializer(serializers.ModelSerializer): class Meta: model = ProductModel fields = "__all__" - +class ProductChatSerializer(serializers.ModelSerializer): + class Meta: + model = ProductModel + fields = ['name', 'description', 'price', 'in_stock', 'discount', ] class CommentSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/requirements.txt b/backend/requirements.txt index de429bc..5b64054 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,7 @@ aiohappyeyeballs==2.4.0 aiohttp==3.10.5 aiosignal==1.3.1 +annotated-types==0.7.0 anyio==4.6.0 asgiref==3.8.1 attrs==24.2.0 @@ -10,6 +11,7 @@ charset-normalizer==3.3.2 cryptography==43.0.3 defusedxml==0.8.0rc2 diff-match-patch==20230430 +distro==1.9.0 Django==5.1.2 django-cleanup==8.1.0 django-cors-headers==4.4.0 @@ -27,6 +29,7 @@ Faker==28.4.1 frozenlist==1.4.1 geoip2==4.8.0 ghasedak_sms==1.0.3 +ghasedakpack==0.1.13 gnupg==2.3.1 h11==0.14.0 httpagentparser==1.9.5 @@ -36,15 +39,19 @@ idna==3.10 inflection==0.5.1 jalali_core==1.0.0 jdatetime==5.0.0 +jiter==0.8.2 jsonschema==4.23.0 jsonschema-specifications==2024.10.1 maxminddb==2.6.2 multidict==6.1.0 oauthlib==3.2.2 +openai==1.58.1 pillow==10.4.0 psutil==6.0.0 psycopg2-binary==2.9.10 pycparser==2.22 +pydantic==2.10.4 +pydantic_core==2.27.2 PyJWT==2.9.0 pyTelegramBotAPI==4.23.0 python-dateutil==2.9.0.post0 @@ -64,6 +71,8 @@ social-auth-app-django==5.4.2 social-auth-core==4.5.4 sqlparse==0.5.1 tablib==3.5.0 +tqdm==4.67.1 +typing_extensions==4.12.2 tzdata==2024.1 uritemplate==4.1.1 urllib3==2.2.3 diff --git a/backend/utils/sms.py b/backend/utils/sms.py index 4822747..ad375c6 100644 --- a/backend/utils/sms.py +++ b/backend/utils/sms.py @@ -46,5 +46,3 @@ otp_input = ghasedak_sms.SendOtpInput( # Send the OTP SMS response = sms_api.send_otp_sms(otp_input) -# Print the response to check the result -print(response) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0aad9e7..199d211 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: build: context: ./frontend ports: - - "80:3000" + - "3000:3000" depends_on: - django networks: @@ -13,7 +13,7 @@ services: build: context: ./backend ports: - - "8000:8000" + - "8001:8000" depends_on: - db volumes: diff --git a/frontend/app.vue b/frontend/app.vue index 2cf361f..dd43ce5 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -5,11 +5,18 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools"; diff --git a/frontend/assets/css/animations.css b/frontend/assets/css/animations.css new file mode 100644 index 0000000..9749855 --- /dev/null +++ b/frontend/assets/css/animations.css @@ -0,0 +1,84 @@ +/* + Zoom animation +*/ + +.zoom-leave-active, +.zoom-enter-active { + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; +} + +.zoom-enter-to, +.zoom-leave-from { + opacity: 1; + transform: scale(1) translateY(0px); +} + +.zoom-enter-from, +.zoom-leave-to { + opacity: 0; + transform: scale(0.95) translateY(15px); +} + +/* + Fade animation +*/ + +.fade-leave-active, +.fade-enter-active { + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; +} + +.fade-enter-to, +.fade-leave-from { + opacity: 1; + transform: scale(1); +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; + transform: scale(0.95); +} + +/* + FadeDown animation +*/ + +.fade-down-leave-active, +.fade-down-enter-active { + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; +} + +.fade-down-enter-to, +.fade-down-leave-from { + opacity: 1; + transform: translateY(0px) scale(1); +} + +.fade-down-enter-from, +.fade-down-leave-to { + opacity: 0; + transform: translateY(10px) scale(0.95); +} + +/* + FadeRightToLeft animation +*/ + +.fade-right-to-left-leave-active, +.fade-right-to-left-enter-active { + transform-origin: bottom right; + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; +} + +.fade-right-to-left-enter-to, +.fade-right-to-left-leave-from { + opacity: 1; + transform: translateX(0px) scale(1); +} + +.fade-right-to-left-enter-from, +.fade-right-to-left-leave-to { + opacity: 0; + transform: translateX(30px) scale(0.95); +} \ No newline at end of file diff --git a/frontend/assets/css/button.comp.css b/frontend/assets/css/button.comp.css index 05a12d5..db43161 100644 --- a/frontend/assets/css/button.comp.css +++ b/frontend/assets/css/button.comp.css @@ -36,10 +36,10 @@ } &:disabled { - @apply bg-slate-50 text-slate-300; + @apply bg-slate-100 text-slate-400; svg[class~=iconify] path { - @apply stroke-slate-300; + @apply stroke-slate-400; } } } @@ -57,10 +57,10 @@ } &:disabled { - @apply bg-slate-100 text-slate-300; + @apply bg-slate-100 text-slate-400; svg[class~=iconify] path { - @apply stroke-slate-300; + @apply stroke-slate-400; } } } @@ -103,10 +103,10 @@ } &:disabled { - @apply text-slate-300; + @apply text-slate-400; svg[class~=iconify] path { - @apply stroke-slate-300; + @apply stroke-slate-400; } } } diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 2cf2d1f..40c03b1 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -1,5 +1,6 @@ @import "tailwindcss"; +@import "./animations.css"; @import "./other.utils.css"; @import "./typo.utils.css"; @import "./button.comp.css"; @@ -120,12 +121,16 @@ --breakpoint-xs: 480px; /* ANIMATIONS */ - --animate-marquee: marquee 20s linear infinite; + --animate-marquee: marquee 3s linear infinite; --animate-slide-down: slideDown 300ms ease-out; --animate-slide-up: slideUp 300ms ease-out; --animate-overlay-show: overlayShow 150ms ease-in; --animate-content-show: contentShow 150ms ease-in; + --animate-toast-hide: toastHide 100ms ease-in; + --animate-toast-in: toastSlideIn 600ms cubic-bezier(0.16, 1, 0.3, 1); + --animate-toast-out: toastSlideOut 200ms ease-out; + @keyframes marquee { to { transform: translateX(50%); @@ -169,6 +174,37 @@ transform: scale(1); } } + + @keyframes toastHide { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @keyframes toastSlideIn { + from { + opacity: 0; + transform: translateX(calc(100% + var(--viewport-padding))); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + @keyframes toastSlideOut { + from { + opacity: 1; + transform: translateX(var(--reka-toast-swipe-end-x)); + } + to { + opacity: 0; + transform: translateX(calc(100% + var(--viewport-padding))); + } + } } /* CONTAINER */ diff --git a/frontend/components/global/CategoryCard.vue b/frontend/components/global/CategoryCard.vue index f62fa67..33be331 100644 --- a/frontend/components/global/CategoryCard.vue +++ b/frontend/components/global/CategoryCard.vue @@ -2,31 +2,47 @@ // types +import { useImageColor } from "~/composables/useImageColor"; + type Props = { + id: number, category: string; count: number; description: string; picture: string; + darkLayer?: boolean; } // props -defineProps(); +const props = defineProps(); +const { id } = toRefs(props); + +// state + +const { colorObject } = useImageColor(`#category-image-${id.value}`);