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 .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, ) class ChoiceInline(admin.TabularInline): model = Choice extra = 1 @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) 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',) 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) 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' # جستجوی درس هنگامِ افزودنِ تکیِ سوال، به‌جای دراپ‌داونِ کامل. autocomplete_fields = ['lesson'] search_fields = ('prompt',) 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',)