put the whole content pipeline behind the admin panel
سوالها از قبل فلهای وارد میشدند ولی دو شکاف مانده بود: افزودنِ تکی عملاً غیرقابلِ استفاده بود، و خودِ برنامهی درسی هیچ راهی جز ترمینالِ سرور نداشت. افزودنِ تکیِ سوال: انتخابِ درس یک دراپداونِ ۱۳۰۷تایی بود. حالا autocomplete است و بر اساسِ عنوانِ درس، فصل و پایه جستجو میشود. برنامهی درسی (Grade → Unit → Node → Lesson) حالا از /admin/content/grade/ وارد و خارج میشود. تا امروز رساندنش به سرور یعنی اجرای rag_import_collection و بعد seed_curriculum_from_textbook روی همان سرور — یعنی وابستگی به Qdrant و به آدمی با دسترسیِ SSH. حالا یکبار لوکال ساخته میشود و فایلش بالا میرود. همسانسازی با عنوان است نه با id: idها بینِ دو دیتابیس یکی نیستند و تکیه بر آنها یعنی وارد کردن روی سرور محتوای بیربط را بازنویسی میکند. وارد کردنِ برنامهی درسی عمداً چیزی پاک نمیکند و idempotent است — یک فایلِ ناقص نباید محتوای سرور را از بین ببرد، و اجرای دوباره نباید چیزی را دو بار بسازد. (وارد کردنِ سوالها اما replace دارد، چون جایگزینیِ سوالهای یک درس کارِ رایجی است.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ from django.shortcuts import redirect, render
|
||||
from django.urls import path, reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
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 (
|
||||
@@ -20,7 +22,58 @@ class ChoiceInline(admin.TabularInline):
|
||||
|
||||
@admin.register(Grade)
|
||||
class GradeAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
برنامهی درسی (پایه → فصل → درس → نوعِ مسئله) از همینجا فلهای وارد و خارج
|
||||
میشود. تا پیش از این تنها راهِ رساندنش به سرور، اجرای چند دستور روی همان
|
||||
سرور بود که به Qdrant و به دسترسیِ SSH وابسته بودند.
|
||||
"""
|
||||
|
||||
list_display = ('id', 'title', 'icon', 'color')
|
||||
search_fields = ('title',)
|
||||
actions = ['export_curriculum_action']
|
||||
change_list_template = 'admin/content/grade/change_list.html'
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
path(
|
||||
'import-curriculum/',
|
||||
self.admin_site.admin_view(self.import_curriculum_view),
|
||||
name='content_grade_import_curriculum',
|
||||
),
|
||||
] + super().get_urls()
|
||||
|
||||
@admin.action(description='دریافتِ فایلِ برنامهی درسیِ پایههای انتخابشده')
|
||||
def export_curriculum_action(self, request, queryset):
|
||||
payload = json.dumps(export_curriculum(queryset), ensure_ascii=False, indent=2)
|
||||
response = HttpResponse(payload, content_type='application/json; charset=utf-8')
|
||||
response['Content-Disposition'] = 'attachment; filename="curriculum.json"'
|
||||
return response
|
||||
|
||||
def import_curriculum_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)
|
||||
try:
|
||||
counts = import_curriculum(parse_curriculum(uploaded.read()))
|
||||
except CurriculumError as exc:
|
||||
self.message_user(request, str(exc), messages.ERROR)
|
||||
return redirect(request.path)
|
||||
|
||||
self.message_user(request, (
|
||||
f"{counts['grades']} پایه، {counts['units']} فصل، {counts['nodes']} درس و "
|
||||
f"{counts['lessons']} نوعِ مسئله اضافه شد"
|
||||
+ (f"؛ {counts['updated']} متنِ درس بهروز شد" if counts['updated'] else '')
|
||||
+ '.'
|
||||
), messages.SUCCESS)
|
||||
return redirect(reverse('admin:content_grade_changelist'))
|
||||
|
||||
return render(request, 'admin/content/grade/import.html', {
|
||||
**self.admin_site.each_context(request),
|
||||
'title': 'وارد کردنِ برنامهی درسی',
|
||||
'opts': self.model._meta,
|
||||
})
|
||||
|
||||
|
||||
@admin.register(Unit)
|
||||
@@ -33,12 +86,16 @@ class UnitAdmin(admin.ModelAdmin):
|
||||
class NodeAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'unit', 'order')
|
||||
list_filter = ('unit',)
|
||||
search_fields = ('title', 'unit__title')
|
||||
|
||||
|
||||
@admin.register(Lesson)
|
||||
class LessonAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'node')
|
||||
list_filter = ('node',)
|
||||
# بدونِ این، انتخابِ درس در فرمِ سوال یک دراپداونِ بیش از هزارتایی است
|
||||
# که عملاً غیرقابلِ استفاده است.
|
||||
search_fields = ('title', 'node__title', 'node__unit__title', 'node__unit__grade__title')
|
||||
|
||||
|
||||
@admin.register(Question)
|
||||
@@ -56,6 +113,9 @@ class QuestionAdmin(admin.ModelAdmin):
|
||||
inlines = [ChoiceInline]
|
||||
actions = ['export_selected']
|
||||
change_list_template = 'admin/content/question/change_list.html'
|
||||
# جستجوی درس هنگامِ افزودنِ تکیِ سوال، بهجای دراپداونِ کامل.
|
||||
autocomplete_fields = ['lesson']
|
||||
search_fields = ('prompt',)
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user