Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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}'
|
||||
@@ -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',)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('all', views.AllBlogView.as_view(), name='product-chat-view'),
|
||||
path('<int:pk>', views.BlogView.as_view(), name='product-chat-view'),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -111,6 +111,7 @@ INSTALLED_APPS = [
|
||||
'chat',
|
||||
'order',
|
||||
'home',
|
||||
'blog',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetCategories from "~/composables/api/product/useGetCategories";
|
||||
import useGetProducts, {
|
||||
type GetProductsFilters,
|
||||
} from "~/composables/api/products/useGetProducts";
|
||||
import { useParams } from "~/composables/global/useParams";
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
@@ -24,36 +24,43 @@ const in_stock = ref(JSON.parse(params.in_stock) ?? false);
|
||||
|
||||
const sliderValueDebounced = refDebounced(sliderValue, 1000);
|
||||
|
||||
const options = [
|
||||
{
|
||||
name: "میوه",
|
||||
children: [
|
||||
{ name: "سیب" },
|
||||
{ name: "موز" },
|
||||
{ name: "پرتقال" },
|
||||
{ name: "طالبی" },
|
||||
{ name: "انگور" },
|
||||
{ name: "هندوانه" },
|
||||
{ name: "خربزه" },
|
||||
{ name: "گلابی" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "سبزیجات",
|
||||
children: [
|
||||
{ name: "کلم" },
|
||||
{ name: "بروکلی" },
|
||||
{ name: "هویج" },
|
||||
{ name: "کاهو" },
|
||||
{ name: "اسفناج" },
|
||||
{ name: "چوی بوک" },
|
||||
{ name: "گل کلم" },
|
||||
{ name: "سیب زمینی" },
|
||||
],
|
||||
},
|
||||
];
|
||||
// queries
|
||||
|
||||
const { isPending: productsIsPending } = useGetProducts(params);
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
sort: params.sort ?? "newest",
|
||||
search: params.search ?? "",
|
||||
price_gte: params.price_gte ?? PRODUCT_RANGE.min,
|
||||
price_lte: params.price_lte ?? PRODUCT_RANGE.max,
|
||||
in_stock: params.in_stock ?? false,
|
||||
has_discount: params.has_discount ?? false,
|
||||
category: params.category ?? undefined,
|
||||
page: params.page ?? 1,
|
||||
};
|
||||
});
|
||||
|
||||
const { data: categories, suspense } = useGetCategories();
|
||||
|
||||
await useAsyncData(async () => {
|
||||
await suspense();
|
||||
});
|
||||
|
||||
const { isPending: productsIsPending } = useGetProducts(filters);
|
||||
|
||||
// computed
|
||||
|
||||
const allCategories = computed(() => {
|
||||
return categories.value!.map((category) => {
|
||||
return {
|
||||
name: category.name,
|
||||
children: category.subcategorys.map((sub) => {
|
||||
return {
|
||||
name: sub.name,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
@@ -130,7 +137,7 @@ watch(
|
||||
<Icon name="ci:grid" size="24" />
|
||||
دسته بندی
|
||||
</div>
|
||||
<ComboBox :options="options" v-model="params.category" />
|
||||
<ComboBox :options="allCategories" v-model="params.category" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-5">
|
||||
|
||||
@@ -20,22 +20,11 @@ export type GetProductsFilters = {
|
||||
|
||||
// composable
|
||||
|
||||
const useGetProducts = (params?: GetProductsFilters) => {
|
||||
const useGetProducts = (params?: ComputedRef<GetProductsFilters>) => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
const {
|
||||
search,
|
||||
sort,
|
||||
in_stock,
|
||||
has_discount,
|
||||
category,
|
||||
price_gte,
|
||||
price_lte,
|
||||
page,
|
||||
} = toRefs(params as GetProductsFilters);
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetProducts = async (params?: GetProductsFilters) => {
|
||||
@@ -50,7 +39,7 @@ const useGetProducts = (params?: GetProductsFilters) => {
|
||||
category: params?.category,
|
||||
price_gte: params?.price_gte,
|
||||
price_lte: params?.price_lte,
|
||||
offset: params?.page ? Number(params.page) * 9 - 9 : 0,
|
||||
offset: Number(params?.page) * 9 - 9,
|
||||
limit: 9,
|
||||
},
|
||||
}
|
||||
@@ -61,18 +50,8 @@ const useGetProducts = (params?: GetProductsFilters) => {
|
||||
|
||||
return useQuery({
|
||||
staleTime: 60 * 1000,
|
||||
queryKey: [
|
||||
QUERY_KEYS.products,
|
||||
search,
|
||||
sort,
|
||||
in_stock,
|
||||
has_discount,
|
||||
category,
|
||||
price_gte,
|
||||
price_lte,
|
||||
page,
|
||||
],
|
||||
queryFn: () => handleGetProducts(params),
|
||||
queryKey: [QUERY_KEYS.products, params],
|
||||
queryFn: () => handleGetProducts(params?.value),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+25
-17
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// import
|
||||
|
||||
import useGetProducts, {
|
||||
@@ -9,17 +8,19 @@ import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const params: GetProductsFilters = useUrlSearchParams("history", {
|
||||
initialValue: {
|
||||
search: "",
|
||||
sort: "newest",
|
||||
price_gte: PRODUCT_RANGE.min,
|
||||
price_lte: PRODUCT_RANGE.max,
|
||||
in_stock: false,
|
||||
has_discount: false,
|
||||
category: "",
|
||||
page: "1",
|
||||
},
|
||||
const params: GetProductsFilters = useUrlSearchParams("history");
|
||||
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
sort: params.sort ?? "newest",
|
||||
search: params.search ?? "",
|
||||
price_gte: params.price_gte ?? PRODUCT_RANGE.min,
|
||||
price_lte: params.price_lte ?? PRODUCT_RANGE.max,
|
||||
in_stock: params.in_stock ?? false,
|
||||
has_discount: params.has_discount ?? false,
|
||||
category: params.category ?? undefined,
|
||||
page: params.page ?? 1,
|
||||
};
|
||||
});
|
||||
|
||||
const search = ref(params.search ?? "");
|
||||
@@ -31,7 +32,7 @@ provide("params", params);
|
||||
|
||||
// queries
|
||||
|
||||
const { data, isLoading: productsIsLoading } = useGetProducts(params);
|
||||
const { data, isLoading: productsIsLoading } = useGetProducts(filters);
|
||||
|
||||
const products = computed(() => {
|
||||
return data.value?.results.flat();
|
||||
@@ -55,17 +56,17 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="w-full container flex flex-col">
|
||||
<div class="w-full flex justify-end items-center py-[5rem]">
|
||||
<div class="w-full flex justify-end items-end py-[5rem]">
|
||||
<div
|
||||
class="flex flex-col items-start gap-[1.5rem] text-black w-full"
|
||||
>
|
||||
<!-- <div class="flex-center gap-[.75rem]">
|
||||
<div class="flex-center gap-[.75rem]">
|
||||
<span>خانه</span>
|
||||
<span>/</span>
|
||||
<span>محصولات</span>
|
||||
<span>/</span>
|
||||
<span>همه</span>
|
||||
</div> -->
|
||||
</div>
|
||||
<h1 class="typo-h-3">لیست محصولات</h1>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +87,14 @@ watch(
|
||||
</div>
|
||||
</template>
|
||||
</Input>
|
||||
<FilterButton />
|
||||
<!-- <Suspense>
|
||||
<FilterButton />
|
||||
<template #fallback>
|
||||
<Skeleton
|
||||
class="!w-[10.35rem] !h-[3.35rem] !rounded-full"
|
||||
/>
|
||||
</template>
|
||||
</Suspense> -->
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
|
||||
Reference in New Issue
Block a user