From e8e900c39352e8698c3db340dd13fe8261e7a613 Mon Sep 17 00:00:00 2001 From: muhammadvadud Date: Thu, 27 Nov 2025 00:40:27 +0500 Subject: [PATCH] Notification api lari chiqarildi --- core/apps/accounts/admin/others.py | 18 ++++- .../0007_notification_usernotification.py | 49 ++++++++++++ core/apps/accounts/models/__init__.py | 1 + core/apps/accounts/models/notification.py | 9 +++ core/apps/api/serializers/__init__.py | 1 + .../api/serializers/notification/__init__.py | 1 + .../serializers/notification/natification.py | 49 ++++++++++++ core/apps/api/tests/user/__init__.py | 1 + core/apps/api/tests/user/test_user_like.py | 3 +- .../api/tests/user/test_user_notification.py | 78 +++++++++++++++++++ core/apps/api/urls.py | 3 +- core/apps/api/views/__init__.py | 1 + core/apps/api/views/notification/__init__.py | 1 + .../api/views/notification/notification.py | 54 +++++++++++++ 14 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 core/apps/accounts/migrations/0007_notification_usernotification.py create mode 100644 core/apps/api/serializers/notification/__init__.py create mode 100644 core/apps/api/serializers/notification/natification.py create mode 100644 core/apps/api/tests/user/test_user_notification.py create mode 100644 core/apps/api/views/notification/__init__.py create mode 100644 core/apps/api/views/notification/notification.py diff --git a/core/apps/accounts/admin/others.py b/core/apps/accounts/admin/others.py index 9eadbb6..e2821b5 100644 --- a/core/apps/accounts/admin/others.py +++ b/core/apps/accounts/admin/others.py @@ -1,7 +1,7 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.accounts.models import SearchHistory, UserLike +from core.apps.accounts.models import SearchHistory, UserLike, UserNotification, Notification @admin.register(SearchHistory) @@ -18,3 +18,19 @@ class UserLikeAdmin(ModelAdmin): "id", "__str__", ) + + +@admin.register(UserNotification) +class UserNotificationAdmin(ModelAdmin): + list_display = ( + "id", + "__str__", + ) + + +@admin.register(Notification) +class NotificationAdmin(ModelAdmin): + list_display = ( + "id", + "__str__", + ) diff --git a/core/apps/accounts/migrations/0007_notification_usernotification.py b/core/apps/accounts/migrations/0007_notification_usernotification.py new file mode 100644 index 0000000..9093280 --- /dev/null +++ b/core/apps/accounts/migrations/0007_notification_usernotification.py @@ -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', + }, + ), + ] diff --git a/core/apps/accounts/models/__init__.py b/core/apps/accounts/models/__init__.py index 92bbf71..a8fe8b7 100644 --- a/core/apps/accounts/models/__init__.py +++ b/core/apps/accounts/models/__init__.py @@ -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 diff --git a/core/apps/accounts/models/notification.py b/core/apps/accounts/models/notification.py index 6d696c4..01d4f87 100644 --- a/core/apps/accounts/models/notification.py +++ b/core/apps/accounts/models/notification.py @@ -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) diff --git a/core/apps/api/serializers/__init__.py b/core/apps/api/serializers/__init__.py index 2da7c06..0ffa197 100644 --- a/core/apps/api/serializers/__init__.py +++ b/core/apps/api/serializers/__init__.py @@ -2,3 +2,4 @@ from .category import * # noqa from .search import * # noqa from .ad import * # noqa from .user import * # noqa +from .notification import * # noqa diff --git a/core/apps/api/serializers/notification/__init__.py b/core/apps/api/serializers/notification/__init__.py new file mode 100644 index 0000000..fc19dd2 --- /dev/null +++ b/core/apps/api/serializers/notification/__init__.py @@ -0,0 +1 @@ +from .natification import * # noqa diff --git a/core/apps/api/serializers/notification/natification.py b/core/apps/api/serializers/notification/natification.py new file mode 100644 index 0000000..6e2b64a --- /dev/null +++ b/core/apps/api/serializers/notification/natification.py @@ -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" + ] + + diff --git a/core/apps/api/tests/user/__init__.py b/core/apps/api/tests/user/__init__.py index 2598d2b..34ff1ee 100644 --- a/core/apps/api/tests/user/__init__.py +++ b/core/apps/api/tests/user/__init__.py @@ -1 +1,2 @@ from .test_user_like import * # noqa +from .test_user_notification import * # noqa diff --git a/core/apps/api/tests/user/test_user_like.py b/core/apps/api/tests/user/test_user_like.py index 00b77c0..82e1d26 100644 --- a/core/apps/api/tests/user/test_user_like.py +++ b/core/apps/api/tests/user/test_user_like.py @@ -46,11 +46,10 @@ def test_list(data): @pytest.mark.django_db -def test_create(data,ad): +def test_create(data, ad): urls, client, instance = data response = client.post(urls["list"], data={"ad": ad.pk}) data_resp = response.json() - print(data_resp) assert response.status_code == 201 assert data_resp["status"] is True diff --git a/core/apps/api/tests/user/test_user_notification.py b/core/apps/api/tests/user/test_user_notification.py new file mode 100644 index 0000000..c9bf769 --- /dev/null +++ b/core/apps/api/tests/user/test_user_notification.py @@ -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 diff --git a/core/apps/api/urls.py b/core/apps/api/urls.py index 8495667..d3dd93f 100644 --- a/core/apps/api/urls.py +++ b/core/apps/api/urls.py @@ -2,9 +2,10 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from core.apps.api.views import CategoryHomeApiViewSet, CategoryViewSet, HomeAdApiView, SearchHistoryViewSet, \ - UserLikeViewSet + UserLikeViewSet, NotificationViewSet router = DefaultRouter() +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") diff --git a/core/apps/api/views/__init__.py b/core/apps/api/views/__init__.py index 2da7c06..0ffa197 100644 --- a/core/apps/api/views/__init__.py +++ b/core/apps/api/views/__init__.py @@ -2,3 +2,4 @@ from .category import * # noqa from .search import * # noqa from .ad import * # noqa from .user import * # noqa +from .notification import * # noqa diff --git a/core/apps/api/views/notification/__init__.py b/core/apps/api/views/notification/__init__.py new file mode 100644 index 0000000..9d441eb --- /dev/null +++ b/core/apps/api/views/notification/__init__.py @@ -0,0 +1 @@ +from .notification import * # noqa diff --git a/core/apps/api/views/notification/notification.py b/core/apps/api/views/notification/notification.py new file mode 100644 index 0000000..5b103db --- /dev/null +++ b/core/apps/api/views/notification/notification.py @@ -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)