Files
MathApp/content/admin_browser.py
Zareian 37d66a4a4c 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>
2026-09-13 21:08:00 +03:30

112 lines
4.4 KiB
Python
Raw 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.
"""
مرورگرِ محتوا در پنلِ ادمین: پایه → فصل → درس → نوعِ مسئله → سوال‌ها.
چرا: فهرستِ پیش‌فرضِ جنگو برای پیدا کردنِ «سوال‌های فلان مبحث» مناسب نیست —
باید بینِ چند صفحه‌ی جدا با فیلترهای دراپ‌داونی بالا و پایین رفت، و وقتی
۱۳۰۷ درس و هزاران سوال داری عملاً کار نمی‌کند.
اینجا هر ستون فقط وقتی پر می‌شود که ستونِ قبلی انتخاب شده باشد، پس هیچ‌وقت
فهرستِ هزارتایی جلوی چشم نیست. کنارِ هر سوال دو عددِ تصمیم‌ساز می‌آید:
گزارش‌های بازِ کاربران، و درصدِ پاسخِ غلط — یعنی همان سیگنالی که می‌گوید کدام
سوال ایراد دارد یا بیش از حد سخت است.
وابستگیِ تازه‌ای اضافه نمی‌کند: چند اندپوینتِ کوچکِ JSON و JavaScript ساده.
"""
from django.contrib.admin.views.decorators import staff_member_required
from django.db.models import Count, Q
from django.http import JsonResponse
from django.shortcuts import render
from django.urls import reverse
from .models import Grade, Lesson, Node, Question, Unit
@staff_member_required
def browser_view(request):
return render(request, 'admin/content/browser.html', {
'title': 'مرورِ محتوا',
'grades': Grade.objects.order_by('title').values('id', 'title'),
})
@staff_member_required
def units_json(request, grade_id: int):
rows = (
Unit.objects.filter(grade_id=grade_id)
.annotate(node_count=Count('nodes', distinct=True))
.order_by('order', 'id')
.values('id', 'title', 'node_count')
)
return JsonResponse({'results': list(rows)})
@staff_member_required
def nodes_json(request, unit_id: int):
rows = (
Node.objects.filter(unit_id=unit_id)
.annotate(lesson_count=Count('lessons', distinct=True))
.order_by('order', 'id')
.values('id', 'title', 'lesson_count')
)
return JsonResponse({'results': list(rows)})
@staff_member_required
def lessons_json(request, node_id: int):
rows = (
Lesson.objects.filter(node_id=node_id)
.annotate(question_count=Count('questions', distinct=True))
.order_by('id')
.values('id', 'title', 'question_count')
)
return JsonResponse({'results': list(rows)})
@staff_member_required
def questions_json(request, lesson_id: int):
"""
هر سوال با دو سیگنال: گزارشِ بازِ کاربران، و درصدِ پاسخِ غلط.
درصدِ غلط فقط وقتی معنا دارد که تعدادِ پاسخ کم نباشد؛ زیرِ ۵ پاسخ عمداً
None برمی‌گردد تا یک نمونه‌ی تصادفی سوالِ سالم را «مشکل‌دار» نشان ندهد.
"""
questions = (
Question.objects.filter(lesson_id=lesson_id)
.annotate(
open_reports=Count('reports', filter=Q(reports__status='open'), distinct=True),
total_answers=Count('answers', distinct=True),
wrong_answers=Count('answers', filter=Q(answers__correct=False), distinct=True),
)
.prefetch_related('choices')
.order_by('id')
)
results = []
for question in questions:
wrong_percent = (
round(100 * question.wrong_answers / question.total_answers)
if question.total_answers >= 5 else None
)
results.append({
'id': question.pk,
'prompt': question.prompt,
'type': question.get_type_display(),
'choices': [
{'value': c.value, 'isCorrect': c.is_correct}
for c in question.choices.all()
],
'openReports': question.open_reports,
'totalAnswers': question.total_answers,
'wrongPercent': wrong_percent,
'editUrl': reverse('admin:content_question_change', args=[question.pk]),
'reportsUrl': (
reverse('admin:content_questionreport_changelist')
+ f'?question__id__exact={question.pk}'
),
})
return JsonResponse({
'results': results,
'addUrl': reverse('admin:content_question_add') + f'?lesson={lesson_id}',
})