Add customizable passing threshold and display statistics to students
This commit is contained in:
+10
-22
@@ -125,23 +125,12 @@ class SubmissionAdmin(ModelAdmin):
|
|||||||
|
|
||||||
@admin.display(description='Статистика (сохраните оценку в формате X/Y, чтобы обновить)')
|
@admin.display(description='Статистика (сохраните оценку в формате X/Y, чтобы обновить)')
|
||||||
def statistics(self, obj):
|
def statistics(self, obj):
|
||||||
if not obj or not obj.grade:
|
stats = obj.get_statistics() if obj else None
|
||||||
|
|
||||||
|
if not stats:
|
||||||
return format_html('<div style="color: #8b949e;">Оценка не выставлена или имеет неверный формат. Укажите оценку в формате "X/Y" (например, "3/27").</div>')
|
return format_html('<div style="color: #8b949e;">Оценка не выставлена или имеет неверный формат. Укажите оценку в формате "X/Y" (например, "3/27").</div>')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parts = obj.grade.split('/')
|
|
||||||
earned = float(parts[0].strip())
|
|
||||||
total = float(parts[1].strip())
|
|
||||||
percentage = (earned / total) * 100 if total > 0 else 0
|
|
||||||
percentage_str = f"{percentage:.1f}"
|
|
||||||
|
|
||||||
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(
|
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="background-color: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; color: #c9d1d9; font-family: ui-sans-serif, system-ui, sans-serif;">
|
||||||
@@ -173,17 +162,16 @@ class SubmissionAdmin(ModelAdmin):
|
|||||||
</div>
|
</div>
|
||||||
''',
|
''',
|
||||||
grade=obj.grade,
|
grade=obj.grade,
|
||||||
percentage_str=percentage_str,
|
percentage_str=stats['percentage_str'],
|
||||||
threshold_text=threshold_text,
|
threshold_text=stats['threshold_text'],
|
||||||
color=color,
|
color=stats['color'],
|
||||||
total_int=total_int,
|
total_int=stats['total'],
|
||||||
threshold_points=threshold_points
|
threshold_points=stats['threshold_points']
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return format_html(
|
return format_html(
|
||||||
'<div style="color: #f85149;">Оценка имеет неверный формат. Ошибка: {error}<br>Значение: "{val}"</div>',
|
'<div style="color: #f85149;">Ошибка при отрисовке статистики: {error}</div>',
|
||||||
error=str(e),
|
error=str(e)
|
||||||
val=obj.grade
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Assignment(models.Model):
|
|||||||
deadline = models.DateTimeField('Дедлайн', blank=True, null=True, help_text='После этого времени ответы не принимаются.')
|
deadline = models.DateTimeField('Дедлайн', blank=True, null=True, help_text='После этого времени ответы не принимаются.')
|
||||||
time_limit = models.PositiveIntegerField('Лимит времени (минуты)', blank=True, null=True, help_text='Например, 235 для 3ч 55м. Оставьте пустым, если без таймера.')
|
time_limit = models.PositiveIntegerField('Лимит времени (минуты)', blank=True, null=True, help_text='Например, 235 для 3ч 55м. Оставьте пустым, если без таймера.')
|
||||||
allow_late_submissions = models.BooleanField('Разрешить дорешивание', default=True, help_text='Если включено, ученики смогут сдавать задание после дедлайна с пометкой "После дедлайна".')
|
allow_late_submissions = models.BooleanField('Разрешить дорешивание', default=True, help_text='Если включено, ученики смогут сдавать задание после дедлайна с пометкой "После дедлайна".')
|
||||||
|
passing_threshold = models.PositiveIntegerField('Порог прохождения (%)', default=40, help_text='Процент выполнения, необходимый для прохождения. Например, 40.')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Задание'
|
verbose_name = 'Задание'
|
||||||
@@ -80,6 +81,36 @@ class Submission(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Ответ от {self.student.username} на {self.assignment.title}"
|
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_percent = self.assignment.passing_threshold
|
||||||
|
is_passed = percentage >= threshold_percent
|
||||||
|
|
||||||
|
total_int = int(total) if total.is_integer() else total
|
||||||
|
earned_int = int(earned) if earned.is_integer() else earned
|
||||||
|
threshold_points = int(total * (threshold_percent / 100.0))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'earned': earned_int,
|
||||||
|
'total': total_int,
|
||||||
|
'percentage': percentage,
|
||||||
|
'percentage_str': f"{percentage:.1f}",
|
||||||
|
'is_passed': is_passed,
|
||||||
|
'threshold_percent': threshold_percent,
|
||||||
|
'threshold_points': threshold_points,
|
||||||
|
'threshold_text': 'Выше порога' if is_passed else f'Ниже порога (Мин: {threshold_percent}%)',
|
||||||
|
'color': '#2ea043' if is_passed else '#f85149'
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
class Answer(models.Model):
|
class Answer(models.Model):
|
||||||
submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name='answers', verbose_name='Решение (Submission)')
|
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='Задача')
|
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers', verbose_name='Задача')
|
||||||
|
|||||||
@@ -37,28 +37,74 @@
|
|||||||
<h3 class="text-lg font-bold text-white mb-6 font-heading border-b border-obsidian-border pb-3">Ваш ответ</h3>
|
<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' %}
|
{% 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 %}">
|
{% with stats=submission.get_statistics %}
|
||||||
{% if submission.status == 'GRADED' %}
|
{% if stats %}
|
||||||
<h4 class="font-bold mb-2 flex items-center gap-2 font-heading">
|
<div class="mb-8 p-6 rounded-xl border bg-obsidian-base border-obsidian-borderLight shadow-sm">
|
||||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
<div class="flex items-center gap-4">
|
||||||
</svg>
|
<div class="flex-shrink-0">
|
||||||
Оценка: {{ submission.grade|default:"Не выставлена" }}
|
<div class="font-mono text-[10px] text-txt-faint uppercase tracking-wider mb-1">Итоговая оценка</div>
|
||||||
</h4>
|
<div class="flex items-baseline gap-2">
|
||||||
{% else %}
|
<div class="bg-obsidian-card border border-obsidian-border rounded-lg px-4 py-2 text-2xl font-bold text-white shadow-sm">
|
||||||
<h4 class="font-bold mb-2 flex items-center gap-2 font-heading">
|
{{ submission.grade }}
|
||||||
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
|
</div>
|
||||||
Ваша работа отправлена и ожидает проверки.
|
<span class="text-xs text-txt-muted">первичных баллов</span>
|
||||||
</h4>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if submission.teacher_feedback %}
|
<div class="flex-grow max-w-md w-full">
|
||||||
<div class="mt-4 p-4 rounded bg-obsidian-base border border-obsidian-borderLight">
|
<div class="flex justify-between items-end mb-2">
|
||||||
<p class="font-medium text-white text-sm mb-1">Общий комментарий преподавателя:</p>
|
<span class="text-sm text-txt-secondary">Выполнение: <b class="text-white">{{ stats.percentage_str }}%</b></span>
|
||||||
<p class="text-sm text-txt-secondary">{{ submission.teacher_feedback }}</p>
|
<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>
|
</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>
|
<h4 class="font-bold mb-5 text-white font-heading">Ваши ответы на задачи:</h4>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user