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:
@@ -1,17 +1,21 @@
|
||||
import json
|
||||
|
||||
from django.contrib import admin, messages
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import path, reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
|
||||
from . import admin_browser
|
||||
from .curriculum_io import CurriculumError, export_curriculum, import_curriculum
|
||||
from .curriculum_io import parse as parse_curriculum
|
||||
from .question_io import ImportError_, export_questions, import_questions, parse
|
||||
|
||||
from .models import (
|
||||
Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserAnswer, UserLessonCompletion,
|
||||
Choice, Grade, Lesson, Node, NodeProgress, Question, QuestionReport, Unit, UserAnswer,
|
||||
UserLessonCompletion,
|
||||
)
|
||||
|
||||
|
||||
@@ -108,8 +112,8 @@ class QuestionAdmin(admin.ModelAdmin):
|
||||
میپذیرد، پس چرخهی ویرایش بسته است.
|
||||
"""
|
||||
|
||||
list_display = ('id', 'prompt', 'lesson', 'type')
|
||||
list_filter = ('type', 'lesson')
|
||||
list_display = ('id', 'prompt', 'lesson', 'type', 'open_reports', 'wrong_percent')
|
||||
list_filter = ('type', 'lesson__node__unit__grade', 'lesson__node__unit')
|
||||
inlines = [ChoiceInline]
|
||||
actions = ['export_selected']
|
||||
change_list_template = 'admin/content/question/change_list.html'
|
||||
@@ -117,6 +121,33 @@ class QuestionAdmin(admin.ModelAdmin):
|
||||
autocomplete_fields = ['lesson']
|
||||
search_fields = ('prompt',)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).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),
|
||||
)
|
||||
|
||||
@admin.display(description='گزارشِ باز', ordering='_open_reports')
|
||||
def open_reports(self, obj):
|
||||
if not obj._open_reports:
|
||||
return '—'
|
||||
url = (reverse('admin:content_questionreport_changelist')
|
||||
+ f'?question__id__exact={obj.pk}&status__exact=open')
|
||||
return format_html('<a href="{}" style="color:#b91c1c;font-weight:600">{}</a>',
|
||||
url, obj._open_reports)
|
||||
|
||||
@admin.display(description='٪ غلط', ordering='_wrong_answers')
|
||||
def wrong_percent(self, obj):
|
||||
# زیرِ ۵ پاسخ عددی نشان نمیدهیم: یک نمونهی کوچک سوالِ سالم را
|
||||
# «مشکلدار» جلوه میدهد.
|
||||
if obj._total_answers < 5:
|
||||
return '—'
|
||||
percent = round(100 * obj._wrong_answers / obj._total_answers)
|
||||
color = '#b91c1c' if percent >= 70 else '#475569'
|
||||
return format_html('<span style="color:{}">{}٪ از {}</span>',
|
||||
color, percent, obj._total_answers)
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
path(
|
||||
@@ -124,6 +155,16 @@ class QuestionAdmin(admin.ModelAdmin):
|
||||
self.admin_site.admin_view(self.import_view),
|
||||
name='content_question_import',
|
||||
),
|
||||
# مرورگرِ آبشاریِ محتوا؛ زیرِ همین مدل ثبت میشود تا در پنل پیدا باشد.
|
||||
path('browse/', admin_browser.browser_view, name='content_browse'),
|
||||
path('browse/units/<int:grade_id>/', admin_browser.units_json,
|
||||
name='content_browse_units'),
|
||||
path('browse/nodes/<int:unit_id>/', admin_browser.nodes_json,
|
||||
name='content_browse_nodes'),
|
||||
path('browse/lessons/<int:node_id>/', admin_browser.lessons_json,
|
||||
name='content_browse_lessons'),
|
||||
path('browse/questions/<int:lesson_id>/', admin_browser.questions_json,
|
||||
name='content_browse_questions'),
|
||||
] + super().get_urls()
|
||||
|
||||
@admin.action(description='دریافتِ فایلِ سوالهای انتخابشده')
|
||||
@@ -182,3 +223,19 @@ class NodeProgressAdmin(admin.ModelAdmin):
|
||||
)
|
||||
list_filter = ('status', 'node__unit')
|
||||
search_fields = ('user_id',)
|
||||
|
||||
|
||||
@admin.register(QuestionReport)
|
||||
class QuestionReportAdmin(admin.ModelAdmin):
|
||||
"""گزارشِ کاربران روی سوالها — همانجایی که تصمیمِ اصلاحِ محتوا گرفته میشود."""
|
||||
|
||||
list_display = ('id', 'question', 'reason', 'status', 'created_at')
|
||||
list_filter = ('status', 'reason', 'question__lesson__node__unit__grade')
|
||||
search_fields = ('question__prompt', 'note')
|
||||
autocomplete_fields = ['question']
|
||||
actions = ['mark_resolved']
|
||||
|
||||
@admin.action(description='علامت زدن بهعنوانِ بررسیشده')
|
||||
def mark_resolved(self, request, queryset):
|
||||
updated = queryset.update(status='resolved', resolved_at=timezone.now())
|
||||
self.message_user(request, f'{updated} گزارش بررسیشده علامت خورد.', messages.SUCCESS)
|
||||
|
||||
Reference in New Issue
Block a user