Compare commits

..

4 Commits

Author SHA1 Message Date
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
39 changed files with 9826 additions and 53 deletions

View File

@@ -1,4 +1,4 @@
#type: ignore
# type: ignore
import os
import pathlib
from typing import List, Union
@@ -49,7 +49,7 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
] + APPS
] + APPS
MODULES = [app for app in MODULES if isinstance(app, str)]
@@ -72,7 +72,6 @@ if env.bool("SILK_ENABLED", False):
"silk.middleware.SilkyMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
@@ -119,7 +118,6 @@ DATE_FORMAT = "d.m.y"
TIME_FORMAT = "H:i:s"
DATE_INPUT_FORMATS = ["%d.%m.%Y", "%Y.%d.%m", "%Y.%d.%m"]
SEEDERS = ["core.apps.accounts.seeder.UserSeeder"]
STATICFILES_DIRS = [
@@ -156,8 +154,6 @@ SILKY_PYTHON_PROFILER = True
MODELTRANSLATION_LANGUAGES = ("uz", "ru", "en")
MODELTRANSLATION_DEFAULT_LANGUAGE = "uz"
JST_LANGUAGES = [
{
"code": "uz",

View File

@@ -1,7 +1,7 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from core.apps.accounts.models import SearchHistory
from core.apps.accounts.models import SearchHistory, UserLike, UserNotification, Notification
@admin.register(SearchHistory)
@@ -10,3 +10,27 @@ class SearchHistoryAdmin(ModelAdmin):
"id",
"__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__",
)

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

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

View File

@@ -13,9 +13,9 @@ class Business(AbstractBaseModel):
facebook = models.CharField(max_length=255, verbose_name=_('Facebook'))
telegram = models.CharField(max_length=255, verbose_name=_('Telegram'))
bio = models.TextField(verbose_name=_('Bio'))
address_name = models.CharField(max_length=255, verbose_name=_('Address Name'))
longitude = models.FloatField(verbose_name=_('Longitude'))
latitude = models.FloatField(verbose_name=_('Latitude'))
address = models.CharField(max_length=255, verbose_name=_('Address Name'))
long = models.FloatField(verbose_name=_('Longitude'))
lat = models.FloatField(verbose_name=_('Latitude'))
def __str__(self):
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 core.apps.accounts.choices import NotificationType
from django.contrib.auth import get_user_model
from model_bakery import baker
class Notification(AbstractBaseModel):
@@ -12,6 +13,10 @@ class Notification(AbstractBaseModel):
long = models.FloatField(verbose_name=_("Long"))
lat = models.FloatField(verbose_name=_("Lat"))
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self):
return str(self.pk)
@@ -26,6 +31,10 @@ class UserNotification(AbstractBaseModel):
notification = models.ForeignKey(Notification, verbose_name=_("Notification"), on_delete=models.CASCADE)
is_read = models.BooleanField(verbose_name=_("Read"), default=False)
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self):
return str(self.pk)

View File

@@ -3,6 +3,7 @@ from django.db import models
from ..choices import RoleChoice, AccountType
from ..managers import UserManager
from model_bakery import baker
class User(auth_models.AbstractUser):
@@ -22,5 +23,9 @@ class User(auth_models.AbstractUser):
USERNAME_FIELD = "phone"
objects = UserManager()
@classmethod
def _baker(cls):
return baker.make(cls)
def __str__(self):
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_items import * # noqa
from .feedback import * # noqa
from .banner import * # noqa

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

@@ -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

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

View File

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

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

@@ -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 .ad import * # noqa
from .search import * # noqa
from .user import * # noqa
from .banner import * # noqa

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

@@ -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

@@ -1,9 +1,13 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from core.apps.api.views import CategoryHomeApiViewSet, CategoryViewSet, HomeAdApiView, SearchHistoryViewSet
from core.apps.api.views import CategoryHomeApiViewSet, CategoryViewSet, HomeAdApiView, SearchHistoryViewSet, \
UserLikeViewSet, NotificationViewSet, BannerViewSet
router = DefaultRouter()
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-home", CategoryHomeApiViewSet, basename="category-home")
router.register("search-history", SearchHistoryViewSet, basename="search-history")

View File

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

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

@@ -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

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