Merge pull request 'feat/search' (#2) from feat/search into main

Reviewed-on: #2
This commit is contained in:
2025-11-25 09:01:44 +00:00
25 changed files with 297 additions and 14 deletions

View File

@@ -19,8 +19,8 @@ def home(request):
urlpatterns = [ urlpatterns = [
path("health/", home), path("health/", home),
path("", include("core.apps.accounts.urls")), path("", include("core.apps.accounts.urls")),
path("api/", include("core.apps.shared.urls")), path("api/v1/", include("core.apps.shared.urls")),
path("api/", include("core.apps.api.urls")), path("api/v1/", include("core.apps.api.urls")),
] ]
urlpatterns += [ urlpatterns += [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),

View File

@@ -1,2 +1,3 @@
from .core import * # noqa from .core import * # noqa
from .user import * # noqa from .user import * # noqa
from .others import * # noqa

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from core.apps.accounts.models import SearchHistory
@admin.register(SearchHistory)
class SearchHistoryAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)

View File

@@ -0,0 +1 @@
from .category import * # noqa

View File

@@ -0,0 +1 @@
from .category import * # noqa

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from core.apps.api.models import Category
@admin.register(Category)
class CategoryAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)

View File

@@ -0,0 +1 @@
from .category import * # noqa

View File

@@ -0,0 +1,15 @@
from django_filters import rest_framework as filters
from core.apps.api.models import Category
class CategoryFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = Category
fields = [
"show_home",
"id",
"category_type",
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-24 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_adtopplan_color_tags_admodel_adimage_adoption_adsize_and_more'),
]
operations = [
migrations.AddField(
model_name='category',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Image'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-25 07:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0003_category_image'),
]
operations = [
migrations.AddField(
model_name='category',
name='category_type',
field=models.CharField(choices=[('Product', 'Product'), ('Service', 'Service'), ('Auto', 'Auto'), ('Home', 'Home')], default='Product', max_length=255, verbose_name='Category Type'),
),
]

View File

@@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django_core.models.base import AbstractBaseModel from django_core.models.base import AbstractBaseModel
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.apps.api.choices import AdCategoryType
class Category(AbstractBaseModel): class Category(AbstractBaseModel):
@@ -8,6 +9,16 @@ class Category(AbstractBaseModel):
parent = models.ForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) parent = models.ForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE)
show_home = models.BooleanField(default=False, verbose_name=_('Show Home')) show_home = models.BooleanField(default=False, verbose_name=_('Show Home'))
level = models.IntegerField(default=0, verbose_name=_('Level')) level = models.IntegerField(default=0, verbose_name=_('Level'))
image = models.ImageField(verbose_name=_('Image'), null=True, blank=True)
category_type = models.CharField(max_length=255, verbose_name=_('Category Type'), choices=AdCategoryType,
default=AdCategoryType.PRODUCT)
def save(self, *args, **kwargs):
if self.parent:
self.level = self.parent.level + 1
else:
self.level = 0
super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)

View File

View File

@@ -0,0 +1,2 @@
from .category import * # noqa
from .search import * # noqa

View File

@@ -0,0 +1 @@
from .category import * # noqa

View File

@@ -0,0 +1,46 @@
from rest_framework import serializers
from core.apps.api.models import Category
class BaseCategorySerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField()
class Meta:
model = Category
fields = [
"id",
"name",
"show_home",
"level",
"category_type",
"children",
]
def get_children(self, obj):
qs = obj.children.all()
return BaseCategorySerializer(qs, many=True, context=self.context).data
class ListCategorySerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta): ...
class ListCategoryNoChildSerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta):
fields = [
"id",
"name",
"show_home",
"level",
"image",
"category_type",
]
class RetrieveCategorySerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta): ...
class CreateCategorySerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta): ...

View File

@@ -0,0 +1 @@
from .search import * # noqa

View File

@@ -0,0 +1,27 @@
from rest_framework import serializers
from core.apps.accounts.models import SearchHistory
class BaseSearchHistorySerializer(serializers.ModelSerializer):
class Meta:
model = SearchHistory
fields = [
"value",
]
class ListSearchHistorySerializer(BaseSearchHistorySerializer):
class Meta(BaseSearchHistorySerializer.Meta): ...
class RetrieveSearchHistorySerializer(BaseSearchHistorySerializer):
class Meta(BaseSearchHistorySerializer.Meta): ...
class CreateSearchHistorySerializer(BaseSearchHistorySerializer):
class Meta(BaseSearchHistorySerializer.Meta): ...
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
history = SearchHistory.objects.create(**validated_data)
return history

View File

@@ -1,9 +1,9 @@
from django.urls import path, include from django.urls import include, path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from core.apps.api.views import CategoryViewSet, SearchHistoryViewSet
router = DefaultRouter() router = DefaultRouter()
router.register("category", CategoryViewSet, basename="category")
router.register("search-history", SearchHistoryViewSet, basename="search-history")
urlpatterns = [ urlpatterns = [path("", include(router.urls))]
path("", include(router.urls)),
]

View File

@@ -0,0 +1,2 @@
from .category import * # noqa
from .search import * # noqa

View File

@@ -0,0 +1 @@
from .category import * # noqa

View File

@@ -0,0 +1,45 @@
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ReadOnlyModelViewSet
from django_core.mixins.base import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from core.apps.api.models import Category
from django_filters.rest_framework import DjangoFilterBackend
from core.apps.api.filters.category import CategoryFilter
from core.apps.api.serializers.category import (
ListCategorySerializer,
RetrieveCategorySerializer,
CreateCategorySerializer,
ListCategoryNoChildSerializer,
)
@extend_schema(tags=["Category"])
class CategoryViewSet(BaseViewSetMixin, ReadOnlyModelViewSet):
permission_classes = [AllowAny]
serializer_class = ListCategorySerializer
pagination_class = None
filter_backends = [DjangoFilterBackend]
filterset_class = CategoryFilter
action_permission_classes = {}
action_serializer_class = {
"list": ListCategorySerializer,
"retrieve": RetrieveCategorySerializer,
"create": CreateCategorySerializer,
}
def get_queryset(self):
qs = Category.objects.all()
if not self.request.query_params:
qs = qs.filter(level=0)
return qs
def get_serializer_class(self):
if "show_home" in self.request.query_params:
return ListCategoryNoChildSerializer
if hasattr(self, 'action_serializer_class'):
return self.action_serializer_class.get(self.action, self.serializer_class)
return super().get_serializer_class()

View File

@@ -0,0 +1 @@
from .search import * # noqa

View File

@@ -0,0 +1,29 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from django_core.mixins.base import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAuthenticated
from core.apps.accounts.models import SearchHistory
from core.apps.api.serializers.search import (
ListSearchHistorySerializer,
RetrieveSearchHistorySerializer,
CreateSearchHistorySerializer,
)
@extend_schema(tags=['Search'])
class SearchHistoryViewSet(BaseViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin,
mixins.DestroyModelMixin, GenericViewSet):
serializer_class = ListSearchHistorySerializer
permission_classes = [IsAuthenticated]
http_method_names = ['get', 'post', 'delete']
action_permission_classes = {}
action_serializer_class = {
'list': ListSearchHistorySerializer,
'create': CreateSearchHistorySerializer,
}
def get_queryset(self):
queryset = SearchHistory.objects.filter(user=self.request.user)
return queryset

View File

@@ -1,11 +1,8 @@
from django.urls import path, include from django.urls import include, path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import SettingsView from .views import SettingsView
router = DefaultRouter() router = DefaultRouter()
router.register("settings", SettingsView, basename="settings") router.register("settings", SettingsView, basename="settings")
urlpatterns = [path("", include(router.urls))]
urlpatterns = [
path("", include(router.urls)),
]

41
test.py Normal file
View File

@@ -0,0 +1,41 @@
import random
from core.apps.api.models import Category
def generate_categories(total=150):
Category.objects.all().delete()
created = []
queue = []
# 1) Root level: 35 ta
root_count = random.randint(3, 5)
for i in range(root_count):
cat = Category.objects.create(name=f"Category {len(created) + 1}")
created.append(cat)
queue.append(cat)
# 2) Qolganlarini yaratamiz
while len(created) < total:
if not queue:
break
parent = queue.pop(0)
# Har bir parentga 13 ta bola
children_count = random.randint(1, 3)
for _ in range(children_count):
if len(created) >= total:
break
child = Category.objects.create(
name=f"Category {len(created) + 1}",
parent=parent,
)
created.append(child)
# bola yana parent bo'lishi mumkin — shuning uchun queue ga qoshamiz
queue.append(child)
return f"{len(created)} ta category yaratildi!"