From 7c1c34e1e57e7b75b2c11f032543684a29bd0a37 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 24 May 2025 22:02:16 +0330 Subject: [PATCH] view and model notif and news --- backend/account/admin.py | 13 +++- .../migrations/0026_news_usernotification.py | 43 +++++++++++ .../0027_rename_news_newsmodel_and_more.py | 21 ++++++ .../migrations/0028_newsmodel_is_read.py | 18 +++++ backend/account/models.py | 26 +++++++ backend/account/serializers.py | 29 ++++++++ backend/account/urls.py | 1 + backend/account/views.py | 74 ++++++++++++++++++- backend/core/settings/unfold_conf.py | 12 +++ 9 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 backend/account/migrations/0026_news_usernotification.py create mode 100644 backend/account/migrations/0027_rename_news_newsmodel_and_more.py create mode 100644 backend/account/migrations/0028_newsmodel_is_read.py diff --git a/backend/account/admin.py b/backend/account/admin.py index b27d4c9..c780560 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -134,4 +134,15 @@ class SecurityBreachAttemptAdmin(ModelAdmin, ImportExportModelAdmin): return format_html( svg - ) \ No newline at end of file + ) + + +@admin.register(NewsModel) +class NewsAdmin(ModelAdmin): + pass + + +@admin.register(UserNotificationModel) +class UserNotificationAdmin(ModelAdmin): + pass + diff --git a/backend/account/migrations/0026_news_usernotification.py b/backend/account/migrations/0026_news_usernotification.py new file mode 100644 index 0000000..7c7cacd --- /dev/null +++ b/backend/account/migrations/0026_news_usernotification.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.2 on 2025-04-24 22:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0025_alter_pushsubscription_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='News', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=50)), + ('content', models.TextField()), + ('image', models.ImageField(upload_to='')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='UserNotification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=50)), + ('content', models.TextField()), + ('image', models.ImageField(upload_to='')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_read', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/backend/account/migrations/0027_rename_news_newsmodel_and_more.py b/backend/account/migrations/0027_rename_news_newsmodel_and_more.py new file mode 100644 index 0000000..2c0f9e6 --- /dev/null +++ b/backend/account/migrations/0027_rename_news_newsmodel_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2025-04-24 22:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0026_news_usernotification'), + ] + + operations = [ + migrations.RenameModel( + old_name='News', + new_name='NewsModel', + ), + migrations.RenameModel( + old_name='UserNotification', + new_name='UserNotificationModel', + ), + ] diff --git a/backend/account/migrations/0028_newsmodel_is_read.py b/backend/account/migrations/0028_newsmodel_is_read.py new file mode 100644 index 0000000..e4d972b --- /dev/null +++ b/backend/account/migrations/0028_newsmodel_is_read.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-05-24 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0027_rename_news_newsmodel_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='newsmodel', + name='is_read', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index e0e389a..ad85788 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -207,6 +207,32 @@ class PushSubscription(models.Model): +class NotifBaseModel(models.Model): + title = models.CharField(max_length=50) + content = models.TextField() + image = models.ImageField() + created_at = models.DateTimeField(auto_now_add=True) + is_read = models.BooleanField(default=False) + + class Meta: + abstract = True + + def __str__(self): + return self.title + +class NewsModel(NotifBaseModel): + + def notif_type(self): + return 'NEWS' + + +class UserNotificationModel(NotifBaseModel): + user = models.ForeignKey(User, on_delete=models.CASCADE) + + def notif_type(self): + return 'USER_NOTIF' + + def get_location_from_ip(ip_address): try: response = requests.get(f"http://ip-api.com/json/{ip_address}") diff --git a/backend/account/serializers.py b/backend/account/serializers.py index 3d0c88d..48dc619 100644 --- a/backend/account/serializers.py +++ b/backend/account/serializers.py @@ -32,3 +32,32 @@ class PushSubscriptionSerializer(serializers.ModelSerializer): class Meta: model = PushSubscription fields = ('endpoint', 'keys') + + +class NewsSerializer(serializers.ModelSerializer): + notif_type = serializers.SerializerMethodField() + + class Meta: + model = NewsModel + fields = ['title', 'content', 'image', 'created_at', 'notif_type'] + + def get_notif_type(self, obj): + return 'NEWS' + +class UserNotifSerializer(serializers.ModelSerializer): + notif_type = serializers.SerializerMethodField() + + class Meta: + model = UserNotificationModel + fields = ['title', 'content', 'image', 'created_at', 'notif_type', 'is_read'] + + def get_notif_type(self, obj): + return 'USER_NOTIF' + + +class UnifiedNotifSerializer(serializers.Serializer): + def to_representation(self, instance): + if isinstance(instance, NewsModel): + return NewsSerializer(instance).data + elif isinstance(instance, UserNotificationModel): + return UserNotifSerializer(instance).data diff --git a/backend/account/urls.py b/backend/account/urls.py index 2d82f10..32734a5 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -17,4 +17,5 @@ urlpatterns = [ path('unsubscribe', views.UnsubscribeView.as_view(), name='unsubscibe'), path('attack/view/', views.ChangeViewAttack.as_view(), name='attack-view'), path('logout', views.LogoutView.as_view(), name='logout'), + path('notif/list', views.NotificationListAPIView.as_view(), name='notif-list') ] \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index a6c46b7..4ceda49 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -14,6 +14,9 @@ from django.views import View from rest_framework import serializers from rest_framework_simplejwt.tokens import RefreshToken +from itertools import chain +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes +from utils.pagination import StructurePagination class SendOTPView(APIView): @@ -268,4 +271,73 @@ from djoser.urls.jwt import views as djoser_jwt_views post=extend_schema(tags=["authentication"]) ) class TokenVerifyView(djoser_jwt_views.TokenVerifyView): - pass \ No newline at end of file + pass + +class NotificationListAPIView(APIView): + serializer_class = UnifiedNotifSerializer + permission_classes = [IsAuthenticated] + pagination_class = StructurePagination + @extend_schema( + parameters=[ + + + OpenApiParameter( + name="notif_type", + type=OpenApiTypes.STR, + description="NEWS OR USER_NOTIF", + required=False, + ), + + OpenApiParameter( + name="is_read", + type=OpenApiTypes.STR, + description="is_read filter", + required=False, + ), + + OpenApiParameter( + name="limit", + description="Number of results to return per page (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="offset", + description="The starting position of the results (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + ], + ) + def get(self, request): + notif_type = request.query_params.get('notif_type') + is_read_param = request.query_params.get('is_read') + is_read = self.str_to_bool(is_read_param) + + news_qs = NewsModel.objects.all() + user_notif_qs = UserNotificationModel.objects.filter(user=request.user) + + if is_read is not None: + news_qs = news_qs.filter(is_read=is_read) + user_notif_qs = user_notif_qs.filter(is_read=is_read) + + if notif_type is None: + notifs = sorted( + chain(news_qs, user_notif_qs), + key=lambda n: n.created_at, + reverse=True + ) + elif notif_type == 'NEWS': + notifs = news_qs + elif notif_type == 'USER_NOTIF': + notifs = user_notif_qs + else: + return Response({'detail': 'wrong notif type'}, status=status.HTTP_400_BAD_REQUEST) + + paginator = self.pagination_class() + paginated_notifs = paginator.paginate_queryset(notifs, request) + serializer = self.serializer_class(paginated_notifs, many=True) + return paginator.get_paginated_response(serializer.data) + + def str_to_bool(val): + return True if val and val == 'true' else False diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 0055b12..8c50b69 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -221,6 +221,18 @@ UNFOLD = { "link": reverse_lazy("admin:account_securitybreachattemptmodel_changelist"), "badge": 'utils.admin.new_attck_count' }, + { + "title": _("اخبار"), + "icon": "newspaper", + "link": reverse_lazy("admin:account_newsmodel_changelist"), + + }, + { + "title": _("اطلاعیه ها"), + "icon": "notifications", + "link": reverse_lazy("admin:account_usernotificationmodel_changelist"), + + }, ], },