تا الان تنها راهِ گذاشتنِ سوال روی یک سرور، زدنِ دستور در ترمینالِ همان سرور بود. یعنی هر تغییرِ محتوایی به کسی که SSH دارد وابسته میشد، تاریخچه نداشت و برگشتپذیر نبود. محتوا داده است نه کد؛ باید با رابطِ کاربری ویرایش شود. در /admin/content/question/ دکمهی «وارد کردنِ سوالها از فایل» اضافه شد، و اکشنِ «دریافتِ فایلِ سوالهای انتخابشده» خروجی میدهد. خروجی دقیقاً همان قالبی است که ورودی میپذیرد، پس چرخهی «بگیر → ویرایش کن → برگردان» بسته است و برای گرفتنِ نمونه هم لازم نیست کسی مستندات بخواند. قالب عمداً ساده است (lessonId، prompt، choices، correct) تا کسی که برنامهنویس نیست هم بتواند در ویرایشگرِ متن درستش کند. اعتبارسنجی قبل از هر نوشتنی کامل انجام میشود و کلِ فایل در یک تراکنش است: فایلِ نامعتبر هیچ چیزی نمینویسد. وارد کردنِ نیمهکاره بدترین حالت بود، چون کسی که فایل را داده فکر میکند کار شده ولی درس نصفه پر است. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.5 KiB
Python
125 lines
4.5 KiB
Python
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,
|
|
)
|
|
|
|
|
|
class ChoiceInline(admin.TabularInline):
|
|
model = Choice
|
|
extra = 1
|
|
|
|
|
|
@admin.register(Grade)
|
|
class GradeAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'title', 'icon', 'color')
|
|
|
|
|
|
@admin.register(Unit)
|
|
class UnitAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'title', 'grade', 'order')
|
|
list_filter = ('grade',)
|
|
|
|
|
|
@admin.register(Node)
|
|
class NodeAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'title', 'unit', 'order')
|
|
list_filter = ('unit',)
|
|
|
|
|
|
@admin.register(Lesson)
|
|
class LessonAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'title', 'node')
|
|
list_filter = ('node',)
|
|
|
|
|
|
@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)
|
|
class UserLessonCompletionAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'user_id', 'lesson', 'correct_count', 'stars', 'xp_earned', 'coins_earned')
|
|
list_filter = ('lesson',)
|
|
|
|
|
|
@admin.register(UserAnswer)
|
|
class UserAnswerAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'user_id', 'question', 'choice', 'correct', 'xp_earned')
|
|
list_filter = ('correct',)
|
|
|
|
|
|
@admin.register(NodeProgress)
|
|
class NodeProgressAdmin(admin.ModelAdmin):
|
|
list_display = (
|
|
'id', 'user_id', 'node', 'status', 'lessons_completed_count',
|
|
'started_at', 'last_activity_at', 'completed_at',
|
|
)
|
|
list_filter = ('status', 'node__unit')
|
|
search_fields = ('user_id',)
|