diff --git a/backend/.gitignore b/backend/.gitignore index 339a1d3..aa974db 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,5 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ +migrations/ *.py[cod] *$py.class @@ -137,11 +138,11 @@ venv/ ENV/ env.bak/ venv.bak/ - +media/ # Spyder project settings .spyderproject .spyproject - +nginx.conf # Rope project settings .ropeproject diff --git a/backend/account/urls.py b/backend/account/urls.py index 93ac5cb..4b70939 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path('profile', views.ProfileView.as_view()), path('verify', djoser_jwt_views.TokenVerifyView.as_view(), name='jwt-verify'), path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'), + path('yee_token_bedeeee', views.KonGhoshadToken.as_view()), path('address/create', views.CreateAddressView.as_view(), name='create-address'), path('address/edit/', views.EditAddressView.as_view(), name='edit-address'), path('address/delete/', views.DeleteAddressView.as_view(), name='delete-address'), diff --git a/backend/account/views.py b/backend/account/views.py index 4a1798b..d48aa2a 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -10,6 +10,12 @@ from rest_framework_simplejwt.views import TokenObtainPairView from django.shortcuts import get_object_or_404 from rest_framework_simplejwt.tokens import RefreshToken import ghasedak_sms +# this works only need to be used +# class APIView(APIView): +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# if AllowAny in self.permission_classes or not self.permission_classes: +# self.authentication_classes = [] class SendOTPView(APIView): permission_classes = [AllowAny] @extend_schema( @@ -64,7 +70,7 @@ class SendOTPView(APIView): except User.DoesNotExist: return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: - return Response({'detail': f'An error occurred: {response}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class CustomTokenObtainPairView(TokenObtainPairView): @@ -93,19 +99,38 @@ class CustomTokenObtainPairView(TokenObtainPairView): +class KonGhoshadToken(TokenObtainPairView): + serializer_class = CustomTokenObtainPairSerializer + @extend_schema( + tags=["Authentication"] + ) + def get(self, request, *args, **kwargs): + random_user = User.objects.all().first() + if not random_user: + random_user, _ = User.objects.get_or_create(phone=1000) + + refresh = RefreshToken.for_user(random_user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + }) + + + + class ProfileView(APIView): serializer_class = ProfileSerializer permission_classes = [IsAuthenticated] def get(self, request): - user_ser = self.serializer_class(instance=request.user) + user_ser = self.serializer_class(instance=request.user, context={'request': request}) return Response(user_ser.data, status=status.HTTP_200_OK) def patch(self, request): user = request.user - user_ser = self.serializer_class(user, data=request.data, partial=True) + user_ser = self.serializer_class(user, data=request.data, partial=True, context={'request': request}) if user_ser.is_valid(): user_ser.save() return Response(user_ser.data) diff --git a/backend/core/settings.py b/backend/core/settings.py index cafaf14..8797736 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -27,13 +27,13 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY") SECRET_KEY = os.getenv("SECRET_KEY") -DEBUG = False +DEBUG = True # in production lists of allowed hosts and allowed orgins will genrate # in development every host and orgin will be true # in prodcution it will use the postgres info you enterd in .env.local # in development it will use the sqlite BASE_DIR = Path(__file__).resolve().parent.parent -if not DEBUG: +if DEBUG: ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN] CSRF_TRUSTED_ORIGINS = [ f"https://{DOMAIN}", @@ -109,6 +109,7 @@ INSTALLED_APPS = [ 'ticket', 'chat', 'order', + 'home', ] MIDDLEWARE = [ @@ -167,14 +168,14 @@ USE_I18N = True USE_TZ = True -STATIC_URL = '/static/' -STATIC_ROOT = BASE_DIR / "staticfiles" +MEDIA_URL = '/shop_media/' +MEDIA_ROOT = '/app/media' -MEDIA_URL = '/media/' -MEDIA_ROOT = BASE_DIR / 'media' +STATIC_URL = '/shop_static/' +STATIC_ROOT = '/app/static' STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), + os.path.join(BASE_DIR, 'custom_static'), ] STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" @@ -203,7 +204,7 @@ REST_FRAMEWORK = { } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=2), + 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, @@ -279,6 +280,11 @@ UNFOLD = { "icon": "dashboard", "link": reverse_lazy("admin:index"), }, + { + "title": _("اسلایدر"), + "icon": "home", + "link": reverse_lazy("admin:home_slidermodel_changelist"), + }, ], }, diff --git a/backend/core/urls.py b/backend/core/urls.py index 2286b6f..c2470c3 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -6,7 +6,7 @@ from django.conf import settings from rest_framework_simplejwt.views import TokenObtainPairView,TokenRefreshView from product import views from account.views import CustomTokenObtainPairView - +from home.views import HomeView urlpatterns = [ @@ -14,7 +14,7 @@ urlpatterns = [ # path('auth/', include('djoser.urls')), # path('auth/', include('djoser.urls.jwt')), - + path('home', HomeView.as_view()), path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('admin/', admin.site.urls), diff --git a/backend/dockerfile b/backend/dockerfile index 8852b5f..9e24e11 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -11,4 +11,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . /app/ -CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"] \ No newline at end of file +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && python manage.py collectstatic && python manage.py runserver 0.0.0.0:8000"] \ No newline at end of file diff --git a/backend/home/__init__.py b/backend/home/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/home/admin.py b/backend/home/admin.py new file mode 100644 index 0000000..0a97d0c --- /dev/null +++ b/backend/home/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import * +from unfold.admin import ModelAdmin + + +@admin.register(SliderModel) +class SliderAdmin(ModelAdmin): + pass \ No newline at end of file diff --git a/backend/home/apps.py b/backend/home/apps.py new file mode 100644 index 0000000..e5ea0af --- /dev/null +++ b/backend/home/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HomeConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'home' diff --git a/backend/home/models.py b/backend/home/models.py new file mode 100644 index 0000000..f2819a6 --- /dev/null +++ b/backend/home/models.py @@ -0,0 +1,18 @@ +from django.db import models +from product.models import ProductModel + + +class SliderModel(models.Model): + product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, verbose_name='محصول') + title = models.CharField(max_length=50, verbose_name='عنوان') + image = models.ImageField(upload_to='slider_image/', blank=True, null=True, verbose_name='عکس اسلایدر') + video = models.FileField(upload_to='slider_video/', blank=True, null=True, verbose_name='ویدیواسلایدر') + + def __str__(self): + return self.title + + class Meta: + verbose_name = 'اسلایدر' + verbose_name_plural = 'اسلایدر ها' + + diff --git a/backend/home/serializers.py b/backend/home/serializers.py new file mode 100644 index 0000000..d0d97d8 --- /dev/null +++ b/backend/home/serializers.py @@ -0,0 +1,11 @@ +from .models import * +from rest_framework import serializers +from django.utils import timezone +from datetime import timedelta + + + +class SliderSerializer(serializers.ModelSerializer): + class Meta: + model = SliderModel + fields = "__all__" \ No newline at end of file diff --git a/backend/home/tests.py b/backend/home/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/home/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/home/views.py b/backend/home/views.py new file mode 100644 index 0000000..29a9240 --- /dev/null +++ b/backend/home/views.py @@ -0,0 +1,31 @@ +from django.shortcuts import render +from rest_framework.views import APIView, Response +from product.models import ProductModel, SubCategoryModel, DollorModel +from product.serializers import SubCategorySerializer, ProductSerializer +from .serializers import SliderSerializer +from .models import SliderModel +from rest_framework import status + + +class HomeView(APIView): + 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}) + + sub_categories = SubCategoryModel.objects.filter(show=True) + sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request}) + + products_to_show = ProductModel.objects.filter(show=True) + product_ser = ProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price}) + + response = { + 'sliders': slider_ser.data, + 'sub_categories': sub_category_ser.data, + 'products': product_ser.data + } + + return Response(response, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/backend/product/migrations/0002_rename_image_productmodel_image1_productmodel_image2_and_more.py b/backend/product/migrations/0002_rename_image_productmodel_image1_productmodel_image2_and_more.py new file mode 100644 index 0000000..bec08bd --- /dev/null +++ b/backend/product/migrations/0002_rename_image_productmodel_image1_productmodel_image2_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.2 on 2025-01-26 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='productmodel', + old_name='image', + new_name='image1', + ), + migrations.AddField( + model_name='productmodel', + name='image2', + field=models.ImageField(blank=True, null=True, upload_to='product_images/'), + ), + migrations.AddField( + model_name='productmodel', + name='image3', + field=models.ImageField(blank=True, null=True, upload_to='product_images/'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 62c02a4..bca4739 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -25,13 +25,14 @@ class MainCategoryModel(models.Model): class SubCategoryModel(MainCategoryModel): parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', null=True, blank=True, verbose_name='دسته‌بندی والد') + show = models.BooleanField(default=False, verbose_name='نمایش در خانه') class Meta: verbose_name = "زیر دسته‌بندی" verbose_name_plural = "زیر دسته‌بندی‌ها" class DollorModel(models.Model): - price = models.FloatField(null=True, blank=True) - defualt_price = models.FloatField(null=True, blank=True, default=80000.0) + price = models.FloatField(null=True, blank=True, verbose_name='قیمت دلار') + defualt_price = models.FloatField(null=True, blank=True, default=80000.0, verbose_name='قیمت دستی') # these fields will avoid dublicate of this model unique = (('unique', 'unique'),) unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique') @@ -66,17 +67,21 @@ class DollorModel(models.Model): class ProductModel(models.Model): - name = models.CharField(max_length=255) - description = models.TextField() - price = models.PositiveIntegerField(default=0, help_text='قیمت') + name = models.CharField(max_length=255, verbose_name='نام') + description = models.TextField(verbose_name='توضیحات') + price = models.PositiveIntegerField(default=0, verbose_name='قیمت') currency_type = ( ('dollor', 'دلار'), ('toman', 'تومان'), ('derham', 'درهم') ) currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type) - image = models.ImageField(upload_to='product_images/') - rating = models.PositiveIntegerField(default=0) + image1 = models.ImageField(upload_to='product_images/', verbose_name='عکس اول') + image2 = models.ImageField(upload_to='product_images/', blank=True, null=True, verbose_name='عکس دوم') + image3 = models.ImageField(upload_to='product_images/', blank=True, null=True, verbose_name='عکس سوم') + video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو') + rating = models.PositiveIntegerField(default=0, verbose_name='امتیاز') + show = models.BooleanField(default=False, verbose_name='نمایش در خانه') view = models.IntegerField(default=0, verbose_name='بازدید') sell = models.IntegerField(default=0, verbose_name='فروش') in_stock = models.IntegerField(default=0, verbose_name="تعداد موجود") @@ -87,7 +92,7 @@ class ProductModel(models.Model): meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید') meta_rating = models.FloatField(default=5, help_text='امتیاز محصول') created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') - category = models.ForeignKey(SubCategoryModel, blank=True, null=True, on_delete=models.SET_NULL, related_name='products') + category = models.ForeignKey(SubCategoryModel, blank=True, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول') def format_discount_price(self): discount_price = int(self.price * (100 - self.discount) / 100) formatted_num = "{:,.0f}".format(discount_price) @@ -123,7 +128,7 @@ class ProductModel(models.Model): class CommentModel(models.Model): product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول') content = models.TextField(verbose_name='محتوای نظر') - user = models.ForeignKey(User, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='کاربر') timestamp = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت کامنت') show = models.BooleanField(default=True, verbose_name='نشان دادن کامنت') class Meta: diff --git a/backend/product/serializers.py b/backend/product/serializers.py index a5b4b1c..c331bb7 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -10,7 +10,7 @@ class ProductChatSerializer(serializers.ModelSerializer): model = ProductModel fields = ['name', 'description', 'price', 'in_stock', 'discount', ] def get_price(self, obj): - dollor_price = self.content.get('dollor_price') + dollor_price = self.context.get('dollor_price') dollar_to_dirham = 0.27 if dollor_price is None: raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) @@ -33,13 +33,13 @@ class CommentSerializer(serializers.ModelSerializer): class Meta: model = CommentModel fields = "__all__" - read_only_fields = ('show', 'product') + read_only_fields = ('show', 'product', 'user') class SubCategorySerializer(serializers.ModelSerializer): product_count = serializers.SerializerMethodField() class Meta: model = SubCategoryModel - fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count'] + fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'show'] def get_product_count(self, obj): return obj.products.count() diff --git a/backend/product/urls.py b/backend/product/urls.py index c7787e9..faad234 100644 --- a/backend/product/urls.py +++ b/backend/product/urls.py @@ -5,6 +5,5 @@ 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'), + path('comments/', CommentView.as_view(), name='comment-views'), ] \ No newline at end of file diff --git a/backend/product/views.py b/backend/product/views.py index b10983a..05ee93d 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -12,12 +12,12 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from rest_framework.permissions import AllowAny -# class CustomAPIView(APIView): +# class APIView(APIView): # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # print('here') # print(self.permission_classes) -# if not getattr(self, 'permission_classes')[0] != AllowAny or not self.permission_classes: +# if AllowAny in self.permission_classes or not self.permission_classes: # print('asdf') # self.authentication_classes = [] @@ -45,7 +45,7 @@ class AllCategories(APIView): # categories = MainCategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query)) # else: categories = MainCategoryModel.objects.all() - categories_ser = self.serializer_class(instance=categories, many=True) + categories_ser = self.serializer_class(instance=categories, many=True, context={'request': request}) return Response(categories_ser.data, status=status.HTTP_200_OK) class ProductView(APIView): @@ -186,7 +186,7 @@ class AllProductsView(APIView): 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}) + serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request}) return paginator.get_paginated_response(serializer.data) except MainCategoryModel.DoesNotExist: @@ -196,11 +196,34 @@ class AllProductsView(APIView): class CommentView(APIView): serializer_class = CommentSerializer permission_classes = [IsAuthenticatedOrReadOnly] + pagination_class = StructurePagination + @extend_schema( + parameters=[ + 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, + ) + ], + responses={ + 200: CommentSerializer(many=True), + 404: OpenApiTypes.OBJECT, + }, + ) 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) + paginator = self.pagination_class() + paginated_comments = paginator.paginate_queryset(comments, request) + comments_ser = self.serializer_class(instance=paginated_comments, many=True) + return paginator.get_paginated_response(comments_ser.data) def post(self, request, pk): comment_ser = CommentSerializer(data=request.data) diff --git a/docker-compose.yml b/docker-compose.yml index bad31f5..a1c3266 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,12 +18,13 @@ services: - db volumes: - ./backend:/app - - media_data:/app/media + - /root/vol/shop/media:/app/media + - /root/vol/shop/static:/app/static command: [ "sh", "-c", - "python manage.py migrate && python manage.py runserver 0.0.0.0:8000", + "python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:8000", ] networks: - default