Files

221 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.contrib import admin
from unfold.admin import ModelAdmin, StackedInline
from django.db import models
from django.forms import widgets
from .models import Subject, Assignment, Submission, Question, Answer
@admin.register(Subject)
class SubjectAdmin(ModelAdmin):
list_display = ('name',)
list_display_links = ('name',)
class QuestionInline(StackedInline):
model = Question
extra = 0
class Media:
js = ('js/admin_assignment.js',)
@admin.register(Assignment)
class AssignmentAdmin(ModelAdmin):
list_display = ('title', 'subject', 'is_published', 'time_limit', 'deadline', 'allow_late_submissions', 'created_at')
list_display_links = ('title',)
list_filter = ('subject', 'is_published', 'allow_late_submissions')
search_fields = ('title', 'description')
fieldsets = (
('Основная информация', {
'fields': ('title', 'subject', 'short_description', 'description')
}),
('Настройки доступа и времени', {
'fields': ('is_published', 'assign_to_all', 'assigned_students', 'deadline', 'time_limit')
}),
('Настройки проверки', {
'fields': ('allow_late_submissions', 'passing_threshold')
}),
)
filter_horizontal = ('assigned_students',)
inlines = [QuestionInline]
actions = ['make_published', 'make_unpublished', 'delete_selected_assignments']
@admin.action(description="Опубликовать выбранные задания")
def make_published(self, request, queryset):
updated = queryset.update(is_published=True)
self.message_user(request, f"Опубликовано {updated} заданий.")
@admin.action(description="Скрыть выбранные задания (в черновик)")
def make_unpublished(self, request, queryset):
updated = queryset.update(is_published=False)
self.message_user(request, f"Скрыто {updated} заданий.")
@admin.action(description="Удалить выбранные задания")
def delete_selected_assignments(self, request, queryset):
deleted_count = 0
for obj in queryset:
if self.has_delete_permission(request, obj):
obj.delete()
deleted_count += 1
self.message_user(request, f"Успешно удалено {deleted_count} заданий.")
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser or request.user.role == 'ADMIN':
return qs
return qs.filter(created_by=request.user)
def save_model(self, request, obj, form, change):
if getattr(obj, 'created_by_id', None) is None:
obj.created_by = request.user
super().save_model(request, obj, form, change)
def has_delete_permission(self, request, obj=None):
if request.user.is_superuser or request.user.role == 'ADMIN':
return True
if request.user.role == 'TEACHER':
if obj is not None and obj.created_by != request.user:
return False
return True
return False
from django.utils.html import format_html
class AnswerInline(StackedInline):
model = Answer
extra = 0
readonly_fields = ('question', 'get_student_answer')
fields = ('question', 'get_student_answer', 'is_correct', 'points', 'comment')
def get_student_answer(self, obj):
if not obj or not obj.pk:
return ""
if obj.question.question_type == 'TEXT_AUTO':
return obj.text_answer or "Нет ответа"
elif obj.question.question_type == 'TABLE_AUTO':
import json
try:
data = json.loads(obj.text_answer)
html = '<table style="border-collapse: collapse; margin-top: 4px;">'
for row in data:
html += f'<tr><td style="border: 1px solid #e5e7eb; padding: 4px 8px;">{row[0]}</td><td style="border: 1px solid #e5e7eb; padding: 4px 8px;">{row[1]}</td></tr>'
html += '</table>'
return format_html(html)
except Exception:
return obj.text_answer or "Нет ответа"
else:
if obj.file_answer:
return format_html('<a href="{}" target="_blank">Скачать файл</a>', obj.file_answer.url)
return "Файл не прикреплен"
get_student_answer.short_description = 'Ответ ученика'
def has_add_permission(self, request, obj=None):
return False
from django.utils.html import format_html
@admin.register(Submission)
class SubmissionAdmin(ModelAdmin):
change_list_template = "admin/lms/submission/change_list.html"
list_display = ('assignment', 'student', 'status', 'is_time_expired', 'is_late', 'grade', 'submitted_at')
list_filter = ('status', 'assignment__subject', 'student', 'is_time_expired', 'is_late')
search_fields = ('student__username', 'assignment__title')
inlines = [AnswerInline]
fieldsets = (
('Параметры проверки и оценка', {
'fields': ('statistics', ('assignment', 'student'), ('grade', 'status'), ('is_time_expired', 'is_late'), 'teacher_feedback')
}),
('Временные метки', {
'fields': (('started_at', 'submitted_at'),)
}),
)
def get_readonly_fields(self, request, obj=None):
if obj:
return ('statistics', 'assignment', 'student', 'is_time_expired', 'is_late', 'started_at', 'submitted_at')
return ('statistics', 'is_time_expired', 'is_late', 'started_at', 'submitted_at')
@admin.display(description='Статистика (сохраните оценку в формате X/Y, чтобы обновить)')
def statistics(self, obj):
stats = obj.get_statistics() if obj else None
if not stats:
return format_html('<div style="color: #8b949e;">Оценка не выставлена или имеет неверный формат. Укажите оценку в формате "X/Y" (например, "3/27").</div>')
try:
return format_html(
'''
<div style="background-color: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; color: #c9d1d9; font-family: ui-sans-serif, system-ui, sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<div style="font-size: 11px; text-transform: uppercase; color: #8b949e; letter-spacing: 0.05em; margin-bottom: 8px;">Итоговая оценка</div>
<div style="display: flex; align-items: baseline; gap: 12px;">
<div style="background-color: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 6px 16px; font-size: 24px; font-weight: bold; color: #fff;">
{grade}
</div>
<span style="font-size: 13px; color: #8b949e;">первичных баллов</span>
</div>
</div>
<div style="flex-grow: 1; max-width: 50%; display: flex; flex-direction: column; justify-content: flex-end;">
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 8px;">
<span style="color: #c9d1d9;">Выполнение: <b>{percentage_str}%</b></span>
<span style="color: {color}; font-weight: 500;">{threshold_text}</span>
</div>
<div style="width: 100%; background-color: #30363d; border-radius: 9999px; height: 6px; overflow: hidden; margin-bottom: 8px;">
<div style="background-color: {color}; width: {percentage_str}%; height: 100%; border-radius: 9999px;"></div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 11px; color: #8b949e; font-family: ui-monospace, SFMono-Regular, monospace;">
<span>0 б</span>
<span>Порог: {threshold_points} б</span>
<span>Максимум: {total_int} б</span>
</div>
</div>
</div>
</div>
''',
grade=obj.grade,
percentage_str=stats['percentage_str'],
threshold_text=stats['threshold_text'],
color=stats['color'],
total_int=stats['total'],
threshold_points=stats['threshold_points']
)
except Exception as e:
return format_html(
'<div style="color: #f85149;">Ошибка при отрисовке статистики: {error}</div>',
error=str(e)
)
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
if isinstance(instance, Answer):
if instance.is_correct and instance.points is None:
instance.points = instance.question.points
elif instance.is_correct is False and instance.points is None:
instance.points = 0
instance.save()
for obj in formset.deleted_objects:
obj.delete()
formset.save_m2m()
if formset.model == Answer:
submission = formset.instance
total_earned = sum((ans.points or 0) for ans in submission.answers.all())
total_max = sum((q.points or 1) for q in submission.assignment.questions.all())
new_grade = f"{total_earned}/{total_max}"
if submission.grade != new_grade:
submission.grade = new_grade
submission.save(update_fields=['grade'])
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser or request.user.role == 'ADMIN':
return qs
if request.user.role == 'TEACHER':
return qs.filter(assignment__created_by=request.user)
return qs.filter(student=request.user)