128 lines
8.5 KiB
Python
128 lines
8.5 KiB
Python
from django.db import models
|
||
from django.conf import settings
|
||
from .validators import validate_file_extension
|
||
from ckeditor_uploader.fields import RichTextUploadingField
|
||
|
||
class Subject(models.Model):
|
||
name = models.CharField('Название предмета', max_length=100)
|
||
|
||
class Meta:
|
||
verbose_name = 'Предмет'
|
||
verbose_name_plural = 'Предметы'
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
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='Снимите галочку, чтобы скрыть задание (как черновик).')
|
||
assign_to_all = models.BooleanField('Назначить всем', default=True, help_text='Если включено, задание будет доступно всем ученикам.')
|
||
assigned_students = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='assigned_assignments', verbose_name='Назначенные ученики', blank=True, limit_choices_to={'role': 'STUDENT'}, help_text='Выберите учеников, если задание не для всех.')
|
||
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, limit_choices_to={'role__in': ['TEACHER', 'ADMIN']}, verbose_name='Кем создано')
|
||
created_at = models.DateTimeField('Дата создания', auto_now_add=True)
|
||
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 = 'Задание'
|
||
verbose_name_plural = 'Задания'
|
||
|
||
def __str__(self):
|
||
return f"{self.title} ({self.subject})"
|
||
|
||
class Question(models.Model):
|
||
TYPE_CHOICES = (
|
||
('TEXT_AUTO', 'Краткий ответ (автопроверка)'),
|
||
('FILE_MANUAL', 'Файл/Развернутый ответ (ручная проверка)'),
|
||
('TABLE_AUTO', 'Таблица (2 столбца, автопроверка)'),
|
||
)
|
||
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE, related_name='questions', verbose_name='Задание')
|
||
text = RichTextUploadingField('Текст задачи')
|
||
attached_file = models.FileField('Файл (опционально)', upload_to='questions/', blank=True, null=True, validators=[validate_file_extension])
|
||
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:
|
||
verbose_name = 'Задача'
|
||
verbose_name_plural = 'Задачи'
|
||
ordering = ['order', 'id']
|
||
|
||
def __str__(self):
|
||
number = self.order if self.order else self.id
|
||
return f"Задача {number} к {self.assignment.title}"
|
||
|
||
class Submission(models.Model):
|
||
STATUS_CHOICES = (
|
||
('IN_PROGRESS', 'В процессе выполнения'),
|
||
('PENDING', 'Ожидает проверки'),
|
||
('GRADED', 'Проверено'),
|
||
)
|
||
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE, related_name='submissions', verbose_name='Задание')
|
||
student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, limit_choices_to={'role': 'STUDENT'}, verbose_name='Ученик')
|
||
grade = models.CharField('Оценка', max_length=10, blank=True, null=True)
|
||
teacher_feedback = models.TextField('Комментарий учителя', blank=True, null=True)
|
||
status = models.CharField('Статус', max_length=20, choices=STATUS_CHOICES, default='PENDING')
|
||
started_at = models.DateTimeField('Время начала', blank=True, null=True)
|
||
submitted_at = models.DateTimeField('Дата отправки', auto_now_add=True)
|
||
is_time_expired = models.BooleanField('Время вышло', default=False)
|
||
is_late = models.BooleanField('Сдано после дедлайна', default=False)
|
||
|
||
class Meta:
|
||
verbose_name = 'Ответ ученика'
|
||
verbose_name_plural = 'Ответы учеников'
|
||
|
||
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='Задача')
|
||
text_answer = models.TextField('Текстовый ответ', blank=True, null=True)
|
||
file_answer = models.FileField('Файл с ответом', upload_to='answers/%Y/%m/%d/', blank=True, null=True, validators=[validate_file_extension])
|
||
is_correct = models.BooleanField('Правильно?', null=True, blank=True)
|
||
points = models.PositiveIntegerField('Баллы', blank=True, null=True)
|
||
comment = models.TextField('Комментарий', blank=True, null=True)
|
||
|
||
class Meta:
|
||
verbose_name = 'Ответ на задачу'
|
||
verbose_name_plural = 'Ответы на задачи'
|
||
|
||
def __str__(self):
|
||
return f"Ответ на {self.question}"
|