Files
MathApp/notifications/views.py
Zareian 7ecb337263 Finishing a lesson or island needs recorded answers and an unlocked island; auto DB backup before migrate; throttles on 15 more routes
- content: POST nodes/{id}/quiz/complete and lessons/{id}/complete refuse a locked island
  (403 node_locked, same rule as /api/home) and a user who has not answered every question
  (409 answers_missing). Before, only a user with zero hearts and no answers was refused, so
  anyone else finished any island, locked or not, and finishing a locked island's lessons
  unlocked it. A node or lesson with no questions still passes when unlocked, with no XP.
  The client can no longer understate totalQuestions to buy stars.
- throttle_scope on 15 routes that had none (question bank without login, intro, hearts,
  notifications, tickets, plans, subscription status, orders, offers, affiliate, assistant
  history and feedback, logout).
- admin: OTPCode and Session can only be added/changed by a superuser.
- deploy.sh: integrity-checked DB backup before migrate (keeps the last 2); no migrate without it.
- docs and CLAUDE.md updated to say what is verified and what is not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:30:24 +03:30

76 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from .models import Notification
from .serializers import (
MarkAllReadResponseSerializer, MarkReadResponseSerializer, NotificationListResponseSerializer,
)
# سقفِ تعدادِ اعلانِ برگشتی؛ جلوی رشدِ نامحدودِ پاسخ برای کاربرِ قدیمی رو
# می‌گیره. صفحه‌بندیِ واقعی (cursor) وقتی فرانت infinite-scroll اضافه کنه.
NOTIFICATIONS_MAX = 100
class NotificationListView(APIView):
"""GET /api/notifications — فهرست اعلان‌های کاربر (جدیدترین اول)"""
permission_classes = [IsAuthenticated]
throttle_scope = 'read'
throttle_classes = [ScopedRateThrottle]
@extend_schema(responses=NotificationListResponseSerializer, tags=['Notifications (notifications)'])
def get(self, request):
qs = Notification.objects.filter(user_id=request.user.id) # Meta.ordering: جدیدترین اول
unread_count = qs.filter(is_read=False).count()
data = {'items': list(qs[:NOTIFICATIONS_MAX]), 'unreadCount': unread_count}
return Response(NotificationListResponseSerializer(data).data)
class NotificationMarkReadView(APIView):
"""PATCH /api/notifications/{id}/read — علامت‌گذاری یک اعلان به‌عنوان خوانده‌شده"""
permission_classes = [IsAuthenticated]
# همان سقفِ read؛ «همه را خوانده کن» برای همین جدا ساخته شد تا ۷۵ اعلان ۷۵ درخواست نشود.
throttle_scope = 'read'
throttle_classes = [ScopedRateThrottle]
# request=None لازم است: بدونِ آن spectacular نمی‌تواند بدنه‌ی PATCH را حدس
# بزند و **کلِ ویو را از اسکیما کنار می‌گذارد** — یعنی این مسیر در قراردادی
# که فرانت از آن کلاینت می‌سازد اصلاً دیده نمی‌شود.
@extend_schema(request=None, responses=MarkReadResponseSerializer,
tags=['Notifications (notifications)'])
def patch(self, request, id):
notification = get_object_or_404(Notification, pk=id, user_id=request.user.id)
if not notification.is_read:
notification.is_read = True
notification.save(update_fields=['is_read'])
return Response(MarkReadResponseSerializer({'success': True}).data)
class NotificationMarkAllReadView(APIView):
"""
PATCH /api/notifications/read-all — همه‌ی اعلان‌های کاربر خوانده‌شده.
دکمه‌ی «همه خوانده شد» پیش از این برای هر اعلان یک درخواستِ جدا می‌زد؛
حسابی با ۷۵ اعلان یعنی ۷۵ درخواستِ هم‌زمان که سهمِ throttleِ read را
(همان که /home و بقیه مصرف می‌کنند) یک‌جا می‌خورد.
"""
permission_classes = [IsAuthenticated]
throttle_scope = 'read'
throttle_classes = [ScopedRateThrottle]
@extend_schema(request=None, responses=MarkAllReadResponseSerializer,
tags=['Notifications (notifications)'])
def patch(self, request):
updated = Notification.objects.filter(
user_id=request.user.id, is_read=False,
).update(is_read=True)
return Response(MarkAllReadResponseSerializer({'success': True, 'updated': updated}).data)