Files
MathApp/support/views.py
Zareian ae0375c26c make the API docs page answer a consumer's first questions, and fix a missing endpoint
The Swagger header was a list of features. That is what the project does, not
what a caller needs. It now opens with the three things anyone integrating
against this API asks first: how do I get a token, what does an error look like,
and when do I get rate limited -- plus the field-naming and date conventions,
and a note that the schema is the contract and clients should be generated from
it rather than written from memory. Swagger UI and Redoc both render Markdown,
so it is structured rather than one long paragraph, which also fixes how badly
the old mixed Persian/Latin line wrapped right-to-left.

Two defects surfaced while checking the rendered schema:

- /api/health was absent from the schema entirely. The trailing-slash alias was
  excluded with extend_schema(exclude=True)(HealthView), but that decorator
  mutates the class, and both routes pointed at the same class, so the canonical
  route disappeared with it. A subclass now carries the exclusion, and the
  endpoint is back, exactly once. Endpoint count is 56, not 55.
- One endpoint was tagged 'محتوا (content)' while every other content endpoint
  used 'Curriculum (content)', which split the group into two sections.

Tags are now declared with descriptions and ordered by the path a real user
takes, rather than alphabetically.

Comment pass over the codebase, keeping the substance and dropping the noise:
the "⚠️ اضافه شده" prefixes dated comments against a moment no current reader
shares; "فاز ۲ / بخش ۴" referenced a plan that no longer exists and only
repeated the app name on the next line; "فاز اول - ۴ از ۱۱ اپ" had drifted (there
are fourteen). INSTALLED_APPS is now grouped by responsibility. The
TEST_LOGIN_PHONES block in settings shrank from 29 lines to 8 now that the same
explanation lives in docs/operations.md and .env.example -- a comment beside the
code should carry what a code reader needs, not a copy of the runbook.

Checked mechanically that no comment references an identifier that no longer
exists: zero. The dated measurements in rag/services/ingest.py and
generate_questions_from_chunks.py were deliberately left alone -- a measurement
carrying the date it was taken is the useful kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:28:32 +03:30

185 lines
8.6 KiB
Python

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 BugReport, Ticket, TicketMessage
from .serializers import (
BugReportCreateRequestSerializer,
BugReportCreateResponseSerializer,
TicketCreateRequestSerializer,
TicketCreateResponseSerializer,
TicketDetailResponseSerializer,
TicketMessageCreateRequestSerializer,
TicketMessageCreateResponseSerializer,
)
class GuestAwareSupportThrottle(ScopedRateThrottle):
"""
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle/
GuestAwarePurchaseThrottle (discount/views.py، subscription/views.py)
اینجا هم هست. ScopedRateThrottle استاندارد برای درخواستِ
احرازهویت‌شده از request.user.pk به‌عنوانِ کلیدِ throttle استفاده
می‌کنه؛ چون GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ
تازه (pk جدید) می‌سازه، یک مهاجم می‌تونست با ساختنِ پی‌درپیِ حساب‌های
مهمان، برای هر حساب یک bucket تازه از throttle_scope 'support_create'
بگیره. چون هر سه Endpointِ این اپ (BugReportCreateView،
TicketCreateView، TicketMessageCreateView) این scope رو به‌اشتراک
می‌ذارن و هرکدوم واقعاً یک رکورد در دیتابیس می‌سازن (BugReport،
Ticket، یا TicketMessage)، چرخشِ حساب مهمان می‌تونست محدودیتِ
۳/ساعتِ در نظر گرفته‌شده رو چند برابر کنه و باعثِ spam بشه.
این کلاسِ throttle یک‌بار اینجا تعریف شده و بینِ هر سه Endpoint
به‌اشتراک گذاشته می‌شه (به‌جایِ تکرارِ یک کلاسِ مشابه برای هرکدوم)،
دقیقاً چون هر سه از یک throttle_scope مشترک استفاده می‌کنن. فقط
برای کاربرهای مهمان (is_guest=True) کلیدِ cache از pk کاربر به IP
کلاینت (get_ident) تغییر می‌کنه، پس همه‌ی حساب‌های مهمانِ ساخته‌شده
از یک IP یک bucket مشترک دارن و چرخشِ حساب دیگه bucket تازه نمی‌ده.
رفتارِ throttle برای کاربرهای غیرمهمان (ثبت‌نام‌شده) دقیقاً مثلِ قبل
(کلید بر اساسِ pk) باقی می‌مونه؛ منطقِ ساختِ رکورد/ownership/serializer
در هر سه ویو دست‌نخورده‌ست.
"""
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 BugReportCreateView(APIView):
"""POST /api/support/bug-reports — ثبت گزارش باگ"""
permission_classes = [IsAuthenticated]
# بدونِ idempotency/throttle، یک کاربر می‌تونست تعدادِ نامحدودی BugReport
# بسازه؛ GuestAwareSupportThrottle برای مهمان‌ها کلید رو بر اساسِ IP
# می‌سازه تا چرخشِ حساب bucket تازه نده.
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
@extend_schema(
request=BugReportCreateRequestSerializer,
responses=BugReportCreateResponseSerializer,
tags=['Support (support)'],
)
def post(self, request):
serializer = BugReportCreateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
v = serializer.validated_data
bug = BugReport.objects.create(
user_id=request.user.id,
title=v['title'],
description=v['description'],
screenshot_url=v.get('screenshotUrl', ''),
)
data = {'bugReportId': str(bug.pk), 'status': bug.status}
return Response(BugReportCreateResponseSerializer(data).data)
class TicketCreateView(APIView):
"""POST /api/support/tickets — ایجاد تیکت"""
permission_classes = [IsAuthenticated]
# مثلِ BugReportCreateView: هیچ idempotency ای نیست، پس این endpoint هم
# همون scope=support_create رو به‌اشتراک می‌ذاره تا محدودیتِ ساختِ
# رکوردهای پشتیبانی، مستقل از این‌که کدوم نوع رکورده، رعایت بشه.
# ⚠️ اصلاح شده: از همون GuestAwareSupportThrottleِ مشترک استفاده می‌شه
# تا چرخشِ حساب مهمان bucket تازه ندهد.
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
@extend_schema(
request=TicketCreateRequestSerializer,
responses=TicketCreateResponseSerializer,
tags=['Support (support)'],
)
def post(self, request):
serializer = TicketCreateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
v = serializer.validated_data
ticket = Ticket.objects.create(
user_id=request.user.id,
subject=v['subject'],
category=v['category'],
priority=v.get('priority', 'normal'),
)
data = {'ticketId': str(ticket.pk), 'status': ticket.status}
return Response(TicketCreateResponseSerializer(data).data)
class TicketDetailView(APIView):
"""
GET /api/support/tickets/{id} — جزئیات و پیام‌های تیکت
فقط تیکت متعلق به همون کاربر برمی‌گرده (فیلتر با user_id، دقیقاً
همون الگوی محدودسازی به مالک که در progress/views.py برای
UserStats و در ai_tutor/views.py برای Conversation استفاده شده).
"""
permission_classes = [IsAuthenticated]
@extend_schema(responses=TicketDetailResponseSerializer, tags=['Support (support)'])
def get(self, request, ticket_id):
ticket = get_object_or_404(Ticket, pk=ticket_id, user_id=request.user.id)
messages = [
{'id': str(m.pk), 'sender': m.sender, 'text': m.text}
for m in ticket.messages.all()
]
data = {
'ticketId': str(ticket.pk),
'subject': ticket.subject,
'status': ticket.status,
'messages': messages,
}
return Response(TicketDetailResponseSerializer(data).data)
class TicketMessageCreateView(APIView):
"""
POST /api/support/tickets/{id}/messages — ارسال پیام در تیکت
این Endpoint سمت کاربره، پس sender همیشه 'user' ثبت می‌شه. پاسخ
پشتیبانی (sender='support') از پنل ادمین ثبت می‌شه و طبق سند معماری
از همون‌جا رویداد TicketReplied برای Notifications ساطع می‌شه
(پیاده‌سازی در support/admin.py، نه اینجا).
"""
permission_classes = [IsAuthenticated]
# مثلِ دو Endpoint بالا: بدونِ throttle، یک کاربر می‌تونست تعدادِ
# نامحدودی پیام در یک تیکت بسازه.
# ⚠️ اصلاح شده: از همون GuestAwareSupportThrottleِ مشترک استفاده می‌شه
# تا چرخشِ حساب مهمان bucket تازه ندهد.
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
@extend_schema(
request=TicketMessageCreateRequestSerializer,
responses=TicketMessageCreateResponseSerializer,
tags=['Support (support)'],
)
def post(self, request, ticket_id):
ticket = get_object_or_404(Ticket, pk=ticket_id, user_id=request.user.id)
serializer = TicketMessageCreateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
v = serializer.validated_data
message = TicketMessage.objects.create(ticket=ticket, sender='user', text=v['text'])
data = {'messageId': str(message.pk), 'success': True}
return Response(TicketMessageCreateResponseSerializer(data).data)