diff --git a/lmsege/lms/admin.py b/lmsege/lms/admin.py index a12fa88..a325913 100644 --- a/lmsege/lms/admin.py +++ b/lmsege/lms/admin.py @@ -18,9 +18,9 @@ class QuestionInline(StackedInline): @admin.register(Assignment) class AssignmentAdmin(ModelAdmin): - list_display = ('title', 'subject', 'is_published', 'is_closed', 'deadline', 'created_at') + list_display = ('title', 'subject', 'is_published', 'time_limit', 'deadline', 'created_at') list_display_links = ('title',) - list_filter = ('subject', 'is_published', 'is_closed') + list_filter = ('subject', 'is_published') search_fields = ('title', 'description') exclude = ('created_by',) filter_horizontal = ('assigned_students',) @@ -102,8 +102,8 @@ class AnswerInline(StackedInline): @admin.register(Submission) class SubmissionAdmin(ModelAdmin): change_list_template = "admin/lms/submission/change_list.html" - list_display = ('assignment', 'student', 'status', 'grade', 'submitted_at') - list_filter = ('status', 'assignment__subject', 'student') + list_display = ('assignment', 'student', 'status', 'is_time_expired', 'grade', 'submitted_at') + list_filter = ('status', 'assignment__subject', 'student', 'is_time_expired') search_fields = ('student__username', 'assignment__title') inlines = [AnswerInline] diff --git a/lmsege/lms/models.py b/lmsege/lms/models.py index f7593b3..701a17d 100644 --- a/lmsege/lms/models.py +++ b/lmsege/lms/models.py @@ -23,7 +23,7 @@ class Assignment(models.Model): 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='После этого времени ответы не принимаются.') - is_closed = models.BooleanField('Прием ответов закрыт', default=False, help_text='Включите, чтобы вручную закрыть прием ответов на задание.') + time_limit = models.PositiveIntegerField('Лимит времени (минуты)', blank=True, null=True, help_text='Например, 235 для 3ч 55м. Оставьте пустым, если без таймера.') class Meta: verbose_name = 'Задание' @@ -56,6 +56,7 @@ class Question(models.Model): class Submission(models.Model): STATUS_CHOICES = ( + ('IN_PROGRESS', 'В процессе выполнения'), ('PENDING', 'Ожидает проверки'), ('GRADED', 'Проверено'), ) @@ -64,7 +65,9 @@ class Submission(models.Model): 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) class Meta: verbose_name = 'Ответ ученика' diff --git a/lmsege/lms/templates/lms/assignment_detail.html b/lmsege/lms/templates/lms/assignment_detail.html index be7c4e9..dfa091b 100644 --- a/lmsege/lms/templates/lms/assignment_detail.html +++ b/lmsege/lms/templates/lms/assignment_detail.html @@ -94,13 +94,87 @@ {% endfor %} + {% elif needs_start %} +
+ hourglass_top +

Задание на время

+

На выполнение этого задания отводится {{ assignment.time_limit }} минут(ы). Как только вы нажмете кнопку ниже, запустится обратный отсчет. По истечении времени ваши ответы будут отправлены автоматически.

+ +
+ {% csrf_token %} + + +
+
{% elif not can_submit %}

Прием ответов завершен

-

Вы больше не можете отправлять ответы, так как дедлайн истек или преподаватель закрыл задание.

+

Вы больше не можете отправлять ответы, так как дедлайн истек или время на выполнение закончилось.

{% else %} -
+ {% if time_left_seconds is not None %} +
+
+ timer + Осталось времени: +
+
+ --:--:-- +
+
+ + {% endif %} + + {% csrf_token %} {% for q, field_type, field in question_fields %} diff --git a/lmsege/lms/templates/lms/dashboard.html b/lmsege/lms/templates/lms/dashboard.html index d698a83..b5857ce 100644 --- a/lmsege/lms/templates/lms/dashboard.html +++ b/lmsege/lms/templates/lms/dashboard.html @@ -52,15 +52,15 @@ Добавлено: {{ assignment.created_at|date:"d M Y" }} {% if assignment.deadline %} - + timer Дедлайн: {{ assignment.deadline|date:"d M Y, H:i" }} {% endif %} - {% if assignment.is_closed %} - - lock - Прием ответов закрыт + {% if assignment.time_limit %} + + hourglass_empty + Время на решение: {{ assignment.time_limit }} мин. {% endif %} diff --git a/lmsege/lms/views.py b/lmsege/lms/views.py index bae142b..db48349 100644 --- a/lmsege/lms/views.py +++ b/lmsege/lms/views.py @@ -62,31 +62,64 @@ def assignment_detail(request, pk): submission = Submission.objects.filter(assignment=assignment, student=request.user).first() from django.utils import timezone + import datetime + + now = timezone.now() can_submit = True - if assignment.is_closed: - can_submit = False - if assignment.deadline and timezone.now() > assignment.deadline: + time_left_seconds = None + needs_start = False + + if assignment.deadline and now > assignment.deadline: can_submit = False - if submission: - if request.method == 'POST': - # Do not allow updating + if assignment.time_limit: + if not submission: + needs_start = True + can_submit = False # Can't submit until started + elif submission.status == 'IN_PROGRESS': + if submission.started_at: + elapsed = (now - submission.started_at).total_seconds() + allowed = assignment.time_limit * 60 + if elapsed > allowed + 60: # 1 minute grace period for network latency + can_submit = False + submission.is_time_expired = True + submission.status = 'PENDING' # Or graded with 0 + submission.save() + else: + time_left_seconds = int(allowed - elapsed) + if time_left_seconds < 0: + time_left_seconds = 0 + else: + can_submit = False # Already submitted + + if request.method == 'POST': + if needs_start and 'action' in request.POST and request.POST['action'] == 'start': + Submission.objects.create( + assignment=assignment, + student=request.user, + status='IN_PROGRESS', + started_at=now + ) + return redirect('lms:assignment_detail', pk=pk) + + if submission and submission.status != 'IN_PROGRESS': return redirect('lms:dashboard') - form = None - else: - if not can_submit: - form = None - elif request.method == 'POST': + + if can_submit and not needs_start: form = AnswersForm(request.POST, request.FILES, questions=questions) if form.is_valid(): - submission = Submission.objects.create(assignment=assignment, student=request.user) + if not submission: + submission = Submission.objects.create(assignment=assignment, student=request.user) all_auto = True total_points = 0 max_points = 0 for q in questions: - answer_obj = Answer(submission=submission, question=q) + answer_obj = Answer.objects.filter(submission=submission, question=q).first() + if not answer_obj: + answer_obj = Answer(submission=submission, question=q) + if q.question_type == 'TEXT_AUTO': val = form.cleaned_data.get(f'answer_{q.id}', '') answer_obj.text_answer = val @@ -142,7 +175,12 @@ def assignment_detail(request, pk): submission.save() return redirect('lms:dashboard') else: + form = None + else: + if can_submit and not needs_start: form = AnswersForm(questions=questions) + else: + form = None question_fields = [] if form: @@ -165,4 +203,6 @@ def assignment_detail(request, pk): 'questions': questions, 'question_fields': question_fields, 'can_submit': can_submit, + 'needs_start': needs_start, + 'time_left_seconds': time_left_seconds, })