18 Commits

Author SHA1 Message Date
9d494ab34e Merge pull request 'Fix typo' (#16) from fix/stackfile into main
Reviewed-on: #16
2025-12-02 19:38:56 +00:00
1a844132e6 Fix typo 2025-12-03 00:16:16 +05:00
85dcbd8808 Merge pull request 'Fix typo' (#15) from fix/migrations into main
Reviewed-on: #15
2025-12-02 19:04:09 +00:00
5f0df931a7 Fix typo 2025-12-02 17:32:39 +05:00
d38d2cd800 Merge pull request 'Fix typo' (#14) from fix/jenkinsfile into main
Reviewed-on: #14
2025-12-02 12:24:15 +00:00
6204a57f86 Merge branch 'main' into fix/jenkinsfile 2025-12-02 12:23:38 +00:00
4af4b0c02f Fix typo 2025-12-02 17:23:13 +05:00
6402a5b418 Merge pull request 'Jenkinsfile update' (#13) from fix/jenkinsfile into main
Reviewed-on: #13
2025-12-02 11:57:42 +00:00
09d66613ea Jenkinsfile update 2025-12-02 16:56:59 +05:00
b1787200d7 Merge pull request 'Search ads uchun api chiqarildi' (#11) from feat/search_ads into main
Reviewed-on: #11
2025-12-02 11:43:23 +00:00
8337f68a01 Resolve merge conflict in urls.py 2025-12-02 16:42:35 +05:00
2022808579 Merge pull request 'Ad uchun api chiqarildi' (#12) from feat/ads into main
Reviewed-on: #12
2025-11-29 18:10:37 +00:00
97e7098b9e Ad uchun api chiqarildi 2025-11-28 16:49:38 +05:00
02fd95fe1f Search ads uchun api chiqarildi 2025-11-27 15:02:23 +05:00
94f129c446 Merge pull request 'Banner api lari' (#10) from feat/banner into main
Reviewed-on: #10
2025-11-27 07:06:55 +00:00
1211f6ebb5 Banner api lari tayyor 2025-11-27 12:02:34 +05:00
e8e900c393 Notification api lari chiqarildi 2025-11-27 00:40:27 +05:00
900f23e5f6 Ad like uchun apilar chiqarildi 2025-11-26 16:47:37 +05:00
74 changed files with 10656 additions and 67 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

@@ -72,7 +72,6 @@ if env.bool("SILK_ENABLED", False):
"silk.middleware.SilkyMiddleware", "silk.middleware.SilkyMiddleware",
] ]
ROOT_URLCONF = "config.urls" ROOT_URLCONF = "config.urls"
TEMPLATES = [ TEMPLATES = [
@@ -119,7 +118,6 @@ DATE_FORMAT = "d.m.y"
TIME_FORMAT = "H:i:s" TIME_FORMAT = "H:i:s"
DATE_INPUT_FORMATS = ["%d.%m.%Y", "%Y.%d.%m", "%Y.%d.%m"] DATE_INPUT_FORMATS = ["%d.%m.%Y", "%Y.%d.%m", "%Y.%d.%m"]
SEEDERS = ["core.apps.accounts.seeder.UserSeeder"] SEEDERS = ["core.apps.accounts.seeder.UserSeeder"]
STATICFILES_DIRS = [ STATICFILES_DIRS = [
@@ -156,8 +154,6 @@ SILKY_PYTHON_PROFILER = True
MODELTRANSLATION_LANGUAGES = ("uz", "ru", "en") MODELTRANSLATION_LANGUAGES = ("uz", "ru", "en")
MODELTRANSLATION_DEFAULT_LANGUAGE = "uz" MODELTRANSLATION_DEFAULT_LANGUAGE = "uz"
JST_LANGUAGES = [ JST_LANGUAGES = [
{ {
"code": "uz", "code": "uz",

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 from core.apps.accounts.models import SearchHistory, UserLike, UserNotification, Notification, Business
@admin.register(SearchHistory) @admin.register(SearchHistory)
@@ -10,3 +10,35 @@ class SearchHistoryAdmin(ModelAdmin):
"id", "id",
"__str__", "__str__",
) )
@admin.register(UserLike)
class UserLikeAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)
@admin.register(UserNotification)
class UserNotificationAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)
@admin.register(Notification)
class NotificationAdmin(ModelAdmin):
list_display = (
"id",
"__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,31 @@
# Generated by Django 5.2.7 on 2025-11-26 10:04
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_business_searchhistory'),
('api', '0013_alter_feedback_comment'),
]
operations = [
migrations.CreateModel(
name='UserLike',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='api.admodel', verbose_name='Ad')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'User Like',
'verbose_name_plural': 'User Likes',
'db_table': 'user_like',
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-26 10:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_userlike'),
]
operations = [
migrations.RenameField(
model_name='business',
old_name='address_name',
new_name='address',
),
migrations.RenameField(
model_name='business',
old_name='latitude',
new_name='lat',
),
migrations.RenameField(
model_name='business',
old_name='longitude',
new_name='long',
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 5.2.7 on 2025-11-26 12:24
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_rename_address_name_business_address_and_more'),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=255, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')),
('notification_type', models.CharField(choices=[('System', 'System'), ('Another', 'Another')], max_length=255, verbose_name='Type')),
('long', models.FloatField(verbose_name='Long')),
('lat', models.FloatField(verbose_name='Lat')),
],
options={
'verbose_name': 'Notification',
'verbose_name_plural': 'Notifications',
'db_table': 'notification',
},
),
migrations.CreateModel(
name='UserNotification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_read', models.BooleanField(default=False, verbose_name='Read')),
('notification', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.notification', verbose_name='Notification')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'User Notification',
'verbose_name_plural': 'User Notifications',
'db_table': 'user_notification',
},
),
]

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

@@ -5,3 +5,4 @@ from .address import * # noqa
from .business import * # noqa from .business import * # noqa
from .user_like import * # noqa from .user_like import * # noqa
from .search_history import * # noqa from .search_history import * # noqa
from .notification import * # noqa

View File

@@ -6,16 +6,17 @@ 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'))
facebook = models.CharField(max_length=255, verbose_name=_('Facebook')) facebook = models.CharField(max_length=255, verbose_name=_('Facebook'))
telegram = models.CharField(max_length=255, verbose_name=_('Telegram')) telegram = models.CharField(max_length=255, verbose_name=_('Telegram'))
bio = models.TextField(verbose_name=_('Bio')) bio = models.TextField(verbose_name=_('Bio'))
address_name = models.CharField(max_length=255, verbose_name=_('Address Name')) address = models.CharField(max_length=255, verbose_name=_('Address Name'))
longitude = models.FloatField(verbose_name=_('Longitude')) long = models.FloatField(verbose_name=_('Longitude'))
latitude = models.FloatField(verbose_name=_('Latitude')) lat = models.FloatField(verbose_name=_('Latitude'))
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)

View File

@@ -3,6 +3,7 @@ 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.accounts.choices import NotificationType from core.apps.accounts.choices import NotificationType
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from model_bakery import baker
class Notification(AbstractBaseModel): class Notification(AbstractBaseModel):
@@ -12,6 +13,10 @@ class Notification(AbstractBaseModel):
long = models.FloatField(verbose_name=_("Long")) long = models.FloatField(verbose_name=_("Long"))
lat = models.FloatField(verbose_name=_("Lat")) lat = models.FloatField(verbose_name=_("Lat"))
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)
@@ -26,6 +31,10 @@ class UserNotification(AbstractBaseModel):
notification = models.ForeignKey(Notification, verbose_name=_("Notification"), on_delete=models.CASCADE) notification = models.ForeignKey(Notification, verbose_name=_("Notification"), on_delete=models.CASCADE)
is_read = models.BooleanField(verbose_name=_("Read"), default=False) is_read = models.BooleanField(verbose_name=_("Read"), default=False)
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)

View File

@@ -3,6 +3,7 @@ from django.db import models
from ..choices import RoleChoice, AccountType from ..choices import RoleChoice, AccountType
from ..managers import UserManager from ..managers import UserManager
from model_bakery import baker
class User(auth_models.AbstractUser): class User(auth_models.AbstractUser):
@@ -18,9 +19,13 @@ 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()
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self): def __str__(self):
return self.phone return self.phone

View File

@@ -0,0 +1,23 @@
from django.contrib.auth import get_user_model
from django.db import models
from django_core.models.base import AbstractBaseModel
from django.utils.translation import gettext_lazy as _
from core.apps.api.models import AdModel
from model_bakery import baker
class UserLike(AbstractBaseModel):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_("User"), related_name="likes")
ad = models.ForeignKey(AdModel, on_delete=models.CASCADE, verbose_name=_("Ad"), related_name="likes")
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self):
return str(self.pk)
class Meta:
db_table = "user_like"
verbose_name = _("User Like")
verbose_name_plural = _("User Likes")

View File

@@ -2,3 +2,4 @@ from .category import * # noqa
from .ad import * # noqa from .ad import * # noqa
from .ad_items import * # noqa from .ad_items import * # noqa
from .feedback import * # noqa from .feedback import * # noqa
from .banner import * # noqa

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

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

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from core.apps.api.models import Banner
@admin.register(Banner)
class BannerAdmin(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,18 @@
# Generated by Django 5.2.7 on 2025-11-26 10:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0012_rename_command_feedback_comment'),
]
operations = [
migrations.AlterField(
model_name='feedback',
name='comment',
field=models.CharField(max_length=255, verbose_name='Comment'),
),
]

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,6 +1,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_core.models.base import AbstractBaseModel from django_core.models.base import AbstractBaseModel
from model_bakery import baker
class Banner(AbstractBaseModel): class Banner(AbstractBaseModel):
@@ -12,6 +13,10 @@ class Banner(AbstractBaseModel):
bg_color = models.CharField(verbose_name=_("BG Color"), max_length=255) bg_color = models.CharField(verbose_name=_("BG Color"), max_length=255)
text_color = models.CharField(verbose_name=_("Text Color"), max_length=255) text_color = models.CharField(verbose_name=_("Text Color"), max_length=255)
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)

View File

@@ -1,3 +1,6 @@
from .category import * # noqa from .category import * # noqa
from .search import * # noqa from .search import * # noqa
from .ad import * # noqa from .ad import * # noqa
from .user import * # noqa
from .notification import * # noqa
from .banner import * # noqa

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

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

View File

@@ -0,0 +1,28 @@
from rest_framework import serializers
from core.apps.api.models import Banner
class BaseBannerSerializer(serializers.ModelSerializer):
class Meta:
model = Banner
fields = [
"title",
"description",
"mobile_image",
"desktop_image",
"link",
"bg_color",
"text_color",
]
class ListBannerSerializer(BaseBannerSerializer):
class Meta(BaseBannerSerializer.Meta): ...
class RetrieveBannerSerializer(BaseBannerSerializer):
class Meta(BaseBannerSerializer.Meta): ...
class CreateBannerSerializer(BaseBannerSerializer):
class Meta(BaseBannerSerializer.Meta): ...

View File

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

View File

@@ -0,0 +1,49 @@
from rest_framework import serializers
from core.apps.accounts.models import UserNotification, Notification
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = [
"id",
"title",
"description",
"long",
"lat"
]
class BaseUserNotificationSerializer(serializers.ModelSerializer):
notification = NotificationSerializer(many=False, read_only=True)
class Meta:
model = UserNotification
fields = [
"id",
"is_read",
"notification",
"created_at",
]
class ListUserNotificationSerializer(BaseUserNotificationSerializer):
class Meta(BaseUserNotificationSerializer.Meta): ...
class RetrieveUserNotificationSerializer(BaseUserNotificationSerializer):
class Meta(BaseUserNotificationSerializer.Meta): ...
class CreateUserNotificationSerializer(BaseUserNotificationSerializer):
class Meta(BaseUserNotificationSerializer.Meta): ...
class UpdateUserNotificationSerializer(BaseUserNotificationSerializer):
class Meta(BaseUserNotificationSerializer.Meta):
fields = [
"is_read"
]

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

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

View File

@@ -0,0 +1,45 @@
from rest_framework import serializers
from core.apps.accounts.models import UserLike
from core.apps.api.models import AdModel
from core.apps.api.serializers.ad.home_api import ListHomeAdSerializer
from rest_framework.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class BaseUserLikeSerializer(serializers.ModelSerializer):
ad = ListHomeAdSerializer(many=False, read_only=True)
class Meta:
model = UserLike
fields = [
"id",
"ad",
]
class ListUserLikeSerializer(BaseUserLikeSerializer):
class Meta(BaseUserLikeSerializer.Meta): ...
class RetrieveUserLikeSerializer(BaseUserLikeSerializer):
class Meta(BaseUserLikeSerializer.Meta): ...
class CreateUserLikeSerializer(BaseUserLikeSerializer):
ad = serializers.PrimaryKeyRelatedField(queryset=AdModel.objects.all())
class Meta(BaseUserLikeSerializer.Meta): ...
def validate(self, data):
user = self.context["request"].user
ad = data["ad"]
if UserLike.objects.filter(user=user, ad=ad).exists():
raise ValidationError({"detail": _("Siz bu elonga allaqachon like bosgansiz.")})
return data
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
like = UserLike.objects.create(**validated_data)
return like

View File

@@ -1,3 +1,5 @@
from .category import * # noqa from .category import * # noqa
from .ad import * # noqa from .ad import * # noqa
from .search import * # noqa from .search import * # noqa
from .user import * # noqa
from .banner import * # noqa

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

@@ -0,0 +1 @@
from .test_banner 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 Banner
@pytest.fixture
def instance(db):
return Banner._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("banner-list"),
"retrieve": reverse("banner-detail", kwargs={"pk": instance.pk}),
"retrieve-not-found": reverse("banner-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,2 @@
from .test_user_like import * # noqa
from .test_user_notification import * # noqa

View File

@@ -0,0 +1,61 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.accounts.models import UserLike, AdModel
@pytest.fixture
def instance(db):
return UserLike._baker()
@pytest.fixture
def ad(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("user-like-list"),
"retrieve": reverse("user-like-detail", kwargs={"pk": instance.pk}),
"retrieve-not-found": reverse("user-like-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_create(data, ad):
urls, client, instance = data
response = client.post(urls["list"], data={"ad": ad.pk})
data_resp = response.json()
assert response.status_code == 201
assert data_resp["status"] is True
@pytest.mark.django_db
def test_destroy(data):
urls, client, _ = data
response = client.delete(urls["retrieve"])
assert response.status_code == 204

View File

@@ -0,0 +1,78 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.accounts.models import UserNotification
@pytest.fixture
def instance(db):
return UserNotification._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("notification-list"),
"retrieve": reverse("notification-detail", kwargs={"pk": instance.pk}),
"retrieve-not-found": reverse("notification-detail", kwargs={"pk": 1000}),
"notification-read": reverse("notification-notification-read", kwargs={"pk": instance.pk}),
"all-read": reverse("notification-all-read"),
},
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
@pytest.mark.django_db
def test_notification_reads(data):
urls, client, _ = data
response = client.post(urls["notification-read"])
data_resp = response.json()
assert response.status_code == 200
assert data_resp["status"] is True
@pytest.mark.django_db
def test_all_read(data):
urls, client, _ = data
response = client.post(urls["all-read"])
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,9 +1,24 @@
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 (
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("notification", NotificationViewSet, basename="notification")
router.register("user-like", UserLikeViewSet, basename="user-like")
router.register("category", CategoryViewSet, basename="category") router.register("category", CategoryViewSet, basename="category")
router.register("category-home", CategoryHomeApiViewSet, basename="category-home") router.register("category-home", CategoryHomeApiViewSet, basename="category-home")
router.register("search-history", SearchHistoryViewSet, basename="search-history") router.register("search-history", SearchHistoryViewSet, basename="search-history")

View File

@@ -1,4 +1,6 @@
from .category import * # noqa from .category import * # noqa
from .search import * # noqa from .search import * # noqa
from .ad import * # noqa from .ad import * # noqa
from .user import * # noqa
from .notification import * # noqa
from .banner import * # noqa

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

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

View File

@@ -0,0 +1,25 @@
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.viewsets import ReadOnlyModelViewSet
from drf_spectacular.utils import extend_schema
from django_core.mixins import BaseViewSetMixin
from core.apps.api.models import Banner
from core.apps.api.serializers.banner import (
ListBannerSerializer,
RetrieveBannerSerializer,
CreateBannerSerializer,
)
@extend_schema(tags=['Banner'])
class BannerViewSet(BaseViewSetMixin, ReadOnlyModelViewSet):
queryset = Banner.objects.all()
serializer_class = ListBannerSerializer
permission_classes = [AllowAny]
action_permission_classes = {}
action_serializers = {
'list': ListBannerSerializer,
'retrieve': RetrieveBannerSerializer,
'create': CreateBannerSerializer,
}

View File

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

View File

@@ -0,0 +1,54 @@
from xmlrpc.client import Fault
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from django_core.mixins.base import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from core.apps.accounts.models import UserNotification
from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend
from django.utils.translation import gettext_lazy as _
from core.apps.api.serializers.notification import (
ListUserNotificationSerializer,
CreateUserNotificationSerializer,
RetrieveUserNotificationSerializer,
UpdateUserNotificationSerializer
)
@extend_schema(tags=["Notification"])
class NotificationViewSet(BaseViewSetMixin, ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = ListUserNotificationSerializer
filter_backends = [DjangoFilterBackend]
action_permission_classes = {}
action_serializer_class = {
"list": ListUserNotificationSerializer,
"retrieve": RetrieveUserNotificationSerializer,
"create": CreateUserNotificationSerializer,
}
def get_queryset(self):
qs = UserNotification.objects.filter(user=self.request.user).order_by("-created_at")
return qs
@action(detail=True, methods=["post"])
def notification_read(self, request, pk=None):
notification = self.get_object()
serializer = UpdateUserNotificationSerializer(
notification,
data={"is_read": True},
partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
@action(detail=False, methods=["post"])
def all_read(self, request):
user = request.user
UserNotification.objects.filter(user=user, is_read=False).update(is_read=True)
return Response({"detail": _("Barcha xabarlar o'qilgan qilindi!")}, status=status.HTTP_200_OK)

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

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

View File

@@ -0,0 +1,28 @@
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 UserLike
from core.apps.api.serializers.user.ad_like import (
ListUserLikeSerializer,
CreateUserLikeSerializer,
)
@extend_schema(tags=['User Like'])
class UserLikeViewSet(BaseViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin,
mixins.DestroyModelMixin, GenericViewSet):
serializer_class = ListUserLikeSerializer
permission_classes = [IsAuthenticated]
http_method_names = ['get', 'post', 'delete']
action_permission_classes = {}
action_serializer_class = {
'list': ListUserLikeSerializer,
'create': CreateUserLikeSerializer,
}
def get_queryset(self):
queryset = UserLike.objects.filter(user=self.request.user).order_by('-id')
return queryset

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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