Compare commits
13 Commits
ece2e51b04
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 53b9a76848 | |||
| d7e1364426 | |||
| 85d138280d | |||
| 646bc81724 | |||
| 7009f90a6c | |||
| 1a8da1351f | |||
| 93f5709683 | |||
| 08eba4f9d7 | |||
| c7d443e2ee | |||
| 96d4ed2425 | |||
| 431056e69e | |||
| 01412bc1b7 | |||
| 043a73fbe1 |
+105
-1
@@ -22,7 +22,19 @@ class AssignmentAdmin(ModelAdmin):
|
||||
list_display_links = ('title',)
|
||||
list_filter = ('subject', 'is_published', 'allow_late_submissions')
|
||||
search_fields = ('title', 'description')
|
||||
exclude = ('created_by',)
|
||||
|
||||
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']
|
||||
@@ -99,6 +111,8 @@ class AnswerInline(StackedInline):
|
||||
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"
|
||||
@@ -107,6 +121,96 @@ class SubmissionAdmin(ModelAdmin):
|
||||
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':
|
||||
|
||||
@@ -15,6 +15,7 @@ class Subject(models.Model):
|
||||
|
||||
class Assignment(models.Model):
|
||||
title = models.CharField('Название задания', max_length=255)
|
||||
short_description = models.TextField('Краткое описание (для карточки)', blank=True, null=True, help_text='Отображается на главной странице в карточке задания. Если пусто, используется начало полного описания.')
|
||||
description = RichTextUploadingField('Описание задания', blank=True, null=True)
|
||||
subject = models.ForeignKey(Subject, on_delete=models.CASCADE, verbose_name='Предмет')
|
||||
is_published = models.BooleanField('Опубликовано', default=True, help_text='Снимите галочку, чтобы скрыть задание (как черновик).')
|
||||
@@ -25,6 +26,7 @@ class Assignment(models.Model):
|
||||
deadline = models.DateTimeField('Дедлайн', blank=True, null=True, help_text='После этого времени ответы не принимаются.')
|
||||
time_limit = models.PositiveIntegerField('Лимит времени (минуты)', blank=True, null=True, help_text='Например, 235 для 3ч 55м. Оставьте пустым, если без таймера.')
|
||||
allow_late_submissions = models.BooleanField('Разрешить дорешивание', default=True, help_text='Если включено, ученики смогут сдавать задание после дедлайна с пометкой "После дедлайна".')
|
||||
passing_threshold = models.PositiveIntegerField('Порог прохождения (в баллах)', default=10, help_text='Количество баллов, необходимое для прохождения. Например, 10.')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Задание'
|
||||
@@ -45,6 +47,7 @@ class Question(models.Model):
|
||||
question_type = models.CharField('Тип ответа', max_length=20, choices=TYPE_CHOICES, default='TEXT_AUTO')
|
||||
table_rows = models.PositiveIntegerField('Количество строк в таблице', default=2, blank=True, null=True, help_text='Укажите количество строк. Столбцов всегда 2.')
|
||||
correct_answer = models.CharField('Правильный ответ (для автопроверки)', max_length=255, blank=True, null=True, help_text='Оставьте пустым для ручной проверки. Для таблицы вводите ответы в формате "Ответ1|Ответ2;Ответ3|Ответ4" (строки через точку с запятой, столбцы через прямую черту).')
|
||||
points = models.PositiveIntegerField('Баллы за правильный ответ', default=1, help_text='Количество баллов, которое дается за эту задачу (для автопроверки).')
|
||||
order = models.PositiveIntegerField('Порядок', default=0)
|
||||
|
||||
class Meta:
|
||||
@@ -79,6 +82,34 @@ class Submission(models.Model):
|
||||
def __str__(self):
|
||||
return f"Ответ от {self.student.username} на {self.assignment.title}"
|
||||
|
||||
def get_statistics(self):
|
||||
if not self.grade:
|
||||
return None
|
||||
try:
|
||||
parts = self.grade.split('/')
|
||||
earned = float(parts[0].strip())
|
||||
total = float(parts[1].strip())
|
||||
percentage = (earned / total) * 100 if total > 0 else 0
|
||||
|
||||
threshold_points = self.assignment.passing_threshold
|
||||
is_passed = earned >= threshold_points
|
||||
|
||||
total_int = int(total) if total.is_integer() else total
|
||||
earned_int = int(earned) if earned.is_integer() else earned
|
||||
|
||||
return {
|
||||
'earned': earned_int,
|
||||
'total': total_int,
|
||||
'percentage': percentage,
|
||||
'percentage_str': f"{percentage:.1f}",
|
||||
'is_passed': is_passed,
|
||||
'threshold_points': threshold_points,
|
||||
'threshold_text': 'Выше порога' if is_passed else f'Ниже порога (Мин: {threshold_points} б)',
|
||||
'color': '#2ea043' if is_passed else '#f85149'
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
class Answer(models.Model):
|
||||
submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name='answers', verbose_name='Решение (Submission)')
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers', verbose_name='Задача')
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400 pl-2">
|
||||
<p class="flex items-center gap-1 mb-1"><span class="material-symbols-outlined text-[16px]">person</span> {{ result.student.first_name|default:result.student.username }}</p>
|
||||
<p class="flex items-center gap-1"><span class="material-symbols-outlined text-[16px]">schedule</span> {{ result.submitted_at|date:"d M Y, H:i" }}</p>
|
||||
{% if result.is_late %}
|
||||
<p class="flex items-center gap-1 mt-1 text-xs font-medium text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/20 py-0.5 px-2 rounded-md w-fit border border-red-100 dark:border-red-800/30"><span class="material-symbols-outlined text-[14px]">error</span> Сдано после дедлайна</p>
|
||||
{% endif %}
|
||||
{% if result.is_time_expired %}
|
||||
<p class="flex items-center gap-1 mt-1 text-xs font-medium text-orange-500 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20 py-0.5 px-2 rounded-md w-fit border border-orange-100 dark:border-orange-800/30"><span class="material-symbols-outlined text-[14px]">timer_off</span> Время вышло</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-4 pl-2">
|
||||
<a href="{% url 'admin:lms_submission_change' result.pk %}" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-primary-600 hover:bg-primary-700 transition">
|
||||
@@ -66,6 +72,12 @@
|
||||
<p class="flex items-center gap-1 mb-1"><span class="material-symbols-outlined text-[16px]">person</span> {{ result.student.first_name|default:result.student.username }}</p>
|
||||
<p class="flex items-center gap-1 font-medium text-green-600 dark:text-green-400"><span class="material-symbols-outlined text-[16px]">done_all</span> Оценка: {{ result.grade|default:"-" }}</p>
|
||||
<p class="flex items-center gap-1 mt-1 text-xs"><span class="material-symbols-outlined text-[14px]">event</span> {{ result.submitted_at|date:"d M Y, H:i" }}</p>
|
||||
{% if result.is_late %}
|
||||
<p class="flex items-center gap-1 mt-1 text-xs font-medium text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/20 py-0.5 px-2 rounded-md w-fit border border-red-100 dark:border-red-800/30"><span class="material-symbols-outlined text-[14px]">error</span> Сдано после дедлайна</p>
|
||||
{% endif %}
|
||||
{% if result.is_time_expired %}
|
||||
<p class="flex items-center gap-1 mt-1 text-xs font-medium text-orange-500 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20 py-0.5 px-2 rounded-md w-fit border border-orange-100 dark:border-orange-800/30"><span class="material-symbols-outlined text-[14px]">timer_off</span> Время вышло</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-4 pl-2">
|
||||
<a href="{% url 'admin:lms_submission_change' result.pk %}" class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 text-xs font-medium rounded text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
|
||||
@@ -37,28 +37,74 @@
|
||||
<h3 class="text-lg font-bold text-white mb-6 font-heading border-b border-obsidian-border pb-3">Ваш ответ</h3>
|
||||
|
||||
{% if submission and submission.status != 'IN_PROGRESS' %}
|
||||
<div class="mb-8 p-5 rounded-lg border {% if submission.status == 'GRADED' %}bg-emerald-950/20 text-emerald-300 border-emerald-800/40{% else %}bg-amber-950/20 text-amber-300 border-amber-800/40{% endif %}">
|
||||
{% if submission.status == 'GRADED' %}
|
||||
<h4 class="font-bold mb-2 flex items-center gap-2 font-heading">
|
||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
||||
</svg>
|
||||
Оценка: {{ submission.grade|default:"Не выставлена" }}
|
||||
</h4>
|
||||
{% else %}
|
||||
<h4 class="font-bold mb-2 flex items-center gap-2 font-heading">
|
||||
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
|
||||
Ваша работа отправлена и ожидает проверки.
|
||||
</h4>
|
||||
{% endif %}
|
||||
{% with stats=submission.get_statistics %}
|
||||
{% if stats %}
|
||||
<div class="mb-8 p-6 rounded-xl border bg-obsidian-base border-obsidian-borderLight shadow-sm">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="font-mono text-[10px] text-txt-faint uppercase tracking-wider mb-1">Итоговая оценка</div>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<div class="bg-obsidian-card border border-obsidian-border rounded-lg px-4 py-2 text-2xl font-bold text-white shadow-sm">
|
||||
{{ submission.grade }}
|
||||
</div>
|
||||
<span class="text-xs text-txt-muted">первичных баллов</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if submission.teacher_feedback %}
|
||||
<div class="mt-4 p-4 rounded bg-obsidian-base border border-obsidian-borderLight">
|
||||
<p class="font-medium text-white text-sm mb-1">Общий комментарий преподавателя:</p>
|
||||
<p class="text-sm text-txt-secondary">{{ submission.teacher_feedback }}</p>
|
||||
<div class="flex-grow max-w-md w-full">
|
||||
<div class="flex justify-between items-end mb-2">
|
||||
<span class="text-sm text-txt-secondary">Выполнение: <b class="text-white">{{ stats.percentage_str }}%</b></span>
|
||||
<span class="text-xs font-medium px-2 py-0.5 rounded-full" style="color: {{ stats.color }}; background-color: {{ stats.color }}15; border: 1px solid {{ stats.color }}30;">
|
||||
{{ stats.threshold_text }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-obsidian-border rounded-full height-1.5 overflow-hidden mb-2" style="height: 6px;">
|
||||
<div class="h-full rounded-full transition-all duration-1000" style="background-color: {{ stats.color }}; width: {{ stats.percentage_str }}%;"></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-[10px] text-txt-muted font-mono uppercase tracking-wide">
|
||||
<span>0 б</span>
|
||||
<span>Порог: {{ stats.threshold_points }} б</span>
|
||||
<span>Максимум: {{ stats.total }} б</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if submission.teacher_feedback %}
|
||||
<div class="mt-6 pt-5 border-t border-obsidian-borderLight">
|
||||
<p class="font-mono text-[10px] uppercase text-accent-blue mb-2 tracking-wider">Комментарий преподавателя</p>
|
||||
<p class="text-sm text-txt-secondary leading-relaxed bg-obsidian-card p-4 rounded-lg border border-obsidian-border shadow-inner">
|
||||
{{ submission.teacher_feedback }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-8 p-5 rounded-lg border {% if submission.status == 'GRADED' %}bg-emerald-950/20 text-emerald-300 border-emerald-800/40{% else %}bg-amber-950/20 text-amber-300 border-amber-800/40{% endif %}">
|
||||
{% if submission.status == 'GRADED' %}
|
||||
<h4 class="font-bold mb-2 flex items-center gap-2 font-heading">
|
||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
||||
</svg>
|
||||
Оценка: {{ submission.grade|default:"Не выставлена" }}
|
||||
</h4>
|
||||
{% else %}
|
||||
<h4 class="font-bold mb-2 flex items-center gap-2 font-heading">
|
||||
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
|
||||
Ваша работа отправлена и ожидает проверки.
|
||||
</h4>
|
||||
{% endif %}
|
||||
|
||||
{% if submission.teacher_feedback %}
|
||||
<div class="mt-4 p-4 rounded bg-obsidian-base border border-obsidian-borderLight">
|
||||
<p class="font-medium text-white text-sm mb-1">Общий комментарий преподавателя:</p>
|
||||
<p class="text-sm text-txt-secondary">{{ submission.teacher_feedback }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<h4 class="font-bold mb-5 text-white font-heading">Ваши ответы на задачи:</h4>
|
||||
<div class="space-y-6">
|
||||
@@ -67,7 +113,10 @@
|
||||
<!-- Status indicator line -->
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1 {% if answer.is_correct %}bg-emerald-500{% elif answer.is_correct == False %}bg-rose-500{% else %}bg-txt-faint{% endif %}"></div>
|
||||
|
||||
<h5 class="font-medium text-sm text-txt-muted mb-3 pl-2">Задача {{ forloop.counter }}: {{ answer.question.text|striptags|truncatewords:10 }}</h5>
|
||||
<h5 class="font-bold text-sm text-txt-secondary mb-2 pl-2">Задача {{ forloop.counter }}</h5>
|
||||
<div class="prose prose-invert max-w-none text-white text-sm mb-4 pl-2 font-body">
|
||||
{{ answer.question.text|safe }}
|
||||
</div>
|
||||
|
||||
<div class="pl-2">
|
||||
{% if answer.question.question_type == 'TEXT_AUTO' %}
|
||||
|
||||
@@ -10,10 +10,26 @@
|
||||
</div>
|
||||
|
||||
{% if assignments %}
|
||||
<!-- Tabs -->
|
||||
<div class="flex items-center gap-6 border-b border-obsidian-borderLight mb-6 overflow-x-auto no-scrollbar">
|
||||
<button onclick="filterTasks('all')" id="tab-all" class="text-sm font-medium pb-3 border-b-2 transition-colors border-accent-blue text-white whitespace-nowrap">
|
||||
Все задания <span class="ml-1 px-1.5 py-0.5 rounded bg-obsidian-base border border-obsidian-border text-xs text-txt-secondary">{{ total_count }}</span>
|
||||
</button>
|
||||
<button onclick="filterTasks('waiting')" id="tab-waiting" class="text-sm font-medium pb-3 border-b-2 border-transparent text-txt-muted hover:text-white transition-colors whitespace-nowrap">
|
||||
Ожидают решения <span class="ml-1 px-1.5 py-0.5 rounded bg-obsidian-base border border-obsidian-border text-xs text-txt-secondary">{{ waiting_solution_count }}</span>
|
||||
</button>
|
||||
<button onclick="filterTasks('pending')" id="tab-pending" class="text-sm font-medium pb-3 border-b-2 border-transparent text-txt-muted hover:text-white transition-colors whitespace-nowrap">
|
||||
Ожидает проверки <span class="ml-1 px-1.5 py-0.5 rounded bg-obsidian-base border border-amber-900/50 text-xs text-amber-500/80">{{ pending_count }}</span>
|
||||
</button>
|
||||
<button onclick="filterTasks('graded')" id="tab-graded" class="text-sm font-medium pb-3 border-b-2 border-transparent text-txt-muted hover:text-white transition-colors whitespace-nowrap">
|
||||
Проверено <span class="ml-1 px-1.5 py-0.5 rounded bg-obsidian-base border border-emerald-900/50 text-xs text-emerald-500/80">{{ graded_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5" data-purpose="task-cards-grid">
|
||||
{% for assignment in assignments %}
|
||||
{% get_submission submissions assignment.id as sub %}
|
||||
<article class="bg-obsidian-card rounded border border-obsidian-border hover:border-obsidian-borderLight p-6 flex flex-col justify-between transition-colors duration-150 relative" data-purpose="task-card">
|
||||
<article class="task-card bg-obsidian-card rounded border border-obsidian-border hover:border-obsidian-borderLight p-6 flex flex-col justify-between transition-colors duration-150 relative" data-status="{% if not sub or sub.status == 'IN_PROGRESS' %}waiting{% elif sub.status == 'PENDING' %}pending{% elif sub.status == 'GRADED' %}graded{% endif %}">
|
||||
<div>
|
||||
<!-- Header Badges -->
|
||||
<div class="flex items-center justify-between gap-2 mb-4">
|
||||
@@ -48,8 +64,8 @@
|
||||
{% else %}
|
||||
{% if assignment.deadline and now > assignment.deadline %}
|
||||
{% if assignment.allow_late_submissions %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-mono font-medium bg-obsidian-borderLight/20 text-txt-muted border border-obsidian-borderLight/50">
|
||||
Дедлайн прошел
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-mono font-medium bg-accent-blue/10 text-accent-blue border border-accent-blue/30">
|
||||
Дорешивание
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-mono font-medium bg-rose-950/40 text-rose-400 border border-rose-800/50">
|
||||
@@ -71,7 +87,11 @@
|
||||
{{ assignment.title }}
|
||||
</h2>
|
||||
|
||||
{% if assignment.description %}
|
||||
{% if assignment.short_description %}
|
||||
<div class="mb-5 text-sm text-txt-secondary font-body whitespace-pre-line">
|
||||
{{ assignment.short_description }}
|
||||
</div>
|
||||
{% elif assignment.description %}
|
||||
<div class="mb-5 line-clamp-3 text-sm text-txt-secondary font-body">
|
||||
{{ assignment.description|striptags }}
|
||||
</div>
|
||||
@@ -142,4 +162,28 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function filterTasks(status) {
|
||||
const cards = document.querySelectorAll('.task-card');
|
||||
cards.forEach(card => {
|
||||
if (status === 'all' || card.dataset.status === status) {
|
||||
card.style.display = 'flex';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = ['all', 'waiting', 'pending', 'graded'];
|
||||
tabs.forEach(t => {
|
||||
const btn = document.getElementById('tab-' + t);
|
||||
if (t === status) {
|
||||
btn.classList.add('border-accent-blue', 'text-white');
|
||||
btn.classList.remove('border-transparent', 'text-txt-muted');
|
||||
} else {
|
||||
btn.classList.remove('border-accent-blue', 'text-white');
|
||||
btn.classList.add('border-transparent', 'text-txt-muted');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+16
-9
@@ -40,16 +40,23 @@ def dashboard(request):
|
||||
is_published=True
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
# Check which ones the student has submitted
|
||||
submissions = Submission.objects.filter(student=request.user)
|
||||
submitted_assignment_ids = submissions.values_list('assignment_id', flat=True)
|
||||
submissions_dict = {sub.assignment_id: sub for sub in submissions}
|
||||
|
||||
total_count = assignments.count()
|
||||
waiting_solution_count = sum(1 for a in assignments if a.id not in submissions_dict or submissions_dict[a.id].status == 'IN_PROGRESS')
|
||||
pending_count = sum(1 for a in assignments if a.id in submissions_dict and submissions_dict[a.id].status == 'PENDING')
|
||||
graded_count = sum(1 for a in assignments if a.id in submissions_dict and submissions_dict[a.id].status == 'GRADED')
|
||||
|
||||
from django.utils import timezone
|
||||
return render(request, 'lms/dashboard.html', {
|
||||
'assignments': assignments,
|
||||
'submitted_assignment_ids': submitted_assignment_ids,
|
||||
'submissions': {sub.assignment_id: sub for sub in submissions},
|
||||
'now': timezone.now()
|
||||
'submissions': submissions_dict,
|
||||
'now': timezone.now(),
|
||||
'total_count': total_count,
|
||||
'waiting_solution_count': waiting_solution_count,
|
||||
'pending_count': pending_count,
|
||||
'graded_count': graded_count,
|
||||
})
|
||||
|
||||
@login_required
|
||||
@@ -135,11 +142,11 @@ def assignment_detail(request, pk):
|
||||
if q.correct_answer:
|
||||
correct = strip_tags(q.correct_answer).strip().lower()
|
||||
answer_obj.is_correct = (val.strip().lower() == correct)
|
||||
answer_obj.points = 1 if answer_obj.is_correct else 0
|
||||
answer_obj.points = q.points if answer_obj.is_correct else 0
|
||||
total_points += answer_obj.points
|
||||
else:
|
||||
all_auto = False
|
||||
max_points += 1
|
||||
max_points += q.points
|
||||
elif q.question_type == 'TABLE_AUTO':
|
||||
rows = getattr(q, 'table_rows', 2) or 2
|
||||
table_data = []
|
||||
@@ -164,11 +171,11 @@ def assignment_detail(request, pk):
|
||||
is_correct = False
|
||||
break
|
||||
answer_obj.is_correct = is_correct
|
||||
answer_obj.points = 1 if is_correct else 0
|
||||
answer_obj.points = q.points if is_correct else 0
|
||||
total_points += answer_obj.points
|
||||
else:
|
||||
all_auto = False
|
||||
max_points += 1
|
||||
max_points += q.points
|
||||
else:
|
||||
all_auto = False
|
||||
val = form.cleaned_data.get(f'file_{q.id}')
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user