manage questions from the admin panel, not the server terminal

تا الان تنها راهِ گذاشتنِ سوال روی یک سرور، زدنِ دستور در ترمینالِ همان سرور
بود. یعنی هر تغییرِ محتوایی به کسی که SSH دارد وابسته می‌شد، تاریخچه نداشت و
برگشت‌پذیر نبود. محتوا داده است نه کد؛ باید با رابطِ کاربری ویرایش شود.

در /admin/content/question/ دکمه‌ی «وارد کردنِ سوال‌ها از فایل» اضافه شد، و
اکشنِ «دریافتِ فایلِ سوال‌های انتخاب‌شده» خروجی می‌دهد. خروجی دقیقاً همان قالبی
است که ورودی می‌پذیرد، پس چرخه‌ی «بگیر → ویرایش کن → برگردان» بسته است و برای
گرفتنِ نمونه هم لازم نیست کسی مستندات بخواند.

قالب عمداً ساده است (lessonId، prompt، choices، correct) تا کسی که برنامه‌نویس
نیست هم بتواند در ویرایشگرِ متن درستش کند.

اعتبارسنجی قبل از هر نوشتنی کامل انجام می‌شود و کلِ فایل در یک تراکنش است:
فایلِ نامعتبر هیچ چیزی نمی‌نویسد. وارد کردنِ نیمه‌کاره بدترین حالت بود، چون
کسی که فایل را داده فکر می‌کند کار شده ولی درس نصفه پر است.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 20:29:24 +03:30
parent cbe9b63dc4
commit 190bff3d0f
5 changed files with 344 additions and 1 deletions

View File

@@ -1,4 +1,12 @@
from django.contrib import admin
import json
from django.contrib import admin, messages
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.urls import path, reverse
from django.utils.html import format_html
from .question_io import ImportError_, export_questions, import_questions, parse
from .models import (
Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserAnswer, UserLessonCompletion,
@@ -35,9 +43,63 @@ class LessonAdmin(admin.ModelAdmin):
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
"""
سوال‌ها علاوه بر ویرایشِ تکی، از همین‌جا فله‌ای وارد و خارج می‌شوند.
چرا اینجا و نه در ترمینال: محتوا باید بدونِ دسترسیِ SSH و بدونِ برنامه‌نویس
قابلِ تغییر باشد. خروجیِ «دریافتِ فایل» دقیقاً همان قالبی‌ست که «وارد کردن»
می‌پذیرد، پس چرخه‌ی ویرایش بسته است.
"""
list_display = ('id', 'prompt', 'lesson', 'type')
list_filter = ('type', 'lesson')
inlines = [ChoiceInline]
actions = ['export_selected']
change_list_template = 'admin/content/question/change_list.html'
def get_urls(self):
return [
path(
'import/',
self.admin_site.admin_view(self.import_view),
name='content_question_import',
),
] + super().get_urls()
@admin.action(description='دریافتِ فایلِ سوال‌های انتخاب‌شده')
def export_selected(self, request, queryset):
payload = json.dumps(export_questions(queryset), ensure_ascii=False, indent=2)
response = HttpResponse(payload, content_type='application/json; charset=utf-8')
response['Content-Disposition'] = 'attachment; filename="questions.json"'
return response
def import_view(self, request):
if request.method == 'POST':
uploaded = request.FILES.get('file')
if uploaded is None:
self.message_user(request, 'فایلی انتخاب نشده بود.', messages.ERROR)
return redirect(request.path)
replace = request.POST.get('replace') == 'on'
try:
rows = parse(uploaded.read())
result = import_questions(rows, replace=replace)
except ImportError_ as exc:
# خطای قابلِ فهم برای کاربر — هیچ چیزی نوشته نشده.
self.message_user(request, str(exc), messages.ERROR)
return redirect(request.path)
note = f"{result['created']} سوال در {result['lessons']} درس اضافه شد"
if result['removed']:
note += f" و {result['removed']} ردیفِ قبلی پاک شد"
self.message_user(request, note + '.', messages.SUCCESS)
return redirect(reverse('admin:content_question_changelist'))
return render(request, 'admin/content/question/import.html', {
**self.admin_site.each_context(request),
'title': 'وارد کردنِ سوال‌ها',
'opts': self.model._meta,
})
@admin.register(UserLessonCompletion)