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:
111
content/admin_browser.py
Normal file
111
content/admin_browser.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
مرورگرِ محتوا در پنلِ ادمین: پایه → فصل → درس → نوعِ مسئله → سوالها.
|
||||
|
||||
چرا: فهرستِ پیشفرضِ جنگو برای پیدا کردنِ «سوالهای فلان مبحث» مناسب نیست —
|
||||
باید بینِ چند صفحهی جدا با فیلترهای دراپداونی بالا و پایین رفت، و وقتی
|
||||
۱۳۰۷ درس و هزاران سوال داری عملاً کار نمیکند.
|
||||
|
||||
اینجا هر ستون فقط وقتی پر میشود که ستونِ قبلی انتخاب شده باشد، پس هیچوقت
|
||||
فهرستِ هزارتایی جلوی چشم نیست. کنارِ هر سوال دو عددِ تصمیمساز میآید:
|
||||
گزارشهای بازِ کاربران، و درصدِ پاسخِ غلط — یعنی همان سیگنالی که میگوید کدام
|
||||
سوال ایراد دارد یا بیش از حد سخت است.
|
||||
|
||||
وابستگیِ تازهای اضافه نمیکند: چند اندپوینتِ کوچکِ 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}',
|
||||
})
|
||||
Reference in New Issue
Block a user