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/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..97d8b4a --- /dev/null +++ b/core/apps/api/filters/category.py @@ -0,0 +1,14 @@ +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", + ] 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/models/ad/category.py b/core/apps/api/models/ad/category.py index 5b6e560..4ee6afa 100644 --- a/core/apps/api/models/ad/category.py +++ b/core/apps/api/models/ad/category.py @@ -8,6 +8,14 @@ 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) + + 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/serializers/__init__.py b/core/apps/api/serializers/__init__.py index e69de29..d63c50f 100644 --- a/core/apps/api/serializers/__init__.py +++ b/core/apps/api/serializers/__init__.py @@ -0,0 +1 @@ +from .category 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..9436fe8 --- /dev/null +++ b/core/apps/api/serializers/category/category.py @@ -0,0 +1,44 @@ +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", + "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", + ] + + +class RetrieveCategorySerializer(BaseCategorySerializer): + class Meta(BaseCategorySerializer.Meta): ... + + +class CreateCategorySerializer(BaseCategorySerializer): + class Meta(BaseCategorySerializer.Meta): ... diff --git a/core/apps/api/urls.py b/core/apps/api/urls.py index 5fa41be..bbbe74f 100644 --- a/core/apps/api/urls.py +++ b/core/apps/api/urls.py @@ -1,9 +1,8 @@ -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 + router = DefaultRouter() - - -urlpatterns = [ - path("", include(router.urls)), -] +router.register("category", CategoryViewSet, basename="category") +urlpatterns = [path("", include(router.urls))] diff --git a/core/apps/api/views/__init__.py b/core/apps/api/views/__init__.py index e69de29..d63c50f 100644 --- a/core/apps/api/views/__init__.py +++ b/core/apps/api/views/__init__.py @@ -0,0 +1 @@ +from .category 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/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!"