diff --git a/backend/account/admin.py b/backend/account/admin.py index 4beb386..2ee028d 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -1,22 +1,38 @@ from django.contrib import admin from .models import * from unfold.admin import ModelAdmin - +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField @admin.register(User) -class UserAdmin(ModelAdmin, ImportExportModelAdmin): +class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): + filter_horizontal = [] + ordering = [] + list_filter = [] + list_display = ['phone', 'email', 'is_superuser'] - readonly_fields = ['phone'] + readonly_fields = [] exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login') import_form_class = ImportForm export_form_class = ExportForm + fieldsets = ( + ('Personal info', {'fields': ('first_name', 'last_name', 'profile_photo')}), + ('contact', {'fields': ('phone', 'email')}), + ) + + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('phone', 'password1', 'password2'), + }), + ) + compressed_fields = True warn_unsaved_form = True diff --git a/backend/blog/__init__.py b/backend/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/blog/admin.py b/backend/blog/admin.py new file mode 100644 index 0000000..07a907c --- /dev/null +++ b/backend/blog/admin.py @@ -0,0 +1,27 @@ +from django.contrib import admin +from .models import * +from unfold.admin import ModelAdmin + +from import_export.admin import ImportExportModelAdmin +from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm +from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget +from django.contrib.postgres.fields import ArrayField + + +@admin.register(BlogModel) +class BlogModelAdmin(ModelAdmin, ImportExportModelAdmin): + import_form_class = ImportForm + export_form_class = ExportForm + + + compressed_fields = True + warn_unsaved_form = True + + formfield_overrides = { + models.TextField: { + "widget": WysiwygWidget, + }, + ArrayField: { + "widget": ArrayWidget, + } + } diff --git a/backend/blog/apps.py b/backend/blog/apps.py new file mode 100644 index 0000000..94788a5 --- /dev/null +++ b/backend/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'blog' diff --git a/backend/blog/migrations/0001_initial.py b/backend/blog/migrations/0001_initial.py new file mode 100644 index 0000000..f4718fc --- /dev/null +++ b/backend/blog/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.2 on 2025-01-29 12:17 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('product', '0004_alter_subcategorymodel_parent'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BlogModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('slug', models.SlugField(blank=True, max_length=200, unique=True)), + ('content', models.TextField()), + ('summery', models.TextField()), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_published', models.BooleanField(default=False)), + ('cover_image', models.ImageField(upload_to='blog_covers/')), + ('views', models.PositiveIntegerField(default=0)), + ('meta_description', models.CharField(help_text='این فیلد را حتما پر کنید', max_length=300)), + ('meta_keywords', models.CharField(help_text='این فیلد را حتما پر کنید', max_length=300)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blogs', to=settings.AUTH_USER_MODEL)), + ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='blogs', to='product.subcategorymodel')), + ], + ), + ] diff --git a/backend/blog/migrations/__init__.py b/backend/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/blog/models.py b/backend/blog/models.py new file mode 100644 index 0000000..103521a --- /dev/null +++ b/backend/blog/models.py @@ -0,0 +1,38 @@ +from django.db import models +from account.models import User +from django.utils.text import slugify +from django.utils.timezone import now +from product.models import SubCategoryModel + +class BlogModel(models.Model): + author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blogs') + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=200, unique=True, blank=True) + content = models.TextField() + summery = models.TextField() + category = models.ForeignKey(SubCategoryModel, on_delete=models.SET_NULL, null=True, related_name='blogs') + created_at = models.DateTimeField(default=now, editable=False) + updated_at = models.DateTimeField(auto_now=True) + is_published = models.BooleanField(default=False) + cover_image = models.ImageField(upload_to='blog_covers/', blank=True) + views = models.PositiveIntegerField(default=0) + meta_description = models.CharField(max_length=300, help_text='این فیلد را حتما پر کنید') + meta_keywords = models.CharField(max_length=300, help_text='این فیلد را حتما پر کنید') + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.title) + super().save(*args, **kwargs) + + def __str__(self): + return self.title + + + +# class Comment(models.Model): +# blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='comments') +# user = models.ForeignKey(User, on_delete=models.CASCADE) +# content = models.TextField() +# created_at = models.DateTimeField(default=now) + +# def __str__(self): +# return f'Comment by {self.user} on {self.blog}' \ No newline at end of file diff --git a/backend/blog/serializers.py b/backend/blog/serializers.py new file mode 100644 index 0000000..f392204 --- /dev/null +++ b/backend/blog/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from .models import BlogModel + +class BlogSerilizer(serializers.ModelSerializer): + class Meta: + model = BlogModel + fields = ['title','author', 'slug', 'category', 'created_at', 'updated_at', 'cover_image', 'views'] + + +class AllBlogSerilizer(serializers.ModelSerializer): + class Meta: + model = BlogModel + exclude = ('is_published',) \ No newline at end of file diff --git a/backend/blog/tests.py b/backend/blog/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/blog/urls.py b/backend/blog/urls.py new file mode 100644 index 0000000..ca90f37 --- /dev/null +++ b/backend/blog/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('all', views.AllBlogView.as_view(), name='product-chat-view'), + path('', views.BlogView.as_view(), name='product-chat-view'), +] \ No newline at end of file diff --git a/backend/blog/views.py b/backend/blog/views.py new file mode 100644 index 0000000..48dcfa7 --- /dev/null +++ b/backend/blog/views.py @@ -0,0 +1,77 @@ +from django.shortcuts import render +from rest_framework.views import APIView, Response +from rest_framework import status +from .models import BlogModel +from .serializers import AllBlogSerilizer, BlogSerilizer +from django.shortcuts import get_object_or_404 +from utils.pagination import StructurePagination +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes + + + +class AllBlogView(APIView): + serializer_class = AllBlogSerilizer + pagination_class = StructurePagination + authentication_classes = [] + @extend_schema( + parameters=[ + OpenApiParameter( + name="limit", + description="لیمیتش", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="offset", + description="افستش", + required=False, + type=OpenApiTypes.INT, + ) + ], + responses={ + 200: AllBlogSerilizer(many=True), + 404: OpenApiTypes.OBJECT, + }, + ) + def get(self, request): + blogs = BlogModel.objects.filter(is_published=True) + paginator = self.pagination_class() + paginated_blogs = paginator.paginate_queryset(blogs, request) + blog_ser = self.serializer_class(instance=paginated_blogs, many=True, context={'request': request}) + return paginator.get_paginated_response(blog_ser.data) + + + +class BlogView(APIView): + serializer_class = BlogSerilizer + authentication_classes = [] + def get_client_ip(self, request): + """Helper function to get the client IP from request headers.""" + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + + def get(self, request, pk): + blog = get_object_or_404(BlogModel, pk=pk) + if blog.is_published: + # Track views using session + client_ip = self.get_client_ip(request) + session_key = f'viewed_blog_{pk}_{client_ip}' + + if not request.session.get(session_key): + blog.views += 1 + blog.save() + print(f'views {blog.views}') + print(session_key) + request.session[session_key] = True + request.session.set_expiry(3600) + + blog_ser = self.serializer_class(instance=blog, context={'request': request}) + return Response(blog_ser.data, status=status.HTTP_200_OK) + else: + return Response({'detail': 'object with the given id does not exist or is not published yet'}, + status=status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py index 7595fbe..f9e4201 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -111,6 +111,7 @@ INSTALLED_APPS = [ 'chat', 'order', 'home', + 'blog', ] MIDDLEWARE = [ diff --git a/backend/core/urls.py b/backend/core/urls.py index c2470c3..b5e2f05 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path('accounts/', include('account.urls')), path('chat/', include('chat.urls')), path('tickets/', include('ticket.urls')), + path('blogs/', include('blog.urls')), path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 12e25f4..02beac6 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -8,7 +8,7 @@ class ProductChatSerializer(serializers.ModelSerializer): is_new = serializers.SerializerMethodField() class Meta: model = ProductModel - fields = ['name', 'description', 'price', 'in_stock', 'discount', ] + fields = ['name', 'description', 'price', 'in_stock', 'discount', 'is_new'] def get_price(self, obj): dollor_price = self.context.get('dollor_price') dollar_to_dirham = 0.27 diff --git a/backend/product/views.py b/backend/product/views.py index ca9675e..e0e8f2b 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -63,8 +63,7 @@ class ProductView(APIView): class AllProductsView(APIView): serializer_class = ProductSerializer pagination_class = StructurePagination - authentication_classes = [] # Add authentication if required - + authentication_classes = [] @extend_schema( parameters=[ OpenApiParameter( @@ -172,11 +171,9 @@ class AllProductsView(APIView): products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query)) # Price filters - try: - price_gte = int(request.query_params.get('price_gte', None)) - price_lte = int(request.query_params.get('price_lte', None)) - except: - return Response({'detail': 'value error price_gte and price_lte should be a number'}, status=status.HTTP_400_BAD_REQUEST) + 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: diff --git a/frontend/components/global/products/FilterProducts.vue b/frontend/components/global/products/FilterProducts.vue index 2ce7268..bf774a2 100644 --- a/frontend/components/global/products/FilterProducts.vue +++ b/frontend/components/global/products/FilterProducts.vue @@ -1,10 +1,10 @@