Compare commits

13 Commits

39 changed files with 833 additions and 17 deletions

2
Jenkinsfile vendored
View File

@@ -31,7 +31,7 @@ pipeline {
} }
stage('Checkout Code') { stage('Checkout Code') {
steps { steps {
git branch: 'main', credentialsId: 'ssh', url: 'git@github.com:JscorpTech/uzxarid.git' git branch: 'main', credentialsId: 'muhammadvadud', url: 'https://gitea.felixits.uz/uzxarid/backend.git'
} }
} }
stage('Prepare') { stage('Prepare') {

View File

@@ -5,8 +5,12 @@ CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "channels_redis.core.RedisChannelLayer", "BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [(env.str("REDIS_HOST", "redis"), env.int("REDIS_PORT", 6379))], "hosts": [
(
env.str("REDIS_HOST", "redis"),
env.int("REDIS_PORT_6379_TCP_PORT", 6379)
)
],
}, },
}, },
} }

View File

@@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from unfold.admin import ModelAdmin from unfold.admin import ModelAdmin
from core.apps.accounts.models import SearchHistory, UserLike, UserNotification, Notification from core.apps.accounts.models import SearchHistory, UserLike, UserNotification, Notification, Business
@admin.register(SearchHistory) @admin.register(SearchHistory)
@@ -34,3 +34,11 @@ class NotificationAdmin(ModelAdmin):
"id", "id",
"__str__", "__str__",
) )
@admin.register(Business)
class BusinessAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)

View File

@@ -30,6 +30,8 @@ class CustomUserAdmin(admin.UserAdmin, ModelAdmin):
"user_permissions", "user_permissions",
"role", "role",
"validated_at", "validated_at",
"account_type",
"avatar",
), ),
}, },
), ),

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-28 10:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_notification_usernotification'),
]
operations = [
migrations.AddField(
model_name='user',
name='avatar',
field=models.ImageField(default='resources/static/images/default.png', upload_to='avatars/', verbose_name='Avatar'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-28 10:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0008_user_avatar'),
]
operations = [
migrations.AlterField(
model_name='user',
name='avatar',
field=models.ImageField(default='avatars/default.png', upload_to='avatars/', verbose_name='Avatar'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-28 11:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0009_alter_user_avatar'),
]
operations = [
migrations.AlterField(
model_name='business',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='business', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]

View File

@@ -6,7 +6,8 @@ from django.contrib.auth import get_user_model
class Business(AbstractBaseModel): class Business(AbstractBaseModel):
name = models.CharField(max_length=255, verbose_name=_('Business Name')) name = models.CharField(max_length=255, verbose_name=_('Business Name'))
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE) user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name="business",
verbose_name=_('User'))
work_time = models.CharField(max_length=255, verbose_name=_('Work Time')) work_time = models.CharField(max_length=255, verbose_name=_('Work Time'))
contact = models.CharField(max_length=255, verbose_name=_('Contact')) contact = models.CharField(max_length=255, verbose_name=_('Contact'))
instagram = models.CharField(max_length=255, verbose_name=_('Instagram')) instagram = models.CharField(max_length=255, verbose_name=_('Instagram'))

View File

@@ -19,7 +19,7 @@ class User(auth_models.AbstractUser):
choices=RoleChoice, choices=RoleChoice,
default=RoleChoice.USER, default=RoleChoice.USER,
) )
avatar = models.ImageField("Avatar", upload_to="avatars/", default="avatars/default.png")
USERNAME_FIELD = "phone" USERNAME_FIELD = "phone"
objects = UserManager() objects = UserManager()

View File

@@ -2,3 +2,4 @@ from .tags import * # noqa
from .ad_top_plan import * # noqa from .ad_top_plan import * # noqa
from .ad_images import * # noqa from .ad_images import * # noqa
from .ad_variant import * # noqa from .ad_variant import * # noqa
from .ad_options import * # noqa

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
from django_filters import rest_framework as filters
from django.db.models import (
F, Case, When, FloatField, ExpressionWrapper, Subquery, OuterRef
)
from core.apps.api.models import AdVariant, AdModel
class AdFilter(filters.FilterSet):
min_price = filters.NumberFilter(field_name="real_price", lookup_expr="gte")
max_price = filters.NumberFilter(field_name="real_price", lookup_expr="lte")
size = filters.CharFilter(method="filter_by_size")
color = filters.CharFilter(method="filter_by_color")
category = filters.CharFilter(field_name="category__name", lookup_expr="icontains")
created_at = filters.DateTimeFilter(field_name="created_at", lookup_expr="gte")
has_discount = filters.BooleanFilter(method="filter_has_discount")
has_normal_user = filters.BooleanFilter(method="filter_has_normal_user")
has_business_user = filters.BooleanFilter(method="filter_has_business_user")
class Meta:
model = AdModel
fields = ["min_price", "max_price"]
def filter_has_business_user(self, queryset, name, value):
return queryset.filter(
user__account_type="business"
)
def filter_has_normal_user(self, queryset, name, value):
return queryset.filter(
user__account_type="personal"
)
def filter_has_discount(self, queryset, name, value):
return queryset.filter(
variants__discount__gte=1
).distinct()
def filter_by_size(self, queryset, name, value):
return queryset.filter(
variants__variant="Size",
variants__value__iexact=value
).distinct()
def filter_by_color(self, queryset, name, value):
return queryset.filter(
variants__variant="Color",
variants__value__iexact=value
).distinct()
def filter_queryset(self, queryset):
variant_real_price_expr = Case(
When(discount=-1, then=F("price")),
When(
discount__gte=0,
then=ExpressionWrapper(
F("price") - (F("price") * F("discount") / 100),
output_field=FloatField()
)
),
output_field=FloatField()
)
cheapest_variant_qs = (
AdVariant.objects
.filter(ad=OuterRef("pk"))
.annotate(real_price=variant_real_price_expr)
.order_by("real_price")
.values("real_price")[:1]
)
ad_real_price = F("price")
queryset = queryset.annotate(
real_price=Case(
When(
variants__isnull=False,
then=Subquery(cheapest_variant_qs)
),
default=ad_real_price,
output_field=FloatField()
)
).distinct()
return super().filter_queryset(queryset)

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-28 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0013_alter_feedback_comment'),
]
operations = [
migrations.AddField(
model_name='admodel',
name='description',
field=models.TextField(default=1, verbose_name='Description'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-27 07:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0013_alter_feedback_comment'),
]
operations = [
migrations.AddField(
model_name='category',
name='name_en',
field=models.CharField(max_length=255, null=True, verbose_name='Category Name'),
),
migrations.AddField(
model_name='category',
name='name_ru',
field=models.CharField(max_length=255, null=True, verbose_name='Category Name'),
),
migrations.AddField(
model_name='category',
name='name_uz',
field=models.CharField(max_length=255, null=True, verbose_name='Category Name'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-28 11:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0014_admodel_description'),
]
operations = [
migrations.AlterField(
model_name='adoption',
name='ad',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='api.admodel', verbose_name='Ad'),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 5.2.7 on 2025-12-02 12:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0014_category_name_en_category_name_ru_category_name_uz'),
('api', '0015_alter_adoption_ad'),
]
operations = [
]

View File

@@ -18,6 +18,7 @@ class AdModel(AbstractBaseModel):
plan = models.ForeignKey("api.AdTopPlan", on_delete=models.CASCADE, verbose_name=_("Plan")) plan = models.ForeignKey("api.AdTopPlan", on_delete=models.CASCADE, verbose_name=_("Plan"))
tags = models.ManyToManyField("api.Tags", verbose_name=_("Tags")) tags = models.ManyToManyField("api.Tags", verbose_name=_("Tags"))
image = models.ImageField(verbose_name=_("Image")) image = models.ImageField(verbose_name=_("Image"))
description = models.TextField(verbose_name=_("Description"))
@classmethod @classmethod
def _baker(cls): def _baker(cls):

View File

@@ -7,7 +7,7 @@ from core.apps.api.models import AdModel
class AdOption(AbstractBaseModel): class AdOption(AbstractBaseModel):
name = models.CharField(_("Name"), max_length=255) name = models.CharField(_("Name"), max_length=255)
value = models.CharField(_("Value"), max_length=255) value = models.CharField(_("Value"), max_length=255)
ad = models.ForeignKey(AdModel, on_delete=models.CASCADE) ad = models.ForeignKey(AdModel, on_delete=models.CASCADE, related_name="options", verbose_name=_("Ad"))
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)

View File

@@ -1 +1,2 @@
from .home_api import * # noqa from .home_api import * # noqa
from .ad import * # noqa

View File

@@ -0,0 +1,270 @@
from rest_framework import serializers
from django.db.models import Avg
from core.apps.accounts.choices import AccountType
from core.apps.api.models import AdModel, AdVariant, Category, AdImage, AdOption
from core.apps.accounts.models import UserLike
from core.apps.api.choices import AdVariantType
class AdOptionSerializer(serializers.ModelSerializer):
class Meta:
model = AdOption
fields = [
"id",
"name",
"value",
]
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name"]
class AdImageSerializer(serializers.ModelSerializer):
class Meta:
model = AdImage
fields = [
"image",
"ad_variant"
]
def to_representation(self, instance):
data = super().to_representation(instance)
if instance.ad_variant is None:
data.pop("ad_variant", None)
return data
class AdVariantSerializer(serializers.ModelSerializer):
class Meta:
model = AdVariant
fields = [
"id",
"variant",
"value",
"is_available",
"price",
"discount",
]
class BaseAdSerializer(serializers.ModelSerializer):
is_liked = serializers.SerializerMethodField()
star = serializers.SerializerMethodField()
comment_count = serializers.SerializerMethodField()
class Meta:
model = AdModel
fields = [
"id",
"name",
"price",
"image",
"is_liked",
"star",
"comment_count",
]
def get_star(self, obj):
avg = obj.feedback.aggregate(avg=Avg("star"))["avg"]
return avg or 0
def get_comment_count(self, obj):
count = obj.feedback.count()
return count or 0
def get_is_liked(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not user.is_authenticated:
return False
return UserLike.objects.filter(user=user, ad=obj).exists()
class ListAdSerializer(BaseAdSerializer):
price = serializers.SerializerMethodField()
discount = serializers.SerializerMethodField()
class Meta(BaseAdSerializer.Meta):
fields = [
"id",
"name",
"price",
"image",
"star",
"comment_count",
"discount",
"is_liked",
]
def _get_first_variant(self, obj):
if not hasattr(self, "_variant_cache"):
self._variant_cache = {}
if obj.id not in self._variant_cache:
self._variant_cache[obj.id] = obj.variants.order_by("price").first()
return self._variant_cache[obj.id]
def get_price(self, obj):
variant = self._get_first_variant(obj)
if not variant:
return obj.price
return variant.price if variant else 0
def get_discount(self, obj):
variant = self._get_first_variant(obj)
return variant.discount if variant else -1.0
class FullListAdSerializer(serializers.Serializer):
ads = ListAdSerializer(many=True)
categories = serializers.SerializerMethodField()
colors = serializers.SerializerMethodField()
sizes = serializers.SerializerMethodField()
min_price = serializers.SerializerMethodField()
max_price = serializers.SerializerMethodField()
def get_categories(self, obj):
ads = obj.get("ads", [])
category_ids = set()
categories = []
for ad in ads:
category = ad.category
if category and category.id not in category_ids:
category_ids.add(category.id)
categories.append(category)
return CategorySerializer(categories, many=True).data
def get_colors(self, obj):
ads = obj.get("ads", [])
color_values = set()
for ad in ads:
variants = getattr(ad, "variants", [])
for v in variants.all():
if v.variant == AdVariantType.COLOR:
color_values.add(v.value)
return list(color_values)
def get_sizes(self, obj):
ads = obj.get("ads", [])
size_values = set()
for ad in ads:
variants = getattr(ad, "variants", [])
for v in variants.all():
if v.variant == AdVariantType.SIZE:
size_values.add(v.value)
return list(size_values)
def get_min_price(self, obj):
ads = obj.get("ads", [])
prices = []
for ad in ads:
ad_data = ListAdSerializer(ad, context=self.context).data
price = ad_data.get("price")
if price is not None:
prices.append(price)
return min(prices) if prices else None
def get_max_price(self, obj):
ads = obj.get("ads", [])
prices = []
for ad in ads:
ad_data = ListAdSerializer(ad, context=self.context).data
price = ad_data.get("price")
if price is not None:
prices.append(price)
return max(prices) if prices else None
class RetrieveAdSerializer(BaseAdSerializer):
variants = AdVariantSerializer(many=True, read_only=True)
images = serializers.SerializerMethodField()
colors = serializers.SerializerMethodField()
sizes = serializers.SerializerMethodField()
creator = serializers.SerializerMethodField()
options = AdOptionSerializer(many=True, read_only=True)
class Meta(BaseAdSerializer.Meta):
fields = [
"id",
"name",
"price",
"image",
"star",
"comment_count",
"is_liked",
"images",
"variants",
"colors",
"sizes",
"creator",
"description",
"options"
]
def get_images(self, obj):
objects = obj.images.all()
return AdImageSerializer(objects, many=True, context=self.context).data
def get_colors(self, obj):
color_values = set()
variants = getattr(obj, "variants", [])
for v in variants.all():
if v.variant == AdVariantType.COLOR:
color_values.add(v.value)
return list(color_values)
def get_sizes(self, obj):
size_values = set()
variants = getattr(obj, "variants", [])
for v in variants.all():
if v.variant == AdVariantType.SIZE:
size_values.add(v.value)
return list(size_values)
def get_creator(self, obj):
user = obj.user
user_type = user.account_type
request = self.context.get("request")
avatar_url = request.build_absolute_uri(user.avatar.url) if user.avatar else None
if user_type == AccountType.BUSINESS:
return {
"username": user.business.name,
"avatar": avatar_url,
"create_at": user.validated_at,
"last_live": "endi qo'shamiz! waiting pls ))"
}
else:
username = f"{user.first_name} {user.last_name}"
return {
"username": username,
"avatar": avatar_url,
"create_at": user.validated_at,
"last_live": "endi qo'shamiz! waiting pls ))"
}
class CreateAdSerializer(BaseAdSerializer):
class Meta(BaseAdSerializer.Meta): ...

View File

@@ -1,6 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from django.db.models import Avg from django.db.models import Avg
from core.apps.api.models import AdModel, AdVariant from core.apps.api.models import AdModel, AdVariant
from core.apps.accounts.models import UserLike
class AdVariantSerializer(serializers.ModelSerializer): class AdVariantSerializer(serializers.ModelSerializer):
@@ -20,6 +21,7 @@ class BaseHomeAdSerializer(serializers.ModelSerializer):
comment_count = serializers.SerializerMethodField() comment_count = serializers.SerializerMethodField()
price = serializers.SerializerMethodField() price = serializers.SerializerMethodField()
discount = serializers.SerializerMethodField() discount = serializers.SerializerMethodField()
is_liked = serializers.SerializerMethodField()
class Meta: class Meta:
model = AdModel model = AdModel
@@ -31,6 +33,7 @@ class BaseHomeAdSerializer(serializers.ModelSerializer):
"star", "star",
"comment_count", "comment_count",
"discount", "discount",
"is_liked",
] ]
def _get_first_variant(self, obj): def _get_first_variant(self, obj):
@@ -48,7 +51,7 @@ class BaseHomeAdSerializer(serializers.ModelSerializer):
def get_discount(self, obj): def get_discount(self, obj):
variant = self._get_first_variant(obj) variant = self._get_first_variant(obj)
return variant.discount if variant else 0 return variant.discount if variant else -1.0
def get_star(self, obj): def get_star(self, obj):
avg = obj.feedback.aggregate(avg=Avg("star"))["avg"] avg = obj.feedback.aggregate(avg=Avg("star"))["avg"]
@@ -58,6 +61,15 @@ class BaseHomeAdSerializer(serializers.ModelSerializer):
count = obj.feedback.count() count = obj.feedback.count()
return count or 0 return count or 0
def get_is_liked(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not user.is_authenticated:
return False
return UserLike.objects.filter(user=user, ad=obj).exists()
class ListHomeAdSerializer(BaseHomeAdSerializer): class ListHomeAdSerializer(BaseHomeAdSerializer):
class Meta(BaseHomeAdSerializer.Meta): ... class Meta(BaseHomeAdSerializer.Meta): ...

View File

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

View File

@@ -0,0 +1,58 @@
from rest_framework import serializers
from core.apps.api.models import AdModel
class BaseSearchAdsSerializer(serializers.ModelSerializer):
category = serializers.SerializerMethodField()
class Meta:
model = AdModel
fields = [
"id",
"name",
"image",
"category"
]
def get_category(self, obj):
request = self.context.get("request")
lang = request.headers.get("Accept-Language", "uz")
lang = lang.split(",")[0].split("-")[0]
if lang not in ["uz", "ru", "en"]:
lang = "uz"
category = obj.category
if not category:
return None
chain = []
current = category
while current:
chain.append(current)
current = current.parent
chain = list(reversed(chain))
result = None
for cat in reversed(chain):
result = {
"id": cat.id,
"name": getattr(cat, f"name_{lang}"),
"children": result
}
return result
class ListSearchAdsSerializer(BaseSearchAdsSerializer):
class Meta(BaseSearchAdsSerializer.Meta): ...
class RetrieveSearchAdsSerializer(BaseSearchAdsSerializer):
class Meta(BaseSearchAdsSerializer.Meta): ...
class CreateSearchAdsSerializer(BaseSearchAdsSerializer):
class Meta(BaseSearchAdsSerializer.Meta): ...

View File

@@ -1 +1,2 @@
from .test_home_api import * # noqa from .test_home_api import * # noqa
from .test_ad import * # noqa

View File

@@ -0,0 +1,58 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.api.models import AdModel
@pytest.fixture
def instance(db):
return AdModel._baker()
@pytest.fixture
def api_client(instance):
client = APIClient()
##client.force_authenticate(user=instance.user)
return client, instance
@pytest.fixture
def data(api_client):
client, instance = api_client
return (
{
"list": reverse("ads-list"),
"retrieve": reverse("ads-detail", kwargs={"pk": instance.pk}),
"retrieve-not-found": reverse("ads-detail", kwargs={"pk": 1000}),
},
client,
instance,
)
@pytest.mark.django_db
def test_list(data):
urls, client, _ = data
response = client.get(urls["list"])
data_resp = response.json()
assert response.status_code == 200
assert data_resp["status"] is True
@pytest.mark.django_db
def test_retrieve(data):
urls, client, _ = data
response = client.get(urls["retrieve"])
data_resp = response.json()
assert response.status_code == 200
assert data_resp["status"] is True
@pytest.mark.django_db
def test_retrieve_not_found(data):
urls, client, _ = data
response = client.get(urls["retrieve-not-found"])
data_resp = response.json()
assert response.status_code == 404
assert data_resp["status"] is False

View File

@@ -1 +1,2 @@
from .test_search_history import * # noqa from .test_search_history import * # noqa
from .test_search_ads import * # noqa

View File

@@ -0,0 +1,38 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.api.models import AdModel
@pytest.fixture
def instance(db):
return AdModel._baker()
@pytest.fixture
def api_client(instance):
client = APIClient()
##client.force_authenticate(user=instance.user)
return client, instance
@pytest.fixture
def data(api_client):
client, instance = api_client
return (
{
"list": reverse("search-ads-list"),
},
client,
instance,
)
@pytest.mark.django_db
def test_list(data):
urls, client, _ = data
response = client.get(urls["list"])
data_resp = response.json()
assert response.status_code == 200
assert data_resp["status"] is True

View File

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

View File

@@ -0,0 +1,10 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.api.models import Category
@register(Category)
class CategoryTranslation(TranslationOptions):
fields = [
"name",
]

View File

@@ -1,10 +1,21 @@
from django.urls import include, path from django.urls import include, path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from core.apps.api.views import CategoryHomeApiViewSet, CategoryViewSet, HomeAdApiView, SearchHistoryViewSet, \ from core.apps.api.views import (
UserLikeViewSet, NotificationViewSet, BannerViewSet BannerViewSet,
CategoryHomeApiViewSet,
CategoryViewSet,
HomeAdApiView,
NotificationViewSet,
SearchAdsViewSet,
SearchHistoryViewSet,
UserLikeViewSet,
AdsView,
)
router = DefaultRouter() router = DefaultRouter()
router.register("search-ads", SearchAdsViewSet, basename="search-ads")
router.register("ads", AdsView, basename="ads")
router.register("banner", BannerViewSet, basename="banner") router.register("banner", BannerViewSet, basename="banner")
router.register("notification", NotificationViewSet, basename="notification") router.register("notification", NotificationViewSet, basename="notification")
router.register("user-like", UserLikeViewSet, basename="user-like") router.register("user-like", UserLikeViewSet, basename="user-like")

View File

@@ -1 +1,2 @@
from .home_api import * # noqa from .home_api import * # noqa
from .ad import * # noqa

View File

@@ -0,0 +1,43 @@
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import AllowAny
from drf_spectacular.utils import extend_schema
from django_core.mixins import BaseViewSetMixin
from core.apps.api.models import AdModel
from django_filters.rest_framework import DjangoFilterBackend
from core.apps.api.filters import AdFilter
from core.apps.api.serializers.ad.ad import (
FullListAdSerializer,
RetrieveAdSerializer,
CreateAdSerializer,
)
@extend_schema(tags=["Ads"])
class AdsView(BaseViewSetMixin, ReadOnlyModelViewSet):
queryset = AdModel.objects.all().order_by("-created_at")
serializer_class = FullListAdSerializer
permission_classes = [AllowAny]
filter_backends = [DjangoFilterBackend]
filterset_class = AdFilter
action_permission_classes = {}
action_serializer_class = {
"list": FullListAdSerializer,
"retrieve": RetrieveAdSerializer,
"create": CreateAdSerializer,
}
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
data = {"ads": page}
serializer = FullListAdSerializer(data, context={"request": request})
return self.get_paginated_response(serializer.data)
data = {"ads": queryset}
serializer = FullListAdSerializer(data, context={"request": request})
response = self.get_paginated_response(serializer.data)
return response

View File

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

View File

@@ -0,0 +1,48 @@
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 AllowAny
from core.apps.api.models import AdModel
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from core.apps.api.serializers.search import (
ListSearchAdsSerializer,
)
@extend_schema(tags=['Search Ads'])
class SearchAdsViewSet(BaseViewSetMixin, mixins.ListModelMixin, GenericViewSet):
queryset = AdModel.objects.all().order_by('-created_at')
serializer_class = ListSearchAdsSerializer
permission_classes = [AllowAny]
http_method_names = ['get']
filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ["name"]
action_permission_classes = {}
action_serializer_class = {
'list': ListSearchAdsSerializer,
}
def get_queryset(self):
queryset = super().get_queryset()
search_text = self.request.query_params.get('search')
if search_text:
field = f"name"
queryset = queryset.filter(**{f"{field}__icontains": search_text})
return queryset
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
if isinstance(response.data, dict) and "results" in response.data:
response.data["results"] = response.data["results"][:5]
else:
response.data = response.data[:5]
return response

View File

@@ -1,3 +1,4 @@
from .cache import * # noqa from .cache import * # noqa
from .console import * # noqa from .console import * # noqa
from .core import * # noqa from .core import * # noqa
from .language import * # noqa

8
core/utils/language.py Normal file
View File

@@ -0,0 +1,8 @@
def get_request_lang(request):
lang = request.headers.get("Accept-Language", "uz")
lang = lang.split(",")[0].split("-")[0].lower()
if lang not in ["uz", "ru", "en"]:
lang = "uz"
return lang

View File

@@ -1,2 +1,3 @@
* *
!.gitignore !.gitignore
!avatars/default.png

View File

@@ -133,7 +133,7 @@ services:
ports: ports:
- mode: ingress - mode: ingress
target: 80 target: 80
published: ${PORT:?enviromentda PORT topilmadi} published: 8034
protocol: tcp protocol: tcp
volumes: volumes:
- type: bind - type: bind