give content editors a drill-down browser and per-question signals

پیدا کردنِ «سوال‌های فلان مبحث» در فهرستِ پیش‌فرضِ جنگو عملاً ممکن نبود: باید
بینِ چند صفحه‌ی جدا با دراپ‌داون‌های هزارتایی بالا و پایین می‌رفتی.

/admin/content/question/browse/ چهار ستونِ آبشاری دارد — پایه → فصل → درس →
مبحث — و هر ستون فقط وقتی پر می‌شود که قبلی انتخاب شده باشد، پس هیچ‌وقت
فهرستِ هزارتایی جلوی چشم نیست. ته‌اش سوال‌های همان مبحث با گزینه‌هایشان
می‌آیند و از همان‌جا ویرایش یا اضافه می‌شوند.

کنارِ هر سوال دو عددِ تصمیم‌ساز: گزارش‌های بازِ کاربران، و درصدِ پاسخِ غلط.
درصد زیرِ ۵ پاسخ نشان داده نمی‌شود — یک نمونه‌ی کوچک سوالِ سالم را
«مشکل‌دار» جلوه می‌دهد. همین دو ستون به فهرستِ سوال‌ها هم اضافه شدند.

content.QuestionReport و POST /api/questions/{id}/report تازه‌اند: تنها
بازخوردی که تا امروز جمع می‌شد مالِ چتِ دستیار بود، و روی سوال‌های مسیرِ
آموزشی هیچ راهی نبود که کاربر بگوید گزینه‌ی درست غلط است. گزارشِ بازِ تکراریِ
همان کاربر ردیفِ تازه نمی‌سازد تا دابل‌کلیک آمار را به‌هم نزند.

وابستگیِ تازه‌ای اضافه نشد: چند اندپوینتِ کوچکِ JSON و JavaScript ساده.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 21:08:00 +03:30
parent 67361f6711
commit 37d66a4a4c
10 changed files with 576 additions and 7 deletions

View File

@@ -4,7 +4,8 @@ from django.http import Http404
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework import status
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle
@@ -13,7 +14,10 @@ from rest_framework.views import APIView
from progress.models import UserHearts, UserStats
from subscription.services import has_unlimited_hearts
from .models import Choice, Grade, Lesson, NodeProgress, Question, UserAnswer, UserLessonCompletion
from .models import (
Choice, Grade, Lesson, NodeProgress, Question, QuestionReport, UserAnswer,
UserLessonCompletion,
)
from .serializers import (
AnswerRequestSerializer,
AnswerResponseSerializer,
@@ -24,6 +28,8 @@ from .serializers import (
LessonIntroResponseSerializer,
LessonQuestionsResponseSerializer,
NodeProgressListResponseSerializer,
QuestionReportRequestSerializer,
QuestionReportResponseSerializer,
)
@@ -535,3 +541,51 @@ class NodeProgressListView(APIView):
data = {'progress': list(qs.order_by('node__unit_id', 'node__order'))}
return Response(NodeProgressListResponseSerializer(data).data)
class QuestionReportView(APIView):
"""
POST /api/questions/{id}/report — «این سوال ایراد دارد»
چرا لازم بود: تنها بازخوردی که تا امروز جمع می‌شد مالِ چتِ دستیار بود.
روی خودِ سوال‌های مسیرِ آموزشی هیچ راهی نبود که کاربر بگوید گزینه‌ی درست
غلط است یا صورتِ سوال گنگ — و کسی که محتوا را نگه می‌دارد هیچ سیگنالی
نداشت جز حدس.
"""
permission_classes = [IsAuthenticated]
throttle_scope = 'question_report'
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=QuestionReportRequestSerializer,
responses=QuestionReportResponseSerializer,
tags=['محتوا (content)'],
)
def post(self, request, question_id: int):
question = Question.objects.filter(pk=question_id).first()
if question is None:
raise NotFound('سوال یافت نشد.')
serializer = QuestionReportRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# گزارشِ بازِ تکراریِ همان کاربر روی همان سوال، ردیفِ تازه نمی‌سازد:
# دابل‌کلیک نباید صف را شلوغ کند و آمار را به‌هم بزند.
report, created = QuestionReport.objects.get_or_create(
question=question,
user_id=request.user.id,
status='open',
defaults={
'reason': serializer.validated_data['reason'],
'note': serializer.validated_data.get('note', ''),
},
)
return Response(
QuestionReportResponseSerializer({
'success': True,
'reportId': report.pk,
'alreadyReported': not created,
}).data,
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
)