From 7009f90a6c10b24f526938a9aa0e90da85e53549 Mon Sep 17 00:00:00 2001 From: Dmitry Belan Date: Mon, 28 Sep 2026 23:35:35 +0300 Subject: [PATCH] Add customizable passing threshold and display statistics to students --- lmsege/lms/admin.py | 32 ++----- lmsege/lms/models.py | 31 ++++++ .../lms/templates/lms/assignment_detail.html | 88 +++++++++++++----- .../__pycache__/settings.cpython-314.pyc | Bin 3635 -> 5385 bytes 4 files changed, 108 insertions(+), 43 deletions(-) diff --git a/lmsege/lms/admin.py b/lmsege/lms/admin.py index 923e4d5..04f79af 100644 --- a/lmsege/lms/admin.py +++ b/lmsege/lms/admin.py @@ -125,23 +125,12 @@ class SubmissionAdmin(ModelAdmin): @admin.display(description='Статистика (сохраните оценку в формате X/Y, чтобы обновить)') 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('
Оценка не выставлена или имеет неверный формат. Укажите оценку в формате "X/Y" (например, "3/27").
') 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( '''
@@ -173,17 +162,16 @@ class SubmissionAdmin(ModelAdmin):
''', grade=obj.grade, - percentage_str=percentage_str, - threshold_text=threshold_text, - color=color, - total_int=total_int, - threshold_points=threshold_points + 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( - '
Оценка имеет неверный формат. Ошибка: {error}
Значение: "{val}"
', - error=str(e), - val=obj.grade + '
Ошибка при отрисовке статистики: {error}
', + error=str(e) ) def get_queryset(self, request): diff --git a/lmsege/lms/models.py b/lmsege/lms/models.py index 804631f..4442d74 100644 --- a/lmsege/lms/models.py +++ b/lmsege/lms/models.py @@ -26,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=40, help_text='Процент выполнения, необходимый для прохождения. Например, 40.') class Meta: verbose_name = 'Задание' @@ -80,6 +81,36 @@ 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_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): 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='Задача') diff --git a/lmsege/lms/templates/lms/assignment_detail.html b/lmsege/lms/templates/lms/assignment_detail.html index 3bd36c2..898f8e6 100644 --- a/lmsege/lms/templates/lms/assignment_detail.html +++ b/lmsege/lms/templates/lms/assignment_detail.html @@ -37,28 +37,74 @@

Ваш ответ

{% if submission and submission.status != 'IN_PROGRESS' %} -
- {% if submission.status == 'GRADED' %} -

- - - - Оценка: {{ submission.grade|default:"Не выставлена" }} -

- {% else %} -

- - Ваша работа отправлена и ожидает проверки. -

- {% endif %} - - {% if submission.teacher_feedback %} -
-

Общий комментарий преподавателя:

-

{{ submission.teacher_feedback }}

+ {% with stats=submission.get_statistics %} + {% if stats %} +
+
+
+
+
Итоговая оценка
+
+
+ {{ submission.grade }} +
+ первичных баллов +
+
+
+ +
+
+ Выполнение: {{ stats.percentage_str }}% + + {{ stats.threshold_text }} + +
+
+
+
+
+ 0 б + Порог: {{ stats.threshold_points }} б + Максимум: {{ stats.total }} б +
+
- {% endif %} -
+ + {% if submission.teacher_feedback %} +
+

Комментарий преподавателя

+

+ {{ submission.teacher_feedback }} +

+
+ {% endif %} +
+ {% else %} +
+ {% if submission.status == 'GRADED' %} +

+ + + + Оценка: {{ submission.grade|default:"Не выставлена" }} +

+ {% else %} +

+ + Ваша работа отправлена и ожидает проверки. +

+ {% endif %} + + {% if submission.teacher_feedback %} +
+

Общий комментарий преподавателя:

+

{{ submission.teacher_feedback }}

+
+ {% endif %} +
+ {% endif %} + {% endwith %}

Ваши ответы на задачи:

diff --git a/lmsege/lmsege/__pycache__/settings.cpython-314.pyc b/lmsege/lmsege/__pycache__/settings.cpython-314.pyc index 19f8bade991828069ba87656d8df26088f752d56..36748e351e61b9c3a8d81fa0af7eee5617169b90 100644 GIT binary patch delta 2178 zcmbVMOK%fb6rPD4+t|+gVJ9IW5EAnA_?g5&DIqc7HpJ<%fzp<`)Dbm4GxVDzyBAcDysQLhYhk3)Fqjoe&7>rtOXH$@iW2y>qTN_kKP) zJl5D?XK>Ayww6EbVwgWD*;zv`!1G^C_lAFG%i#2|q|?K|$1#?;paB}837Vk=TA>Zv zp#wTyjE4mmxQ+i^&<#DX7kZ%&_CY`FhXXJG2VoF~;1CSM2#mrQjKc(YfCDe*03Z7M zU-$>mKZyPz^q)lkDJYM?GzNq*;4lUp;T&8Y9ECUF7`%!5ILyEam_;;)C;}(p6r6@A zXF~QCvbT}V8|)0S1!OVK63EV?XI+^J&cxY>8}HyM-a!|f zBi(Qw-h~UOc@Im!4<8^}jj4TWvZ=_tbL)jBbt3 zLUb|lZ~E!n>1SEtc$G*Be_8Y_D{>R&k?BQlcOkb@PV1p4Gt3;d>{Rv2?MQXYY-~OT zaot*ya|K?$s@H|QtJZ;xAOAAC$>*g^^od@|{00-ef{(I*!NPo4pe$&xY43D^vXH?h zy_5coS2s-}D_aMG;b1B~shg$@V}K8&1T;XbBgOkBfD2Tu>213CQg0e5{!!sfB1$`-h3IU!Y z;P+E74IwaS;e zIVVU(eGks4noLR3if+#5iYu#a98aGDeL0+>rj|v4aB3rdI_+ESE{5NwWhysxJFh6Y zC1b=_J9owGsLMr+>!oowE7!F9+eQ0#b^4R`i*{ePXLteMNLCb9UQ2_e3puPH6*<*S zv)n@Whn26iA8D9&7jtfD548unr6fwFytwLq(Sxdpvcd`KBz-QFaH^BWZ&cPQ*R{vm zV?4TLNtV=-vfBD024_0U89Hv`hWY&FpXQ-@c4DK^eruwA7Jpuo2{;>1MCOt)h@Ofr z#PvoLiJAF%RLv|d#&yftg_E)Qxy?uR0d{cvS%pqIW&LQ?+_dgSq_qXAX6F;zRLxY^WT{!0 z`oeu%BsKIF% zrqS?HfBYYMS95o3&D!43SQ}?r+*()QwE{<|zs<;V_*I_UeXrEPZFPeK({x*#Fwex0 c%U-jN;%&xQVC{8lmS@?z&H9E>)=ac&`QZJx~-#uu68 zvVbQriU|t{l^G^yn6khWxPqfJPhy6rFu>E)8O-u5h2|Dxcn(#rlA8ag=IIC@7U(SC z1uXI+mUxL;LyecoE2EAER}^s>%H#HWj`};+`=Q+Kb{llIJ;tT z84SBCKg-h5al2D%G!E)#wN|U`RQ)QfzOc#kr}k)NCv@FEA6{MT-5!!ad2H|t_Av0_ T8H?Va>s}fw&Syvmp-$Ex;*w9&