see the real client IP behind ArvanCloud, and mark all notifications read in one call
Production is ArvanCloud -> nginx -> gunicorn, but NUM_PROXIES was 1, so DRF took ArvanCloud's edge IP as the client: every guest and every OTP request behind one edge shared a single throttle bucket (20 guest sessions an hour for everyone). Default is now 2, and /api/health reports clientIp so the proxy count can be checked from outside. PATCH /api/notifications/read-all replaces the frontend's one-PATCH-per-item loop, which spent the shared "read" budget 75 requests at a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,3 +24,8 @@ class NotificationListResponseSerializer(serializers.Serializer):
|
||||
|
||||
class MarkReadResponseSerializer(serializers.Serializer):
|
||||
success = serializers.BooleanField()
|
||||
|
||||
|
||||
class MarkAllReadResponseSerializer(serializers.Serializer):
|
||||
success = serializers.BooleanField()
|
||||
updated = serializers.IntegerField()
|
||||
|
||||
@@ -146,3 +146,29 @@ class NotificationPayloadShapeTests(TestCase):
|
||||
r = self.client.get('/api/notifications', HTTP_AUTHORIZATION=f'Bearer {self.token}')
|
||||
|
||||
self.assertEqual(r.data['items'][0]['body'], '')
|
||||
|
||||
|
||||
class MarkAllReadTests(TestCase):
|
||||
"""دکمهی «همه خوانده شد» با یک درخواست، نه یکی برای هر اعلان."""
|
||||
|
||||
def setUp(self):
|
||||
from django.core.cache import cache
|
||||
cache.clear()
|
||||
from accounts.authentication import issue_access_token
|
||||
from accounts.models import User
|
||||
self.me = User.objects.create(phone='09120000021')
|
||||
self.other = User.objects.create(phone='09120000022')
|
||||
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.me.id)}'}
|
||||
for user in (self.me, self.me, self.other):
|
||||
Notification.objects.create(user_id=user.id, type='xp_earned', title='x')
|
||||
|
||||
def test_marks_only_my_unread_notifications(self):
|
||||
response = self.client.patch('/api/notifications/read-all', **self.auth)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {'success': True, 'updated': 2})
|
||||
self.assertFalse(Notification.objects.filter(user_id=self.me.id, is_read=False).exists())
|
||||
self.assertTrue(Notification.objects.filter(user_id=self.other.id, is_read=False).exists())
|
||||
|
||||
def test_requires_sign_in(self):
|
||||
self.assertEqual(self.client.patch('/api/notifications/read-all').status_code, 401)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import NotificationListView, NotificationMarkReadView
|
||||
from .views import NotificationListView, NotificationMarkAllReadView, NotificationMarkReadView
|
||||
|
||||
urlpatterns = [
|
||||
path('notifications', NotificationListView.as_view(), name='notifications-list'),
|
||||
path('notifications/read-all', NotificationMarkAllReadView.as_view(), name='notifications-mark-all-read'),
|
||||
path('notifications/<int:id>/read', NotificationMarkReadView.as_view(), name='notifications-mark-read'),
|
||||
]
|
||||
|
||||
@@ -6,7 +6,9 @@ from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import Notification
|
||||
from .serializers import MarkReadResponseSerializer, NotificationListResponseSerializer
|
||||
from .serializers import (
|
||||
MarkAllReadResponseSerializer, MarkReadResponseSerializer, NotificationListResponseSerializer,
|
||||
)
|
||||
|
||||
# سقفِ تعدادِ اعلانِ برگشتی؛ جلوی رشدِ نامحدودِ پاسخ برای کاربرِ قدیمی رو
|
||||
# میگیره. صفحهبندیِ واقعی (cursor) وقتی فرانت infinite-scroll اضافه کنه.
|
||||
@@ -60,3 +62,25 @@ class NotificationMarkReadView(APIView):
|
||||
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 = [GuestAwareReadThrottle]
|
||||
|
||||
@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)
|
||||
|
||||
Reference in New Issue
Block a user