From cac63aadbbe1ad25455c43d9a29482c1e143ce95 Mon Sep 17 00:00:00 2001 From: Arty Date: Fri, 30 Jan 2026 19:19:34 +0300 Subject: [PATCH 01/17] feature: Auto variant assignment --- webapp/repositories.py | 7 ++++++ webapp/templates/student/group.jinja | 3 ++- webapp/templates/student/home.jinja | 27 +++++---------------- webapp/templates/student/task.jinja | 8 +++---- webapp/views/student.py | 35 ++++++++++------------------ 5 files changed, 30 insertions(+), 50 deletions(-) diff --git a/webapp/repositories.py b/webapp/repositories.py index c7447c28..c3569507 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -548,6 +548,13 @@ def update_group(self, student: int, group: int | None): .filter_by(id=student) \ .update(dict(group=group)) + def get_free_variant(self, group: int): + with self.db.create_session() as session: + var = session.query(func.max(Student.variant)) \ + .filter_by(group=group) \ + .scalar() + return var + 1 + def update_variant(self, student: int, variant_id: int | None): with self.db.create_session() as session: session.query(Student) \ diff --git a/webapp/templates/student/group.jinja b/webapp/templates/student/group.jinja index f088be51..a3aa92ee 100644 --- a/webapp/templates/student/group.jinja +++ b/webapp/templates/student/group.jinja @@ -105,7 +105,8 @@ {% for variant in group.variants %} - + {{ variant.id + 1 }} diff --git a/webapp/templates/student/home.jinja b/webapp/templates/student/home.jinja index 8149884a..f40d2e32 100644 --- a/webapp/templates/student/home.jinja +++ b/webapp/templates/student/home.jinja @@ -22,7 +22,7 @@

{{ greeting_message }}!

-
+
{% if group.tasks %} @@ -42,7 +42,7 @@
{% endif %}
-
+
Решено обязательных задач
@@ -101,26 +101,11 @@
-
-
-
- -
- {% if variants | length > 1 %} -
- -
- {% endif %} +
+
+ Вариант №{{ variant.id + 1 }}
- +
diff --git a/webapp/templates/student/task.jinja b/webapp/templates/student/task.jinja index f8053f03..96bda76d 100644 --- a/webapp/templates/student/task.jinja +++ b/webapp/templates/student/task.jinja @@ -91,8 +91,7 @@ {% else %}
-
@@ -113,8 +112,7 @@ {{ form.csrf_token }}
-
@@ -131,7 +129,7 @@
-{% if highlight and not (status.disabled or registration and not student) %} +{% if highlight and not disabled %} {% block head %}{% endblock %} diff --git a/webapp/templates/student/group.jinja b/webapp/templates/student/group.jinja index f7c8890a..d14053f6 100644 --- a/webapp/templates/student/group.jinja +++ b/webapp/templates/student/group.jinja @@ -104,6 +104,11 @@ {% for task in group.tasks %}
{% endfor %} diff --git a/webapp/templates/student/home.jinja b/webapp/templates/student/home.jinja index f40d2e32..292cba48 100644 --- a/webapp/templates/student/home.jinja +++ b/webapp/templates/student/home.jinja @@ -113,6 +113,11 @@ {% for task in group.tasks %} {% endfor %} diff --git a/webapp/templates/student/task.jinja b/webapp/templates/student/task.jinja index 50cba2b7..fd8f23a5 100644 --- a/webapp/templates/student/task.jinja +++ b/webapp/templates/student/task.jinja @@ -92,7 +92,7 @@ {% else %}
+ style="min-height: 200px;font-family: monospace, monospace;">{{ form.code.data if form.code.data }}
Введите ответ в поле выше. В отправляемом коде на языке программирования Python должна присутствовать функция diff --git a/webapp/views/teacher.py b/webapp/views/teacher.py index 79ee987d..26676119 100644 --- a/webapp/views/teacher.py +++ b/webapp/views/teacher.py @@ -80,14 +80,14 @@ def select_submissions(teacher: Student): @authorize(db.students, lambda s: s.teacher) def verify(teacher: Student, gid: int, vid: int, tid: int): db.statuses.verify(tid, vid, gid, teacher.id) - return redirect(f'/group/{gid}/variant/{vid}/task/{tid}') + return redirect(f'/group/{gid}') @blueprint.route("/teacher/unverify/group//variant//task/", methods=["GET"]) @authorize(db.students, lambda s: s.teacher) def unverify(teacher: Student, gid: int, vid: int, tid: int): db.statuses.unverify(tid, vid, gid, teacher.id) - return redirect(f'/group/{gid}/variant/{vid}/task/{tid}') + return redirect(f'/group/{gid}') @blueprint.route("/teacher", methods=["GET"]) From aa40cfd022b799c125a4791dc039a1c85d1f0996 Mon Sep 17 00:00:00 2001 From: Arty Date: Sun, 1 Feb 2026 17:21:18 +0300 Subject: [PATCH 13/17] feature: Points export --- tests/managers/test_export.py | 2 +- webapp/managers.py | 46 +++++++++++++++++------- webapp/repositories.py | 18 +++++++++- webapp/templates/teacher/dashboard.jinja | 22 ++++++++++-- webapp/views/teacher.py | 18 ++++++++-- 5 files changed, 85 insertions(+), 21 deletions(-) diff --git a/tests/managers/test_export.py b/tests/managers/test_export.py index 7a494098..75858b41 100644 --- a/tests/managers/test_export.py +++ b/tests/managers/test_export.py @@ -17,7 +17,7 @@ def test_export(db: AppDatabase): ext = ExternalTaskManager(db.groups, db.tasks) s = StatusManager(db.tasks, db.groups, db.variants, db.statuses, c, db.seeds, db.checks, ach, ext, db.students) m = StudentManager(c, db.students, db.mailers) - e = ExportManager(db.groups, db.messages, s, db.variants, db.tasks, db.students, m) + e = ExportManager(db.groups, db.messages, s, db.statuses, db.variants, db.tasks, db.students, m) (group, variant, task) = arrange_task(db) code = "main = lambda x: x**42" diff --git a/webapp/managers.py b/webapp/managers.py index 7db87467..fbef390c 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -455,7 +455,8 @@ def __init__( self, groups: GroupRepository, messages: MessageRepository, - statuses: StatusManager, + status_manager: StatusManager, + statuses: TaskStatusRepository, variants: VariantRepository, tasks: TaskRepository, students: StudentRepository, @@ -463,6 +464,7 @@ def __init__( ): self.groups = groups self.messages = messages + self.status_manager = status_manager self.statuses = statuses self.variants = variants self.tasks = tasks @@ -473,15 +475,37 @@ def export_messages(self, count: int | None, separator: str) -> str: messages = self.__get_latest_messages(count) group_titles = self.__get_group_titles() table = self.__create_messages_table(messages, group_titles) - delimiter = ";" if separator == ";" else "," - output = self.__create_csv(table, delimiter) - return output + return self.__create_csv(table, separator) def export_exam_results(self, group_id: int, separator: str) -> str: table = self.__create_exam_table(group_id) - delimiter = ";" if separator == "semicolon" else "," - output = self.__create_csv(table, delimiter) - return output + return self.__create_csv(table, separator) + + def export_points(self, group_id: int | None, separator: str) -> str: + table = self.__create_points_table(group_id) + return self.__create_csv(table, separator) + + def __create_points_table(self, group_id: int | None) -> list[list[str]]: + verified = [Status.Verified, Status.VerifiedFailed, Status.VerifiedSubmitted] + header = ['Адрес электронной почты'] + blocks = self.tasks.get_blocks() + for block in blocks: + header.append(block.title) + students = self.students.get_all() if group_id is None else \ + self.students.get_group_students(group_id) + table = [header] + for student in students: + if student.variant is None or student.group is None: + continue + row = [student.email] + for block in blocks: + block_done = True + for task in self.tasks.get_all_in_block(block.id): + status = self.statuses.get_task_status(task.id, student.variant, student.group) + block_done &= status is not None and status.status in verified + row.append(int(block_done)) + table.append(row) + return table def __create_messages_table(self, messages: list[Message], group_titles: dict[int, str]) -> list[list[str]]: rows = [["ID", "Время", "Группа", "Задача", "Вариант", "IP", "Отправитель", "Код"]] @@ -517,11 +541,7 @@ def __create_exam_table(self, group_id: int) -> list[list[str]]: row = [group_title, variant.id + 1] score = 0 for task in tasks: - info = self.statuses.get_task_status( - group_id, - variant.id, - task.id - ) + info = self.status_manager.get_task_status(group_id, variant.id, task.id) status = 1 if info.status.value == 2 else 0 row.append(status) row.append(info.external.group_title) @@ -547,7 +567,7 @@ def __get_latest_messages(self, count: int | None) -> list[Message]: def __create_csv(self, table: list[list[str]], delimiter: str): si = io.StringIO() - cw = csv.writer(si, delimiter=delimiter) + cw = csv.writer(si, delimiter=";" if delimiter == ";" else ",") cw.writerows(table) bom = u"\uFEFF" value = bom + si.getvalue() diff --git a/webapp/repositories.py b/webapp/repositories.py index 152c0e4a..a6975512 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -75,7 +75,7 @@ def get_by_prefix(self, prefix: str) -> list[Group]: def get_by_id(self, group_id: int) -> Group: with self.db.create_session() as session: - group = session.get(Group, group_id) + group = session.get_one(Group, group_id) return group def rename(self, group_id: int, title: str, external: str): @@ -115,6 +115,18 @@ def get_all_with_blocks(self) -> list[tuple[Task, TaskBlock | None]]: .all() return tasks + def get_blocks(self) -> list[TaskBlock]: + with self.db.create_session() as session: + blocks = session.query(TaskBlock).all() + return blocks + + def get_all_in_block(self, block: int) -> list[Task]: + with self.db.create_session() as session: + tasks = session.query(Task) \ + .filter_by(block=block) \ + .all() + return tasks + def get_by_id(self, task_id: int) -> Task: with self.db.create_session() as session: task = session.get_one(Task, task_id) @@ -544,6 +556,10 @@ class StudentRepository: def __init__(self, db: DbContextManager): self.db = db + def get_all(self) -> list[Student]: + with self.db.create_session() as session: + return session.query(Student).all() + def get_group_students(self, group_id: int) -> list[Student]: with self.db.create_session() as session: return session.query(Student) \ diff --git a/webapp/templates/teacher/dashboard.jinja b/webapp/templates/teacher/dashboard.jinja index e660c238..fd95241c 100644 --- a/webapp/templates/teacher/dashboard.jinja +++ b/webapp/templates/teacher/dashboard.jinja @@ -94,7 +94,7 @@
{% endif %}
- Выгрузка всех присланных сообщений + Выгрузка последних присланных сообщений
{% for g in glist %} - + {% endfor %} +
+
+ Выгрузка таблицы с оценками для СДО +
+ + + +
{% endblock %} \ No newline at end of file diff --git a/webapp/views/teacher.py b/webapp/views/teacher.py index 26676119..ca2ae515 100644 --- a/webapp/views/teacher.py +++ b/webapp/views/teacher.py @@ -30,7 +30,7 @@ ach = AchievementManager(config) statuses = StatusManager(db.tasks, db.groups, db.variants, db.statuses, config, db.seeds, db.checks, ach, ext, db.students) -exports = ExportManager(db.groups, db.messages, statuses, db.variants, db.tasks, db.students, students) +exports = ExportManager(db.groups, db.messages, statuses, db.statuses, db.variants, db.tasks, db.students, students) @blueprint.route("/teacher/submissions/group//variant//task/", @@ -93,7 +93,7 @@ def unverify(teacher: Student, gid: int, vid: int, tid: int): @blueprint.route("/teacher", methods=["GET"]) @authorize(db.students, lambda s: s.teacher) def dashboard(teacher: Student): - groups = db.groups.get_all() if config.config.no_background_worker or config.config.final_tasks else None + groups = db.groups.get_all() glist = db.groups.get_all() vlist = db.variants.get_all() tlist = db.tasks.get_all() @@ -229,7 +229,7 @@ def exam_csv(teacher: Student, group_id: int): @blueprint.route("/teacher/messages", methods=["GET"]) @authorize(db.students, lambda s: s.teacher) -def messages(teacher: Student): +def messages_csv(teacher: Student): separator = request.args.get('separator') count = request.args.get('count') value = exports.export_messages(count, separator) @@ -239,6 +239,18 @@ def messages(teacher: Student): return output +@blueprint.route("/teacher/points", methods=["GET"]) +@authorize(db.students, lambda s: s.teacher) +def points_csv(teacher: Student): + separator = request.args.get('separator') + group_id = request.args.get('group', None, type=int) + value = exports.export_points(group_id, separator) + output = make_response(value) + output.headers["Content-Disposition"] = "attachment; filename=points.csv" + output.headers["Content-type"] = "text/csv" + return output + + @blueprint.route("/teacher/group/", methods=["GET"]) @authorize(db.students, lambda s: s.teacher) def queue(teacher: Student, group_id: int): From 1cb878d5e61e61115718404bd1ba8e46f6389618 Mon Sep 17 00:00:00 2001 From: Arty Date: Sun, 1 Feb 2026 17:49:11 +0300 Subject: [PATCH 14/17] fix: Accelerate export --- webapp/managers.py | 16 ++++------------ webapp/repositories.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/webapp/managers.py b/webapp/managers.py index fbef390c..3d4bb6dd 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -486,24 +486,16 @@ def export_points(self, group_id: int | None, separator: str) -> str: return self.__create_csv(table, separator) def __create_points_table(self, group_id: int | None) -> list[list[str]]: - verified = [Status.Verified, Status.VerifiedFailed, Status.VerifiedSubmitted] - header = ['Адрес электронной почты'] blocks = self.tasks.get_blocks() - for block in blocks: - header.append(block.title) - students = self.students.get_all() if group_id is None else \ - self.students.get_group_students(group_id) - table = [header] + students = self.students.get_all() if group_id is None else self.students.get_group_students(group_id) + table = [['Адрес электронной почты', *[block.title for block in blocks]]] for student in students: if student.variant is None or student.group is None: continue row = [student.email] for block in blocks: - block_done = True - for task in self.tasks.get_all_in_block(block.id): - status = self.statuses.get_task_status(task.id, student.variant, student.group) - block_done &= status is not None and status.status in verified - row.append(int(block_done)) + done = self.tasks.is_block_done(block.id, student.variant, student.group) + row.append(int(done)) table.append(row) return table diff --git a/webapp/repositories.py b/webapp/repositories.py index a6975512..bf20a88c 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -2,7 +2,7 @@ import uuid from typing import Callable -from sqlalchemy import desc, func, literal, null, text +from sqlalchemy import desc, func, literal, null, text, exists from sqlalchemy.orm import Session from webapp.models import ( @@ -115,6 +115,23 @@ def get_all_with_blocks(self) -> list[tuple[Task, TaskBlock | None]]: .all() return tasks + def is_block_done(self, block: int, variant: int, group: int) -> bool: + with self.db.create_session() as session: + wip = session.query(Task.id) \ + .filter(Task.block == block) \ + .filter(~exists().where( + (TaskStatus.task == Task.id) & + (TaskStatus.variant == variant) & + (TaskStatus.group == group) & + TaskStatus.status.in_([ + Status.Verified, + Status.VerifiedFailed, + Status.VerifiedSubmitted, + ]) + )) \ + .first() + return wip is None + def get_blocks(self) -> list[TaskBlock]: with self.db.create_session() as session: blocks = session.query(TaskBlock).all() From 00335536424d0b34e796d10f5663aae55b01c369 Mon Sep 17 00:00:00 2001 From: Arty Date: Sun, 1 Feb 2026 20:13:15 +0300 Subject: [PATCH 15/17] fix: Linter --- webapp/dto.py | 2 +- webapp/repositories.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/dto.py b/webapp/dto.py index 1937f06f..7fa55547 100644 --- a/webapp/dto.py +++ b/webapp/dto.py @@ -1,6 +1,6 @@ from flask import Config -from webapp.models import FinalSeed, Group, Status, Student, Task, TaskStatus, TypeOfTask, Variant, TaskBlock +from webapp.models import FinalSeed, Group, Status, Student, Task, TaskBlock, TaskStatus, TypeOfTask, Variant class AppConfig: diff --git a/webapp/repositories.py b/webapp/repositories.py index bf20a88c..a42c3385 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -2,7 +2,7 @@ import uuid from typing import Callable -from sqlalchemy import desc, func, literal, null, text, exists +from sqlalchemy import desc, exists, func, literal, null, text from sqlalchemy.orm import Session from webapp.models import ( From 93b8f3f61f988ef910da524944490d7c298e2328 Mon Sep 17 00:00:00 2001 From: Arty Date: Sun, 1 Feb 2026 20:39:11 +0300 Subject: [PATCH 16/17] fix: Migration --- webapp/alembic/versions/20260201.14-02.add_task_blocks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webapp/alembic/versions/20260201.14-02.add_task_blocks.py b/webapp/alembic/versions/20260201.14-02.add_task_blocks.py index 6fddd935..14896783 100644 --- a/webapp/alembic/versions/20260201.14-02.add_task_blocks.py +++ b/webapp/alembic/versions/20260201.14-02.add_task_blocks.py @@ -17,6 +17,11 @@ def upgrade(): + op.create_table( + "task_blocks", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("title", sa.String, nullable=False), + ) with op.batch_alter_table('tasks') as bop: bop.add_column(sa.Column('block', sa.Integer, nullable=True)) bop.create_foreign_key('fk_tasks_block', 'task_blocks', ['block'], ['id']) @@ -26,3 +31,4 @@ def downgrade(): with op.batch_alter_table("tasks") as bop: bop.drop_constraint("fk_tasks_block", type_="foreignkey") bop.drop_column("block") + op.drop_table("task_blocks") From 59620d7985709f793dc201fc8380a2d77bf6b71d Mon Sep 17 00:00:00 2001 From: Arty Date: Sun, 1 Feb 2026 20:54:17 +0300 Subject: [PATCH 17/17] feature: Add weight --- webapp/alembic/versions/20260201.14-02.add_task_blocks.py | 1 + webapp/managers.py | 2 +- webapp/models.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/webapp/alembic/versions/20260201.14-02.add_task_blocks.py b/webapp/alembic/versions/20260201.14-02.add_task_blocks.py index 14896783..555cc3c4 100644 --- a/webapp/alembic/versions/20260201.14-02.add_task_blocks.py +++ b/webapp/alembic/versions/20260201.14-02.add_task_blocks.py @@ -21,6 +21,7 @@ def upgrade(): "task_blocks", sa.Column("id", sa.Integer, primary_key=True, nullable=False), sa.Column("title", sa.String, nullable=False), + sa.Column("weight", sa.Integer, nullable=False), ) with op.batch_alter_table('tasks') as bop: bop.add_column(sa.Column('block', sa.Integer, nullable=True)) diff --git a/webapp/managers.py b/webapp/managers.py index 3d4bb6dd..335bd323 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -495,7 +495,7 @@ def __create_points_table(self, group_id: int | None) -> list[list[str]]: row = [student.email] for block in blocks: done = self.tasks.is_block_done(block.id, student.variant, student.group) - row.append(int(done)) + row.append(done * block.weight) table.append(row) return table diff --git a/webapp/models.py b/webapp/models.py index 5a6f6796..e36d7f13 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -86,6 +86,7 @@ class TaskBlock(Base): __tablename__ = "task_blocks" id = sa.Column("id", sa.Integer, primary_key=True, nullable=False, autoincrement=True) title = sa.Column("title", sa.String, nullable=False) + weight = sa.Column("weight", sa.Integer, nullable=False) class Variant(Base):
№{{ task.id + 1 }} + {% if task.block is not none %} + + {{ task.block + 1 }} + + {% endif %}
№{{ task.id + 1 }} + {% if task.block is not none %} + + {{ task.block + 1 }} + + {% endif %}