diff --git a/backend/account/__init__.py b/backend/account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/account/admin.py b/backend/account/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/account/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/account/apps.py b/backend/account/apps.py new file mode 100644 index 0000000..2b08f1a --- /dev/null +++ b/backend/account/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'account' diff --git a/backend/account/migrations/__init__.py b/backend/account/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/account/models.py b/backend/account/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/account/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/account/tests.py b/backend/account/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/account/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/account/views.py b/backend/account/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/account/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/core/urls.py b/backend/core/urls.py index 96a51a9..931c2b0 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -16,7 +16,8 @@ urlpatterns = [ 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('comment/', views.CommentView.as_view(), name='comment-list'), + path('products/', include('product.urls')), path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] diff --git a/backend/entertainment/__init__.py b/backend/entertainment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/entertainment/admin.py b/backend/entertainment/admin.py new file mode 100644 index 0000000..b9ddb12 --- /dev/null +++ b/backend/entertainment/admin.py @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..b7f4c42 --- /dev/null +++ b/backend/entertainment/apps.py @@ -0,0 +1,7 @@ +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/__init__.py b/backend/entertainment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/entertainment/models.py b/backend/entertainment/models.py new file mode 100644 index 0000000..d575fc2 --- /dev/null +++ b/backend/entertainment/models.py @@ -0,0 +1,142 @@ +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/tests.py b/backend/entertainment/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/entertainment/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/entertainment/views.py b/backend/entertainment/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/entertainment/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/product/migrations/0005_categorymodel.py b/backend/product/migrations/0005_categorymodel.py new file mode 100644 index 0000000..5454b90 --- /dev/null +++ b/backend/product/migrations/0005_categorymodel.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.2 on 2024-12-13 07:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0004_rename_product_productmodel'), + ] + + operations = [ + migrations.CreateModel( + name='CategoryModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='نام دسته\u200cبندی')), + ('slug', models.SlugField(help_text='اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید', unique=True)), + ('icon', models.CharField(blank=True, max_length=100, null=True, verbose_name='آیکون دسته\u200cبندی')), + ('meta_title', models.CharField(blank=True, help_text='عنوان متا برای SEO', max_length=60, null=True, verbose_name='عنوان متا')), + ('meta_description', models.TextField(blank=True, help_text='توضیحات متا برای SEO', max_length=160, null=True, verbose_name='توضیحات متا')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='product.categorymodel', verbose_name='دسته\u200cبندی والد')), + ], + options={ + 'verbose_name': 'دسته\u200cبندی', + 'verbose_name_plural': 'دسته\u200cبندی\u200cها', + 'ordering': ['parent__id', 'id'], + }, + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 69b1034..6ee1562 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -1,6 +1,58 @@ from django.db import models from django.utils.text import slugify from django.contrib.auth.models import User +from django.urls import reverse + +class CategoryModel(models.Model): + name = models.CharField(max_length=50, verbose_name='نام دسته‌بندی') + slug = models.SlugField( + max_length=50, + unique=True, + help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید" + ) + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + related_name='children', + null=True, + blank=True, + verbose_name='دسته‌بندی والد' + ) + icon = models.CharField(max_length=100, verbose_name='آیکون دسته‌بندی', blank=True, null=True) + meta_title = models.CharField( + max_length=60, + verbose_name="عنوان متا", + help_text="عنوان متا برای SEO", + blank=True, + null=True + ) + meta_description = models.TextField( + max_length=160, + verbose_name="توضیحات متا", + help_text="توضیحات متا برای SEO", + blank=True, + null=True + ) + + class Meta: + verbose_name = "دسته‌بندی" + verbose_name_plural = "دسته‌بندی‌ها" + ordering = ['parent__id', 'id'] # Optional: to order by hierarchy + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('category_detail', kwargs={'slug': self.slug}) + + def get_breadcrumb(self): + breadcrumb = [] + category = self + while category: + breadcrumb.append(category) + category = category.parent + return breadcrumb[::-1] + class ProductModel(models.Model): name = models.CharField(max_length=255) description = models.TextField() diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 3852d8e..7abf8ee 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -1,9 +1,28 @@ from .models import * from rest_framework import serializers +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = ProductModel + fields = "__all__" + class CommentSerializer(serializers.ModelSerializer): class Meta: model = CommentModel fields = "__all__" - read_only_fields = ('show', 'product') \ No newline at end of file + read_only_fields = ('show', 'product') + + +class CategorySerializer(serializers.ModelSerializer): + children = serializers.SerializerMethodField() + + class Meta: + model = CategoryModel + fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'parent', 'children'] + + def get_children(self, obj): + children = obj.children.all() + if children.exists(): + return CategorySerializer(children, many=True).data + return [] \ No newline at end of file diff --git a/backend/product/urls.py b/backend/product/urls.py new file mode 100644 index 0000000..c7787e9 --- /dev/null +++ b/backend/product/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import AllCategories, ProductView, AllProductsView, CommentView + +urlpatterns = [ + path('', AllProductsView.as_view(), name='category-products'), + path('categories', AllCategories.as_view(), name='all-categories'), + path('', ProductView.as_view(), name='product-detail'), + path('/comments', CommentView.as_view(), name='product-comments'), + path('comments/', CommentView.as_view(), name='comment-delete'), +] \ No newline at end of file diff --git a/backend/product/views.py b/backend/product/views.py index 4a56668..1d1fdfa 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -7,6 +7,123 @@ from rest_framework.response import Response from django.db.models import Q from django.shortcuts import get_object_or_404 from rest_framework.permissions import IsAuthenticatedOrReadOnly +from utils.pagination import StructurePagination +from drf_spectacular.utils import extend_schema, OpenApiParameter +from drf_spectacular.types import OpenApiTypes + + +class AllCategories(APIView): + serializer_class = CategorySerializer + def get(self, request): + categories = CategoryModel.objects.all() + categories_ser = self.serializer_class(instance=categories, many=True) + return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK) + +class ProductView(APIView): + serializer_class = ProductModel + def get(self, request, pk): + product = get_object_or_404(ProductModel, id=pk) + product_ser = self.serializer_class(instance=product, many=False) + return Response({"product": product_ser.data}, status=status.HTTP_200_OK) + +class AllProductsView(APIView): + serializer_class = ProductSerializer + pagination_class = StructurePagination + + @extend_schema( + parameters=[ + OpenApiParameter( + name="search", + description="Search by product name or description.", + required=False, + type=OpenApiTypes.STR, + ), + OpenApiParameter( + name="category", + description="Filter by category ID.", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="price_gte", + description="Filter products with price greater than or equal to this value.", + required=False, + type=OpenApiTypes.FLOAT, + ), + OpenApiParameter( + name="price_lte", + description="Filter products with price less than or equal to this value.", + required=False, + type=OpenApiTypes.FLOAT, + ), + OpenApiParameter( + name="sort", + description=( + "Sort results by one of the following fields:\n" + "`name`, `-name`, `price`, `-price`, `discount`, `-discount`." + "\nPrefix with `-` for descending order." + ), + required=False, + type=OpenApiTypes.STR, + ), + OpenApiParameter( + name="limit", + description="Number of results to return per page (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="offset", + description="The starting position of the results (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + ], + description=( + "Retrieve products with optional filters and sorting. " + "Provide a category ID to filter products in that category and its subcategories." + ), + responses={ + 200: ProductSerializer(many=True), + 404: OpenApiTypes.OBJECT, + }, + ) + def get(self, request, pk=None): + try: + if pk: + category = Category.objects.get(pk=pk) + products = ProductModel.objects.filter(category__in=category.get_descendants(include_self=True)) + else: + products = ProductModel.objects.all() + + search_query = request.query_params.get('search', None) + if search_query: + products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query)) + + category_filter = request.query_params.get('category', None) + if category_filter: + products = products.filter(category__id=category_filter) + + price_gte = request.query_params.get('price_gte', None) + price_lte = request.query_params.get('price_lte', None) + if price_gte: + products = products.filter(price__gte=price_gte) + if price_lte: + products = products.filter(price__lte=price_lte) + + sort_by = request.query_params.get('sort', None) + if sort_by in ['name', '-name', 'price', '-price', 'discount', '-discount']: + products = products.order_by(sort_by) + else: + products = products.order_by('name') + + paginator = self.pagination_class() + paginated_products = paginator.paginate_queryset(products, request) + serializer = self.serializer_class(paginated_products, many=True) + return paginator.get_paginated_response(serializer.data) + + except Category.DoesNotExist: + return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) class CommentView(APIView): @@ -22,10 +139,10 @@ class CommentView(APIView): comment_ser = CommentSerializer(data=request.data) product = get_object_or_404(ProductModel, id=pk) if comment_ser.is_valid(): - comment_ser.save(product=product) - #TODO comment_ser.save(product=product, user=request.user) + comment_ser.save(product=product, user=request.user) return Response(comment_ser.data, status=status.HTTP_201_CREATED) return Response(comment_ser.errors, status=status.HTTP_400_BAD_REQUEST) + def delete(self, request, pk): comment = get_object_or_404(CommentModel, pk=pk) if comment.user == request.user: diff --git a/backend/utils/example/admin.py b/backend/utils/example/admin.py new file mode 100644 index 0000000..6e2aafe --- /dev/null +++ b/backend/utils/example/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from unfold.admin import ModelAdmin +from .models import NotifModel +from import_export.admin import ImportExportModelAdmin +from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm + +@admin.register(NotifModel) +class NotifModelAdmin(ModelAdmin, ImportExportModelAdmin): + import_form_class = ImportForm + export_form_class = ExportForm + compressed_fields = True + warn_unsaved_form = True + + list_display = ('subject', 'priority', 'send_time', 'send_by', 'send_to_all') + search_fields = ('subject', 'description', 'send_by__email', 'send_to_branch__name') + list_filter = ('priority', 'send_time', 'send_to_all') + ordering = ('-send_time',) + filter_horizontal = ('send_to_branch', 'read_by') diff --git a/backend/utils/example/serializer.py b/backend/utils/example/serializer.py new file mode 100644 index 0000000..edd0b7e --- /dev/null +++ b/backend/utils/example/serializer.py @@ -0,0 +1,14 @@ +from rest_framework import serializers + + +class GetNotifSerializer(serializers.ModelSerializer): + send_by = UserSerializer() + is_seen = serializers.SerializerMethodField() + read_by = UserSerializer(many=True) + class Meta: + model = NotifModel + fields = ('id', 'subject', 'priority', 'description', 'send_time', 'is_seen', 'send_by', 'read_by') + read_only_fields = ("read_by", "send_by") + def get_is_seen(self, obj): + user = self.context['user'] + return user in obj.read_by.all() \ No newline at end of file diff --git a/backend/utils/example/view.py b/backend/utils/example/view.py new file mode 100644 index 0000000..be9e10c --- /dev/null +++ b/backend/utils/example/view.py @@ -0,0 +1,37 @@ +from django.core.paginator import Paginator +from rest_framework.views import APIView +from .models import * +from .serializers import * +from rest_framework import status +from rest_framework.response import Response +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from rest_framework.permissions import IsAuthenticatedOrReadOnly + + +class CommentView(APIView): + serializer_class = CommentSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + def get(self, request, pk): + product = get_object_or_404(ProductModel, id=pk) + comments = product.comments.filter(show=True) + comments_ser = self.serializer_class(instance=comments, many=True) + return Response({'comments': comments_ser.data}, status=status.HTTP_200_OK) + + def post(self, request, pk): + comment_ser = CommentSerializer(data=request.data) + product = get_object_or_404(ProductModel, id=pk) + if comment_ser.is_valid(): + comment_ser.save(product=product, user=request.user) + return Response(comment_ser.data, status=status.HTTP_201_CREATED) + return Response(comment_ser.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, pk): + comment = get_object_or_404(CommentModel, pk=pk) + if comment.user == request.user: + comment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + else: + return Response({"detail": "شما اجازه ی پاک کردن این کامنت را ندارید"}, status=status.HTTP_403_FORBIDDEN) + +