diff --git a/tests/managers/test_export.py b/tests/managers/test_export.py index 0ae5f894..75858b41 100644 --- a/tests/managers/test_export.py +++ b/tests/managers/test_export.py @@ -15,9 +15,9 @@ def test_export(db: AppDatabase): c = AppConfigManager(lambda: dict()) ach = AchievementManager(c) ext = ExternalTaskManager(db.groups, db.tasks) - s = StatusManager(db.tasks, db.groups, db.variants, db.statuses, c, db.seeds, db.checks, ach, ext) + 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/tests/ui/test_task_status.py b/tests/ui/test_task_status.py index 15e4da5c..a6e09311 100644 --- a/tests/ui/test_task_status.py +++ b/tests/ui/test_task_status.py @@ -47,9 +47,9 @@ def test_task_status_html_status(db: AppDatabase, client: FlaskClient): c = 'd-block text-center text-decoration-none p-1 position-relative' for ok, text, color in [ (False, 'x', 'background-color:#ffe3ee'), - (True, '+', 'background-color:#e3ffee'), - (False, '+', 'background-color:#e3ffee'), - (True, '+', 'background-color:#e3ffee'), + (True, '+', 'background-color:#fff9e3'), + (False, '+', 'background-color:#fff9e3'), + (True, '+', 'background-color:#fff9e3'), ]: db.statuses.check(tasks_id, variant_id, group_id, unique_str(), ok, unique_str(), unique_str()) response = client.get(f'/group/{group_id}') diff --git a/webapp/alembic/versions/20260201.13-22.add_reviewer_to_task_status.py b/webapp/alembic/versions/20260201.13-22.add_reviewer_to_task_status.py new file mode 100644 index 00000000..c317d82d --- /dev/null +++ b/webapp/alembic/versions/20260201.13-22.add_reviewer_to_task_status.py @@ -0,0 +1,28 @@ +"""add_reviewer_to_task_status + +Revision ID: 2a3942c432db +Revises: 3782564289c1 +Create Date: 2026-02-01 13:22:32.287727 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2a3942c432db' +down_revision = '3782564289c1' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('task_statuses') as bop: + bop.add_column(sa.Column('reviewer', sa.Integer, nullable=True)) + bop.create_foreign_key('fk_task_statuses_reviewer', 'students', ['reviewer'], ['id']) + + +def downgrade(): + with op.batch_alter_table("task_statuses") as bop: + bop.drop_constraint("fk_task_statuses_reviewer", type_="foreignkey") + bop.drop_column("reviewer") diff --git a/webapp/alembic/versions/20260201.14-02.add_task_blocks.py b/webapp/alembic/versions/20260201.14-02.add_task_blocks.py new file mode 100644 index 00000000..555cc3c4 --- /dev/null +++ b/webapp/alembic/versions/20260201.14-02.add_task_blocks.py @@ -0,0 +1,35 @@ +"""add_task_blocks + +Revision ID: a8e656f1b1df +Revises: 2a3942c432db +Create Date: 2026-02-01 14:02:29.220477 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a8e656f1b1df' +down_revision = '2a3942c432db' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "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)) + bop.create_foreign_key('fk_tasks_block', 'task_blocks', ['block'], ['id']) + + +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") diff --git a/webapp/dto.py b/webapp/dto.py index 5d56f629..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 +from webapp.models import FinalSeed, Group, Status, Student, Task, TaskBlock, TaskStatus, TypeOfTask, Variant class AppConfig: @@ -43,11 +43,13 @@ def __init__(self, group: int, group_title: str, task: int, variant: int, active class TaskDto: - def __init__(self, task: Task, seed: FinalSeed | None): + def __init__(self, task: Task, block: TaskBlock | None, seed: FinalSeed | None): self.id = int(task.id) self.formulation = task.formulation self.active = task.type == TypeOfTask.Static or seed and seed.active self.is_random = task.type == TypeOfTask.Random + self.block_title = block.title if block else '' + self.block = block and block.id class AchievementDto: @@ -113,9 +115,12 @@ def formulation_url(self) -> str: def cell_background(self) -> str: return self.map_status({ Status.Submitted: "inherit", - Status.Checked: "#e3ffee", - Status.CheckedSubmitted: "#e3ffee", - Status.CheckedFailed: "#e3ffee", + Status.Checked: "#fff9e3", + Status.CheckedSubmitted: "#fff9e3", + Status.CheckedFailed: "#fff9e3", + Status.Verified: "#e3ffee", + Status.VerifiedSubmitted: "#e3ffee", + Status.VerifiedFailed: "#e3ffee", Status.Failed: "#ffe3ee", Status.NotSubmitted: "inherit", }) @@ -127,6 +132,9 @@ def name(self) -> str: Status.Checked: "Зачтено", Status.CheckedSubmitted: "Зачтено. Отправлено повторно", Status.CheckedFailed: "Зачтено. Ошибка при повторной отправке!", + Status.Verified: "Защищено", + Status.VerifiedSubmitted: "Защищено. Отправлено повторно", + Status.VerifiedFailed: "Защищено. Ошибка при повторной отправке!", Status.Failed: "Ошибка!", Status.NotSubmitted: "Не отправлено", }) @@ -138,6 +146,9 @@ def code(self) -> str: Status.Checked: "+", Status.CheckedSubmitted: "+", Status.CheckedFailed: "+", + Status.Verified: "✓", + Status.VerifiedSubmitted: "✓", + Status.VerifiedFailed: "✓", Status.Failed: "x", Status.NotSubmitted: "-", }) @@ -145,30 +156,49 @@ def code(self) -> str: @property def color(self) -> str: return self.map_status({ - Status.Submitted: "primary", - Status.Checked: "success", - Status.CheckedSubmitted: "success", - Status.CheckedFailed: "success", + Status.Submitted: "secondary", + Status.Checked: "warning", + Status.CheckedSubmitted: "warning", + Status.CheckedFailed: "warning", + Status.Verified: "success", + Status.VerifiedSubmitted: "success", + Status.VerifiedFailed: "success", Status.Failed: "danger", Status.NotSubmitted: "secondary", }) + @property + def show_achievements(self) -> bool: + return self.achievements and self.status in [ + Status.Checked, + Status.CheckedSubmitted, + Status.CheckedFailed, + Status.Verified, + Status.VerifiedSubmitted, + Status.VerifiedFailed, + ] + + @property + def can_verify(self) -> bool: + return self.status in [ + Status.Checked, + Status.CheckedSubmitted, + Status.CheckedFailed, + ] + + @property + def can_unverify(self) -> bool: + return self.status in [ + Status.Verified, + Status.VerifiedSubmitted, + Status.VerifiedFailed, + ] + @property def disabled(self) -> bool: active = self.external.active return not active or self.readonly - @property - def show_achievements(self) -> bool: - return self.achievements and self.map_status({ - Status.Submitted: False, - Status.Failed: False, - Status.NotSubmitted: False, - Status.Checked: True, - Status.CheckedSubmitted: True, - Status.CheckedFailed: True, - }) - def map_achievements(self, status: TaskStatus | None, achievements: list[int]): dtos = [] for order, count in enumerate(achievements): @@ -182,8 +212,9 @@ def map_status(self, map: dict[Status, str]): class VariantDto: - def __init__(self, variant: Variant, statuses: list[TaskStatusDto]): + def __init__(self, variant: Variant, statuses: list[TaskStatusDto], student: Student | None): self.id = int(variant.id) + self.email = student.email if student else '' self.statuses = statuses self.earned = sum(s.earned for s in statuses if s.earned > 1) self.solved = sum(s.status in [ diff --git a/webapp/managers.py b/webapp/managers.py index 49e056fe..335bd323 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -27,6 +27,7 @@ Status, Student, Task, + TaskBlock, TaskStatus, TypeOfTask, Variant @@ -190,6 +191,7 @@ def __init__( checks: MessageCheckRepository, achievements: AchievementManager, external: ExternalTaskManager, + students: StudentRepository, ): self.tasks = tasks self.groups = groups @@ -200,43 +202,56 @@ def __init__( self.checks = checks self.achievements = achievements self.external = external + self.students = students def get_group_statuses(self, group_id: int, hide_pending: bool) -> GroupDto: config = self.config.config group = self.groups.get_by_id(group_id) seed = self.seeds.get_final_seed(group.id) variants = self.variants.get_all() - tasks = self.tasks.get_all() + tasks = self.tasks.get_all_with_blocks() statuses = self.__get_statuses(group.id) + students = self.__get_students(group.id) dtos: list[VariantDto] = [] - checked = [Status.Checked, Status.CheckedFailed, Status.CheckedSubmitted] for var in variants: - dto = self.__get_variant(group, var, tasks, statuses, seed, config) - if hide_pending and any(status.status not in checked for status in dto.statuses): + dto = self.__get_variant(group, var, tasks, statuses, seed, config, students) + if hide_pending and any(status.status not in [ + Status.Checked, + Status.CheckedFailed, + Status.CheckedSubmitted, + Status.Verified, + Status.VerifiedFailed, + Status.VerifiedSubmitted, + ] for status in dto.statuses): continue dtos.append(dto) - return GroupDto(group, [TaskDto(task, seed) for task in tasks], dtos) + return GroupDto(group, [TaskDto(task, block, seed) for task, block in tasks], dtos) def __get_statuses(self, group: int) -> dict[tuple[int, int], TaskStatus]: statuses = self.statuses.get_by_group(group=group) return {(status.variant, status.task): status for status in statuses} + def __get_students(self, group: int) -> dict[int, Student]: + students = self.students.get_group_students(group) + return {student.variant: student for student in students if student.variant is not None} + def __get_variant( self, group: Group, variant: Variant, - tasks: list[Task], + tasks: list[tuple[Task, TaskBlock | None]], statuses: dict[tuple[int, int], TaskStatus], seed: FinalSeed | None, config: AppConfig, + students: dict[int, Student], ) -> VariantDto: dtos: list[TaskStatusDto] = [] - for task in tasks: + for task, block in tasks: status = statuses.get((variant.id, task.id)) e = self.external.get_external_task(group, variant, task, seed, config) achievements = self.__get_task_achievements(task.id) - dtos.append(TaskStatusDto(group, variant, TaskDto(task, seed), status, e, config, achievements)) - return VariantDto(variant, dtos) + dtos.append(TaskStatusDto(group, variant, TaskDto(task, block, seed), status, e, config, achievements)) + return VariantDto(variant, dtos, students.get(variant.id)) def __get_task_achievements(self, task: int) -> list[int]: achievements = self.achievements.read_achievements() @@ -250,8 +265,9 @@ def get_variant_statuses(self, gid: int, vid: int) -> VariantDto: group = self.groups.get_by_id(gid) seed = self.seeds.get_final_seed(group.id) statuses = self.__get_statuses(group.id) - tasks = self.tasks.get_all() - return self.__get_variant(group, variant, tasks, statuses, seed, config) + tasks = self.tasks.get_all_with_blocks() + students = self.__get_students(group.id) + return self.__get_variant(group, variant, tasks, statuses, seed, config, students) def get_task_status(self, gid: int, vid: int, tid: int) -> TaskStatusDto: status = self.statuses.get_task_status(tid, vid, gid) @@ -267,10 +283,10 @@ def __get_task_status_dto( config = self.config.config group = self.groups.get_by_id(gid) variant = self.variants.get_by_id(vid) - task = self.tasks.get_by_id(tid) + task, block = self.tasks.get_by_id_with_block(tid) seed = self.seeds.get_final_seed(gid) ext = self.external.get_external_task(group, variant, task, seed, self.config.config) - return TaskStatusDto(group, variant, TaskDto(task, seed), status, ext, config, achievements) + return TaskStatusDto(group, variant, TaskDto(task, block, seed), status, ext, config, achievements) def get_submissions_statuses_by_info(self, gid: int, vid: int, tid: int, skip: int, take: int): checks = self.checks.get_by_task(gid, vid, tid, skip, take, self.config.config.enable_registration) @@ -439,7 +455,8 @@ def __init__( self, groups: GroupRepository, messages: MessageRepository, - statuses: StatusManager, + status_manager: StatusManager, + statuses: TaskStatusRepository, variants: VariantRepository, tasks: TaskRepository, students: StudentRepository, @@ -447,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 @@ -457,15 +475,29 @@ 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]]: + blocks = self.tasks.get_blocks() + 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: + done = self.tasks.is_block_done(block.id, student.variant, student.group) + row.append(done * block.weight) + table.append(row) + return table def __create_messages_table(self, messages: list[Message], group_titles: dict[int, str]) -> list[list[str]]: rows = [["ID", "Время", "Группа", "Задача", "Вариант", "IP", "Отправитель", "Код"]] @@ -501,11 +533,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) @@ -531,7 +559,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/models.py b/webapp/models.py index 166dad2c..e36d7f13 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -47,6 +47,9 @@ class Status(enum.IntEnum): NotSubmitted = 4 CheckedSubmitted = 5 CheckedFailed = 6 + Verified = 7 + VerifiedSubmitted = 8 + VerifiedFailed = 9 class TypeOfTask(enum.IntEnum): @@ -76,6 +79,14 @@ class Task(Base): id = sa.Column("id", sa.Integer, primary_key=True, nullable=False, autoincrement=True) formulation = sa.Column("formulation", sa.String, nullable=True) type = sa.Column("type", IntEnum(TypeOfTask), nullable=False) + block = sa.Column("block", sa.Integer, sa.ForeignKey("task_blocks.id"), nullable=True) + + +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): @@ -94,6 +105,7 @@ class TaskStatus(Base): output = sa.Column("output", sa.String, nullable=True) status = sa.Column("status", IntEnum(Status), nullable=False) achievements = sa.Column("achievements", JsonArray, nullable=True) + reviewer = sa.Column("reviewer", sa.Integer, sa.ForeignKey("students.id"), nullable=True) class Message(Base): diff --git a/webapp/repositories.py b/webapp/repositories.py index c7447c28..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 +from sqlalchemy import desc, exists, func, literal, null, text from sqlalchemy.orm import Session from webapp.models import ( @@ -15,6 +15,7 @@ Status, Student, Task, + TaskBlock, TaskStatus, TypeOfTask, Variant, @@ -74,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): @@ -107,11 +108,55 @@ def get_all(self) -> list[Task]: tasks = session.query(Task).all() return tasks + def get_all_with_blocks(self) -> list[tuple[Task, TaskBlock | None]]: + with self.db.create_session() as session: + tasks = session.query(Task, TaskBlock) \ + .outerjoin(TaskBlock, Task.block == TaskBlock.id) \ + .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() + 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(Task, task_id) + task = session.get_one(Task, task_id) return task + def get_by_id_with_block(self, task_id: int) -> tuple[Task, TaskBlock | None]: + with self.db.create_session() as session: + pair = session.query(Task, TaskBlock) \ + .outerjoin(TaskBlock, Task.block == TaskBlock.id) \ + .filter(Task.id == task_id) \ + .one() + return pair + def create(self, id: int, type: TypeOfTask = TypeOfTask.Static): with self.db.create_session() as session: group = Task(id=id, type=type) @@ -178,7 +223,10 @@ def get_group_rating(self) -> list[tuple[Group, int]]: .query(TaskStatus.group, TaskStatus.variant) \ .filter((TaskStatus.status == Status.Checked) | (TaskStatus.status == Status.CheckedFailed) | - (TaskStatus.status == Status.CheckedSubmitted)) \ + (TaskStatus.status == Status.CheckedSubmitted) | + (TaskStatus.status == Status.Verified) | + (TaskStatus.status == Status.VerifiedFailed) | + (TaskStatus.status == Status.VerifiedSubmitted)) \ .group_by(TaskStatus.variant, TaskStatus.group) \ .having(func.count() >= tasks) \ .subquery() @@ -194,7 +242,10 @@ def get_rating(self) -> list[tuple[Group, TaskStatus]]: .join(TaskStatus, TaskStatus.group == Group.id) \ .filter((TaskStatus.status == Status.Checked) | (TaskStatus.status == Status.CheckedFailed) | - (TaskStatus.status == Status.CheckedSubmitted)) \ + (TaskStatus.status == Status.CheckedSubmitted) | + (TaskStatus.status == Status.Verified) | + (TaskStatus.status == Status.VerifiedFailed) | + (TaskStatus.status == Status.VerifiedSubmitted)) \ .all() return statuses @@ -228,37 +279,78 @@ def clear_achievements(self): .update(dict(achievements=None)) def check(self, task: int, variant: int, group: int, code: str, ok: bool, output: str, ip: str): - def status(): - existing = self.get_task_status(task, variant, group) - if existing and existing.status in [Status.Checked, Status.CheckedFailed, Status.CheckedSubmitted]: - return Status.Checked if ok else Status.CheckedFailed - return Status.Checked if ok else Status.Failed - - return self.create_or_update(task, variant, group, code, status(), output, ip) + ts = self.get_task_status(task, variant, group) + match (ok, ts and ts.status): + case (True, Status.Checked | Status.CheckedFailed | Status.CheckedSubmitted): + status = Status.Checked + case (True, Status.Verified | Status.VerifiedFailed | Status.VerifiedSubmitted): + status = Status.Verified + case (True, _): + status = Status.Checked + case (False, Status.Checked | Status.CheckedFailed | Status.CheckedSubmitted): + status = Status.CheckedFailed + case (False, Status.Verified | Status.VerifiedFailed | Status.VerifiedSubmitted): + status = Status.VerifiedFailed + case (False, _): + status = Status.Failed + return self.create_or_update(task, variant, group, code, status, output, ip, ts and ts.reviewer) def submit_task(self, task: int, variant: int, group: int, code: str, ip: str) -> TaskStatus: - checked = [Status.Checked, Status.CheckedFailed, Status.CheckedSubmitted] - existing = self.get_task_status(task, variant, group) - status = Status.CheckedSubmitted if existing and existing.status in checked else Status.Submitted - return self.create_or_update(task, variant, group, code, status, None, ip) - - def create_or_update(self, task: int, variant: int, group: int, code: str, status: int, output: str, ip: str): + ts = self.get_task_status(task, variant, group) + match ts and ts.status: + case Status.Checked | Status.CheckedFailed | Status.CheckedSubmitted: + status = Status.CheckedSubmitted + case Status.Verified | Status.VerifiedFailed | Status.VerifiedSubmitted: + status = Status.VerifiedSubmitted + case _: + status = Status.Submitted + return self.create_or_update(task, variant, group, code, status, None, ip, ts and ts.reviewer) + + def verify(self, task: int, variant: int, group: int, reviewer: int): + ts = self.get_task_status(task, variant, group) + match ts.status: + case Status.Checked: + status = Status.Verified + case Status.CheckedFailed: + status = Status.VerifiedFailed + case Status.CheckedSubmitted: + status = Status.VerifiedSubmitted + case _: + status = ts.status + return self.create_or_update(task, variant, group, ts.code, status, ts.output, ts.ip, reviewer) + + def unverify(self, task: int, variant: int, group: int, reviewer: int): + ts = self.get_task_status(task, variant, group) + match ts.status: + case Status.Verified: + status = Status.Checked + case Status.VerifiedFailed: + status = Status.CheckedFailed + case Status.VerifiedSubmitted: + status = Status.CheckedSubmitted + case _: + status = ts.status + return self.create_or_update(task, variant, group, ts.code, status, ts.output, ts.ip, reviewer) + + def create_or_update(self, task: int, variant: int, group: int, code: str, + status: int, output: str, ip: str, reviewer: int | None): now = datetime.datetime.now() with self.db.create_session() as session: query = session.query(TaskStatus).filter_by(task=task, variant=variant, group=group) if query.count(): - query.update(dict(code=code, status=status, output=output, ip=ip, time=now)) + query.update(dict(code=code, status=status, output=output, ip=ip, time=now, reviewer=reviewer)) updated: TaskStatus = query.one() return updated model = TaskStatus( - time=now, - task=task, - variant=variant, - group=group, code=code, status=status, output=output, - ip=ip) + ip=ip, + time=now, + reviewer=reviewer, + task=task, + variant=variant, + group=group) session.add(model) return model @@ -357,7 +449,7 @@ def get(self, message: int) -> MessageCheck: def checked(self) -> list[MessageCheck]: with self.db.create_session() as session: return session.query(MessageCheck) \ - .filter_by(status=Status.Checked) \ + .filter(MessageCheck.status.in_([Status.Checked, Status.Verified])) \ .all() def get_by_student(self, student: Student, skip: int, take: int) -> list[tuple[MessageCheck, Message]]: @@ -481,10 +573,15 @@ class StudentRepository: def __init__(self, db: DbContextManager): self.db = db - def get_all_by_group(self, group_id: int) -> list[Student] | None: + 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: - students = session.query(Student).filter(Student.group == group_id).all() - return students if students else None + return session.query(Student) \ + .filter(Student.group == group_id) \ + .all() def get_by_id(self, id: int) -> Student | None: with self.db.create_session() as session: @@ -548,6 +645,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() or 0 + 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/layout.jinja b/webapp/templates/layout.jinja index b261d44f..c47c32f4 100644 --- a/webapp/templates/layout.jinja +++ b/webapp/templates/layout.jinja @@ -11,67 +11,70 @@ {% block head %}{% endblock %} diff --git a/webapp/templates/student/group.jinja b/webapp/templates/student/group.jinja index f088be51..d14053f6 100644 --- a/webapp/templates/student/group.jinja +++ b/webapp/templates/student/group.jinja @@ -70,10 +70,14 @@