throttle hot read endpoints and cap unbounded list responses

progress/views.py
  - GuestAwareReadThrottle (same guest-by-IP pattern as the other apps);
    throttle_scope='read' on UserSummaryView, StreakCalendarView,
    LeaderboardView.
notifications/views.py
  - GuestAwareReadThrottle + throttle_scope='read' on NotificationListView.
  - list capped at NOTIFICATIONS_MAX=100 newest (Meta.ordering already
    newest-first); unreadCount still counted over the full set.
notifications/tests.py
  - 150 rows -> 100 returned newest-first, unreadCount = 75.
rag/views.py
  - ChunkListView: explicit order_by('-id') before the existing [:200] cap.
ai_tutor/views.py
  - ConversationMessagesView: return the newest 200 messages in
    chronological order instead of the whole conversation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 08:45:43 +03:30
parent 35218bf6a1
commit ff57adbc5f
5 changed files with 78 additions and 4 deletions

View File

@@ -2,23 +2,44 @@ 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 MarkReadResponseSerializer, NotificationListResponseSerializer
# سقفِ تعدادِ اعلانِ برگشتی؛ جلوی رشدِ نامحدودِ پاسخ برای کاربرِ قدیمی رو
# می‌گیره. صفحه‌بندیِ واقعی (cursor) وقتی فرانت infinite-scroll اضافه کنه.
NOTIFICATIONS_MAX = 100
class GuestAwareReadThrottle(ScopedRateThrottle):
"""همون الگوی GuestAware*Throttle: مهمان با IP کلید می‌خوره."""
def get_cache_key(self, request, view):
user = request.user
if user and user.is_authenticated and getattr(user, 'is_guest', False):
ident = f'guest-ip:{self.get_ident(request)}'
elif user and user.is_authenticated:
ident = user.pk
else:
ident = self.get_ident(request)
return self.cache_format % {'scope': self.scope, 'ident': ident}
class NotificationListView(APIView):
"""GET /api/notifications — فهرست اعلان‌های کاربر (جدیدترین اول)"""
permission_classes = [IsAuthenticated]
throttle_scope = 'read'
throttle_classes = [GuestAwareReadThrottle]
@extend_schema(responses=NotificationListResponseSerializer, tags=['Notifications (notifications)'])
def get(self, request):
qs = Notification.objects.filter(user_id=request.user.id)
qs = Notification.objects.filter(user_id=request.user.id) # Meta.ordering: جدیدترین اول
unread_count = qs.filter(is_read=False).count()
data = {'items': list(qs), 'unreadCount': unread_count}
data = {'items': list(qs[:NOTIFICATIONS_MAX]), 'unreadCount': unread_count}
return Response(NotificationListResponseSerializer(data).data)