Files
LMS/lmsege/lms/admin.py
T
2026-09-28 22:46:32 +03:00

191 lines
9.4 KiB
Python
Raw 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')
exclude = ('created_by',)
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):
if not obj or not obj.grade:
return format_html('<div style="color: #8b949e;">Оценка не выставлена или имеет неверный формат. Укажите оценку в формате "X/Y" (например, "3/27").</div>')
try:
parts = obj.grade.split('/')
earned = float(parts[0].strip())
total = float(parts[1].strip())
percentage = (earned / total) * 100 if total > 0 else 0
is_passed = percentage >= 40
threshold_text = "Выше порога" if is_passed else "Ниже порога (Мин: 40%)"
color = "#2ea043" if is_passed else "#f85149"
total_int = int(total) if total.is_integer() else total
threshold_points = int(total * 0.4)
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:.1f}%</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}%; 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=percentage,
threshold_text=threshold_text,
color=color,
total_int=total_int,
threshold_points=threshold_points
)
except Exception:
return format_html('<div style="color: #f85149;">Оценка имеет неверный формат. Используйте формат "X/Y" (например, "3/27").</div>')
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)