diff --git a/config/urls.py b/config/urls.py index cf3b0af..693ca26 100644 --- a/config/urls.py +++ b/config/urls.py @@ -19,8 +19,8 @@ def home(request): urlpatterns = [ path("health/", home), path("", include("core.apps.accounts.urls")), - path("api/", include("core.apps.shared.urls")), - path("api/", include("core.apps.api.urls")), + path("api/v1/", include("core.apps.shared.urls")), + path("api/v1/", include("core.apps.api.urls")), ] urlpatterns += [ path("admin/", admin.site.urls), diff --git a/core/apps/accounts/admin/__init__.py b/core/apps/accounts/admin/__init__.py index 6e3a821..a57453a 100644 --- a/core/apps/accounts/admin/__init__.py +++ b/core/apps/accounts/admin/__init__.py @@ -1,2 +1,3 @@ from .core import * # noqa from .user import * # noqa +from .others import * # noqa diff --git a/core/apps/accounts/admin/others.py b/core/apps/accounts/admin/others.py new file mode 100644 index 0000000..1be9174 --- /dev/null +++ b/core/apps/accounts/admin/others.py @@ -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__", + ) diff --git a/core/apps/api/admin/__init__.py b/core/apps/api/admin/__init__.py index e69de29..d63c50f 100644 --- a/core/apps/api/admin/__init__.py +++ b/core/apps/api/admin/__init__.py @@ -0,0 +1 @@ +from .category import * # noqa diff --git a/core/apps/api/admin/category/__init__.py b/core/apps/api/admin/category/__init__.py new file mode 100644 index 0000000..d63c50f --- /dev/null +++ b/core/apps/api/admin/category/__init__.py @@ -0,0 +1 @@ +from .category import * # noqa diff --git a/core/apps/api/admin/category/category.py b/core/apps/api/admin/category/category.py new file mode 100644 index 0000000..5cffbde --- /dev/null +++ b/core/apps/api/admin/category/category.py @@ -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__", + ) diff --git a/core/apps/api/filters/__init__.py b/core/apps/api/filters/__init__.py new file mode 100644 index 0000000..d63c50f --- /dev/null +++ b/core/apps/api/filters/__init__.py @@ -0,0 +1 @@ +from .category import * # noqa diff --git a/core/apps/api/filters/category.py b/core/apps/api/filters/category.py new file mode 100644 index 0000000..732c333 --- /dev/null +++ b/core/apps/api/filters/category.py @@ -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", + ] diff --git a/core/apps/api/migrations/0003_category_image.py b/core/apps/api/migrations/0003_category_image.py new file mode 100644 index 0000000..1d24a56 --- /dev/null +++ b/core/apps/api/migrations/0003_category_image.py @@ -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'), + ), + ] diff --git a/core/apps/api/migrations/0004_category_category_type.py b/core/apps/api/migrations/0004_category_category_type.py new file mode 100644 index 0000000..1bb0995 --- /dev/null +++ b/core/apps/api/migrations/0004_category_category_type.py @@ -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'), + ), + ] diff --git a/core/apps/api/models/ad/category.py b/core/apps/api/models/ad/category.py index 5b6e560..224d22d 100644 --- a/core/apps/api/models/ad/category.py +++ b/core/apps/api/models/ad/category.py @@ -1,6 +1,7 @@ from django.db import models from django_core.models.base import AbstractBaseModel from django.utils.translation import gettext_lazy as _ +from core.apps.api.choices import AdCategoryType 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) show_home = models.BooleanField(default=False, verbose_name=_('Show Home')) 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): return str(self.pk) diff --git a/core/apps/api/permissions/__init__.py b/core/apps/api/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/api/serializers/__init__.py b/core/apps/api/serializers/__init__.py index e69de29..5da6a0a 100644 --- a/core/apps/api/serializers/__init__.py +++ b/core/apps/api/serializers/__init__.py @@ -0,0 +1,2 @@ +from .category import * # noqa +from .search import * # noqa diff --git a/core/apps/api/serializers/category/__init__.py b/core/apps/api/serializers/category/__init__.py new file mode 100644 index 0000000..d63c50f --- /dev/null +++ b/core/apps/api/serializers/category/__init__.py @@ -0,0 +1 @@ +from .category import * # noqa diff --git a/core/apps/api/serializers/category/category.py b/core/apps/api/serializers/category/category.py new file mode 100644 index 0000000..0e652c4 --- /dev/null +++ b/core/apps/api/serializers/category/category.py @@ -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): ... diff --git a/core/apps/api/serializers/search/__init__.py b/core/apps/api/serializers/search/__init__.py new file mode 100644 index 0000000..afeb2e5 --- /dev/null +++ b/core/apps/api/serializers/search/__init__.py @@ -0,0 +1 @@ +from .search import * # noqa diff --git a/core/apps/api/serializers/search/search.py b/core/apps/api/serializers/search/search.py new file mode 100644 index 0000000..f815e7f --- /dev/null +++ b/core/apps/api/serializers/search/search.py @@ -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 diff --git a/core/apps/api/urls.py b/core/apps/api/urls.py index 5fa41be..76446df 100644 --- a/core/apps/api/urls.py +++ b/core/apps/api/urls.py @@ -1,9 +1,9 @@ -from django.urls import path, include +from django.urls import include, path from rest_framework.routers import DefaultRouter +from core.apps.api.views import CategoryViewSet, SearchHistoryViewSet + router = DefaultRouter() - - -urlpatterns = [ - path("", include(router.urls)), -] +router.register("category", CategoryViewSet, basename="category") +router.register("search-history", SearchHistoryViewSet, basename="search-history") +urlpatterns = [path("", include(router.urls))] diff --git a/core/apps/api/views/__init__.py b/core/apps/api/views/__init__.py index e69de29..5da6a0a 100644 --- a/core/apps/api/views/__init__.py +++ b/core/apps/api/views/__init__.py @@ -0,0 +1,2 @@ +from .category import * # noqa +from .search import * # noqa diff --git a/core/apps/api/views/category/__init__.py b/core/apps/api/views/category/__init__.py new file mode 100644 index 0000000..d63c50f --- /dev/null +++ b/core/apps/api/views/category/__init__.py @@ -0,0 +1 @@ +from .category import * # noqa diff --git a/core/apps/api/views/category/category.py b/core/apps/api/views/category/category.py new file mode 100644 index 0000000..ea08a04 --- /dev/null +++ b/core/apps/api/views/category/category.py @@ -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() diff --git a/core/apps/api/views/search/__init__.py b/core/apps/api/views/search/__init__.py new file mode 100644 index 0000000..afeb2e5 --- /dev/null +++ b/core/apps/api/views/search/__init__.py @@ -0,0 +1 @@ +from .search import * # noqa diff --git a/core/apps/api/views/search/search.py b/core/apps/api/views/search/search.py new file mode 100644 index 0000000..8ac3d2f --- /dev/null +++ b/core/apps/api/views/search/search.py @@ -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 diff --git a/core/apps/shared/urls.py b/core/apps/shared/urls.py index bc256db..3b67489 100644 --- a/core/apps/shared/urls.py +++ b/core/apps/shared/urls.py @@ -1,11 +1,8 @@ -from django.urls import path, include +from django.urls import include, path from rest_framework.routers import DefaultRouter + from .views import SettingsView router = DefaultRouter() router.register("settings", SettingsView, basename="settings") - - -urlpatterns = [ - path("", include(router.urls)), -] +urlpatterns = [path("", include(router.urls))] diff --git a/test.py b/test.py new file mode 100644 index 0000000..8402e19 --- /dev/null +++ b/test.py @@ -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: 3–5 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 1–3 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 qo‘shamiz + queue.append(child) + + return f"{len(created)} ta category yaratildi!"