From fd24d8224a9c8f34877b3b6cf95ee1712ef9a9f8 Mon Sep 17 00:00:00 2001 From: Fundi1330 Date: Fri, 15 May 2026 15:53:18 +0300 Subject: [PATCH 01/19] refactor(models): tie submission to task instead of tournament --- backend/alembic/versions/127c3fae41db_.py | 276 --------------- backend/alembic/versions/128f25c448b0_.py | 276 --------------- backend/alembic/versions/40e0ecefc6eb_.py | 32 ++ backend/alembic/versions/bd3c7aaffa33_init.py | 335 ------------------ backend/alembic/versions/cfe129b6e3a3_.py | 276 --------------- ...6519cd8df_remove_is_global_column_from_.py | 28 -- backend/app/models/evaluation.py | 5 +- backend/app/models/submission.py | 23 +- backend/app/models/team.py | 2 +- backend/app/routes/submissions.py | 25 +- backend/app/routes/tasks.py | 15 +- backend/app/schemas/submission.py | 1 + backend/tests/test_models.py | 8 +- 13 files changed, 74 insertions(+), 1228 deletions(-) delete mode 100644 backend/alembic/versions/127c3fae41db_.py delete mode 100644 backend/alembic/versions/128f25c448b0_.py create mode 100644 backend/alembic/versions/40e0ecefc6eb_.py delete mode 100644 backend/alembic/versions/bd3c7aaffa33_init.py delete mode 100644 backend/alembic/versions/cfe129b6e3a3_.py delete mode 100644 backend/alembic/versions/d146519cd8df_remove_is_global_column_from_.py diff --git a/backend/alembic/versions/127c3fae41db_.py b/backend/alembic/versions/127c3fae41db_.py deleted file mode 100644 index 9dc9c84..0000000 --- a/backend/alembic/versions/127c3fae41db_.py +++ /dev/null @@ -1,276 +0,0 @@ -"""empty message - -Revision ID: 127c3fae41db -Revises: 128f25c448b0 -Create Date: 2026-05-08 15:59:19.355990 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '127c3fae41db' -down_revision: Union[str, Sequence[str], None] = '128f25c448b0' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('news_categories', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - with op.batch_alter_table('news_categories', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_news_categories_name'), ['name'], unique=False) - - op.create_table('role_request_info_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('roles', - sa.Column('description', sa.String(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('submission_url_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('task_requirement_categories', - sa.Column('main_id', sa.String(), nullable=True), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['main_id'], ['task_requirement_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('task_statuses', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('tournament_status_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - with op.batch_alter_table('tournament_status_options', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_tournament_status_options_name'), ['name'], unique=False) - - op.create_table('users', - sa.Column('firebase_uid', sa.String(), nullable=False), - sa.Column('full_name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('telegram', sa.String(), nullable=True), - sa.Column('github', sa.String(), nullable=True), - sa.Column('discord', sa.String(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('firebase_uid') - ) - op.create_table('news', - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('excerpt', sa.String(length=512), nullable=False), - sa.Column('body', sa.String(), nullable=False), - sa.Column('is_important', sa.Boolean(), nullable=False), - sa.Column('category_name', sa.String(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['category_name'], ['news_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('notifications', - sa.Column('body', sa.String(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('role_requests', - sa.Column('role_name', sa.String(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('task_requirement_options', - sa.Column('category_id', sa.String(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['category_id'], ['task_requirement_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('tasks', - sa.Column('title', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.Column('start_time', sa.DateTime(), nullable=False), - sa.Column('end_time', sa.DateTime(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('status_id', sa.String(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['status_id'], ['task_statuses.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], name='fk_task_tournament', ondelete='CASCADE', use_alter=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('tournaments', - sa.Column('title', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('start_date', sa.DateTime(), nullable=False), - sa.Column('reg_start', sa.DateTime(), nullable=False), - sa.Column('reg_end', sa.DateTime(), nullable=False), - sa.Column('min_people_in_team', sa.Integer(), nullable=False), - sa.Column('max_people_in_team', sa.Integer(), nullable=False), - sa.Column('max_teams', sa.Integer(), nullable=False), - sa.Column('status_id', sa.String(), nullable=False), - sa.Column('creator_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['status_id'], ['tournament_status_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('user_roles', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('user_id', 'role_name') - ) - op.create_table('role_request_info', - sa.Column('request_id', sa.Integer(), nullable=False), - sa.Column('option_name', sa.String(), nullable=False), - sa.Column('value', sa.String(length=4096), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['option_name'], ['role_request_info_options.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['request_id'], ['role_requests.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('task_requirements', - sa.Column('task_id', sa.Integer(), nullable=False), - sa.Column('requirement_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['requirement_id'], ['task_requirement_options.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('task_id', 'requirement_id') - ) - op.create_table('team_members', - sa.Column('full_name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('telegram', sa.String(), nullable=False), - sa.Column('educational_institution', sa.String(), nullable=True), - sa.Column('team_id', sa.Integer(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['team_id'], ['teams.id'], name='fk_teammember_team', ondelete='CASCADE', use_alter=True), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('team_id', 'email', name='uq_team_member_email'), - sa.UniqueConstraint('team_id', 'telegram', name='uq_team_member_telegram'), - sa.UniqueConstraint('tournament_id', 'email', name='uq_tournament_member_email'), - sa.UniqueConstraint('tournament_id', 'telegram', name='uq_tournament_member_telegram') - ) - op.create_table('tournament_juries', - sa.Column('jury_id', sa.Integer(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['jury_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('jury_id', 'tournament_id') - ) - op.create_table('teams', - sa.Column('name', sa.String(), nullable=False), - sa.Column('team_email', sa.String(), nullable=False), - sa.Column('contact_info', sa.String(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('captain_id', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['captain_id'], ['team_members.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('contact_info'), - sa.UniqueConstraint('contact_info', 'tournament_id'), - sa.UniqueConstraint('name'), - sa.UniqueConstraint('team_email') - ) - op.create_table('submissions', - sa.Column('team_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('team_id') - ) - op.create_table('evaluations', - sa.Column('submission_id', sa.Integer(), nullable=False), - sa.Column('jury_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['jury_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['submission_id'], ['submissions.team_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('submission_id', 'jury_id') - ) - op.create_table('submission_urls', - sa.Column('submission_id', sa.Integer(), nullable=False), - sa.Column('url_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['submission_id'], ['submissions.team_id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['url_id'], ['submission_url_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('submission_id', 'url_id') - ) - op.create_table('requirement_evaluations', - sa.Column('evaluation_id', sa.Integer(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['evaluation_id'], ['evaluations.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('evaluation_requirements', - sa.Column('requirement_evaluation_id', sa.Integer(), nullable=False), - sa.Column('requirement_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['requirement_evaluation_id'], ['requirement_evaluations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['requirement_id'], ['task_requirement_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('requirement_evaluation_id', 'requirement_id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('evaluation_requirements') - op.drop_table('requirement_evaluations') - op.drop_table('submission_urls') - op.drop_table('evaluations') - op.drop_table('submissions') - op.drop_table('teams') - op.drop_table('tournament_juries') - op.drop_table('team_members') - op.drop_table('task_requirements') - op.drop_table('role_request_info') - op.drop_table('user_roles') - op.drop_table('tournaments') - op.drop_table('tasks') - op.drop_table('task_requirement_options') - op.drop_table('role_requests') - op.drop_table('notifications') - op.drop_table('news') - op.drop_table('users') - with op.batch_alter_table('tournament_status_options', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_tournament_status_options_name')) - - op.drop_table('tournament_status_options') - op.drop_table('task_statuses') - op.drop_table('task_requirement_categories') - op.drop_table('submission_url_options') - op.drop_table('roles') - op.drop_table('role_request_info_options') - with op.batch_alter_table('news_categories', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_news_categories_name')) - - op.drop_table('news_categories') - # ### end Alembic commands ### diff --git a/backend/alembic/versions/128f25c448b0_.py b/backend/alembic/versions/128f25c448b0_.py deleted file mode 100644 index f66b3a4..0000000 --- a/backend/alembic/versions/128f25c448b0_.py +++ /dev/null @@ -1,276 +0,0 @@ -"""empty message - -Revision ID: 128f25c448b0 -Revises: dfc9ca7d5a57 -Create Date: 2026-05-07 21:42:31.511438 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '128f25c448b0' -down_revision: Union[str, Sequence[str], None] = 'dfc9ca7d5a57' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('news_categories', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - with op.batch_alter_table('news_categories', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_news_categories_name'), ['name'], unique=False) - - op.create_table('role_request_info_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('roles', - sa.Column('description', sa.String(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('submission_url_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('task_requirement_categories', - sa.Column('main_id', sa.String(), nullable=True), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['main_id'], ['task_requirement_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('task_statuses', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('tournament_status_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - with op.batch_alter_table('tournament_status_options', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_tournament_status_options_name'), ['name'], unique=False) - - op.create_table('users', - sa.Column('firebase_uid', sa.String(), nullable=False), - sa.Column('full_name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('telegram', sa.String(), nullable=True), - sa.Column('github', sa.String(), nullable=True), - sa.Column('discord', sa.String(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('firebase_uid') - ) - op.create_table('news', - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('excerpt', sa.String(length=512), nullable=False), - sa.Column('body', sa.String(), nullable=False), - sa.Column('is_important', sa.Boolean(), nullable=False), - sa.Column('category_name', sa.String(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['category_name'], ['news_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('notifications', - sa.Column('body', sa.String(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('role_requests', - sa.Column('role_name', sa.String(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('task_requirement_options', - sa.Column('category_id', sa.String(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['category_id'], ['task_requirement_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('tasks', - sa.Column('title', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.Column('start_time', sa.DateTime(), nullable=False), - sa.Column('end_time', sa.DateTime(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('status_id', sa.String(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['status_id'], ['task_statuses.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], name='fk_task_tournament', ondelete='CASCADE', use_alter=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('tournaments', - sa.Column('title', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('start_date', sa.DateTime(), nullable=False), - sa.Column('reg_start', sa.DateTime(), nullable=False), - sa.Column('reg_end', sa.DateTime(), nullable=False), - sa.Column('min_people_in_team', sa.Integer(), nullable=False), - sa.Column('max_people_in_team', sa.Integer(), nullable=False), - sa.Column('max_teams', sa.Integer(), nullable=False), - sa.Column('status_id', sa.String(), nullable=False), - sa.Column('creator_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['status_id'], ['tournament_status_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('user_roles', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('user_id', 'role_name') - ) - op.create_table('role_request_info', - sa.Column('request_id', sa.Integer(), nullable=False), - sa.Column('option_name', sa.String(), nullable=False), - sa.Column('value', sa.String(length=4096), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['option_name'], ['role_request_info_options.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['request_id'], ['role_requests.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('task_requirements', - sa.Column('task_id', sa.Integer(), nullable=False), - sa.Column('requirement_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['requirement_id'], ['task_requirement_options.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('task_id', 'requirement_id') - ) - op.create_table('team_members', - sa.Column('full_name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('telegram', sa.String(), nullable=False), - sa.Column('educational_institution', sa.String(), nullable=True), - sa.Column('team_id', sa.Integer(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['team_id'], ['teams.id'], name='fk_teammember_team', ondelete='CASCADE', use_alter=True), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('team_id', 'email', name='uq_team_member_email'), - sa.UniqueConstraint('team_id', 'telegram', name='uq_team_member_telegram'), - sa.UniqueConstraint('tournament_id', 'email', name='uq_tournament_member_email'), - sa.UniqueConstraint('tournament_id', 'telegram', name='uq_tournament_member_telegram') - ) - op.create_table('tournament_juries', - sa.Column('jury_id', sa.Integer(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['jury_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('jury_id', 'tournament_id') - ) - op.create_table('teams', - sa.Column('name', sa.String(), nullable=False), - sa.Column('team_email', sa.String(), nullable=False), - sa.Column('contact_info', sa.String(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('captain_id', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['captain_id'], ['team_members.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('contact_info'), - sa.UniqueConstraint('contact_info', 'tournament_id'), - sa.UniqueConstraint('name'), - sa.UniqueConstraint('team_email') - ) - op.create_table('submissions', - sa.Column('team_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('team_id') - ) - op.create_table('evaluations', - sa.Column('submission_id', sa.Integer(), nullable=False), - sa.Column('jury_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['jury_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['submission_id'], ['submissions.team_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('submission_id', 'jury_id') - ) - op.create_table('submission_urls', - sa.Column('submission_id', sa.Integer(), nullable=False), - sa.Column('url_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['submission_id'], ['submissions.team_id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['url_id'], ['submission_url_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('submission_id', 'url_id') - ) - op.create_table('requirement_evaluations', - sa.Column('evaluation_id', sa.Integer(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['evaluation_id'], ['evaluations.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('evaluation_requirements', - sa.Column('requirement_evaluation_id', sa.Integer(), nullable=False), - sa.Column('requirement_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['requirement_evaluation_id'], ['requirement_evaluations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['requirement_id'], ['task_requirement_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('requirement_evaluation_id', 'requirement_id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('evaluation_requirements') - op.drop_table('requirement_evaluations') - op.drop_table('submission_urls') - op.drop_table('evaluations') - op.drop_table('submissions') - op.drop_table('teams') - op.drop_table('tournament_juries') - op.drop_table('team_members') - op.drop_table('task_requirements') - op.drop_table('role_request_info') - op.drop_table('user_roles') - op.drop_table('tournaments') - op.drop_table('tasks') - op.drop_table('task_requirement_options') - op.drop_table('role_requests') - op.drop_table('notifications') - op.drop_table('news') - op.drop_table('users') - with op.batch_alter_table('tournament_status_options', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_tournament_status_options_name')) - - op.drop_table('tournament_status_options') - op.drop_table('task_statuses') - op.drop_table('task_requirement_categories') - op.drop_table('submission_url_options') - op.drop_table('roles') - op.drop_table('role_request_info_options') - with op.batch_alter_table('news_categories', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_news_categories_name')) - - op.drop_table('news_categories') - # ### end Alembic commands ### diff --git a/backend/alembic/versions/40e0ecefc6eb_.py b/backend/alembic/versions/40e0ecefc6eb_.py new file mode 100644 index 0000000..3de5dc0 --- /dev/null +++ b/backend/alembic/versions/40e0ecefc6eb_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 40e0ecefc6eb +Revises: +Create Date: 2026-05-15 15:50:43.251414 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '40e0ecefc6eb' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/bd3c7aaffa33_init.py b/backend/alembic/versions/bd3c7aaffa33_init.py deleted file mode 100644 index ec2bc11..0000000 --- a/backend/alembic/versions/bd3c7aaffa33_init.py +++ /dev/null @@ -1,335 +0,0 @@ -"""init - -Revision ID: bd3c7aaffa33 -Revises: -Create Date: 2026-05-13 20:21:46.540959 -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "bd3c7aaffa33" -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - - op.create_table( - "jury_assignment_statuses", - sa.Column("name", sa.String(), nullable=False), - sa.Column("display_name", sa.String(), nullable=False), - sa.PrimaryKeyConstraint("name"), - ) - - op.create_table( - "task_evaluation_criteria", - sa.Column("task_id", sa.Integer(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column("weight", sa.Integer(), nullable=False), - sa.Column("max_score", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint( - ["task_id"], - ["tasks.id"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - - op.create_table( - "jury_assignments", - sa.Column("task_id", sa.Integer(), nullable=False), - sa.Column("submission_id", sa.Integer(), nullable=False), - sa.Column("jury_id", sa.Integer(), nullable=False), - sa.Column("status_id", sa.String(), nullable=False), - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["jury_id"], - ["users.id"], - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["status_id"], - ["jury_assignment_statuses.name"], - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["submission_id"], - ["submissions.team_id"], - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["task_id"], - ["tasks.id"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("submission_id", "jury_id"), - ) - - op.create_table( - "criterion_scores", - sa.Column("evaluation_id", sa.Integer(), nullable=False), - sa.Column("criterion_id", sa.Integer(), nullable=False), - sa.Column("score", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint( - ["criterion_id"], - ["task_evaluation_criteria.id"], - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["evaluation_id"], - ["evaluations.id"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("evaluation_id", "criterion_id"), - ) - - op.drop_table("evaluation_requirements") - op.drop_table("requirement_evaluations") - - with op.batch_alter_table("evaluations", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "assignment_id", - sa.Integer(), - nullable=True, - ) - ) - - batch_op.add_column( - sa.Column( - "comment", - sa.String(), - nullable=True, - ) - ) - - batch_op.add_column( - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ) - ) - - batch_op.add_column( - sa.Column( - "updated_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ) - ) - - batch_op.create_foreign_key( - None, - "jury_assignments", - ["assignment_id"], - ["id"], - ondelete="CASCADE", - ) - - with op.batch_alter_table("submission_urls", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "value", - sa.String(), - nullable=True, - ) - ) - - with op.batch_alter_table("submissions", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "task_id", - sa.Integer(), - nullable=True, - ) - ) - - batch_op.create_foreign_key( - None, - "tasks", - ["task_id"], - ["id"], - ondelete="CASCADE", - ) - - with op.batch_alter_table("tasks", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "min_reviews_per_submission", - sa.Integer(), - nullable=False, - server_default="1", - ) - ) - - batch_op.add_column( - sa.Column( - "max_score", - sa.Integer(), - nullable=False, - server_default="100", - ) - ) - - batch_op.add_column( - sa.Column( - "is_leaderboard_visible", - sa.Boolean(), - nullable=False, - server_default=sa.text("false"), - ) - ) - - batch_op.create_foreign_key( - "fk_task_tournament", - "tournaments", - ["tournament_id"], - ["id"], - ondelete="CASCADE", - use_alter=True, - ) - - with op.batch_alter_table("team_members", schema=None) as batch_op: - batch_op.create_foreign_key( - "fk_teammember_team", - "teams", - ["team_id"], - ["id"], - ondelete="CASCADE", - use_alter=True, - ) - - -def downgrade() -> None: - """Downgrade schema.""" - - with op.batch_alter_table("team_members", schema=None) as batch_op: - batch_op.drop_constraint( - "fk_teammember_team", - type_="foreignkey", - ) - - with op.batch_alter_table("tasks", schema=None) as batch_op: - batch_op.drop_constraint( - "fk_task_tournament", - type_="foreignkey", - ) - - batch_op.drop_column("is_leaderboard_visible") - batch_op.drop_column("max_score") - batch_op.drop_column("min_reviews_per_submission") - - with op.batch_alter_table("submissions", schema=None) as batch_op: - batch_op.drop_constraint(None, type_="foreignkey") - batch_op.drop_column("task_id") - - with op.batch_alter_table("submission_urls", schema=None) as batch_op: - batch_op.drop_column("value") - - with op.batch_alter_table("evaluations", schema=None) as batch_op: - batch_op.drop_constraint(None, type_="foreignkey") - batch_op.drop_column("updated_at") - batch_op.drop_column("created_at") - batch_op.drop_column("comment") - batch_op.drop_column("assignment_id") - - op.create_table( - "requirement_evaluations", - sa.Column( - "evaluation_id", - sa.INTEGER(), - autoincrement=False, - nullable=False, - ), - sa.Column( - "score", - sa.INTEGER(), - autoincrement=False, - nullable=False, - ), - sa.Column( - "id", - sa.INTEGER(), - autoincrement=True, - nullable=False, - ), - sa.ForeignKeyConstraint( - ["evaluation_id"], - ["evaluations.id"], - name=op.f("requirement_evaluations_evaluation_id_fkey"), - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint( - "id", - name=op.f("requirement_evaluations_pkey"), - ), - ) - - op.create_table( - "evaluation_requirements", - sa.Column( - "requirement_evaluation_id", - sa.INTEGER(), - autoincrement=False, - nullable=False, - ), - sa.Column( - "requirement_id", - sa.VARCHAR(), - autoincrement=False, - nullable=False, - ), - sa.ForeignKeyConstraint( - ["requirement_evaluation_id"], - ["requirement_evaluations.id"], - name=op.f( - "evaluation_requirements_requirement_evaluation_id_fkey" - ), - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["requirement_id"], - ["task_requirement_options.name"], - name=op.f( - "evaluation_requirements_requirement_id_fkey" - ), - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint( - "requirement_evaluation_id", - "requirement_id", - name=op.f("evaluation_requirements_pkey"), - ), - ) - - op.drop_table("criterion_scores") - op.drop_table("jury_assignments") - op.drop_table("task_evaluation_criteria") - op.drop_table("jury_assignment_statuses") \ No newline at end of file diff --git a/backend/alembic/versions/cfe129b6e3a3_.py b/backend/alembic/versions/cfe129b6e3a3_.py deleted file mode 100644 index b347943..0000000 --- a/backend/alembic/versions/cfe129b6e3a3_.py +++ /dev/null @@ -1,276 +0,0 @@ -"""empty message - -Revision ID: cfe129b6e3a3 -Revises: 127c3fae41db -Create Date: 2026-05-11 14:37:01.268953 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'cfe129b6e3a3' -down_revision: Union[str, Sequence[str], None] = '127c3fae41db' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('news_categories', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - with op.batch_alter_table('news_categories', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_news_categories_name'), ['name'], unique=False) - - op.create_table('role_request_info_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('roles', - sa.Column('description', sa.String(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('submission_url_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('task_requirement_categories', - sa.Column('main_id', sa.String(), nullable=True), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['main_id'], ['task_requirement_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('task_statuses', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('tournament_status_options', - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name') - ) - with op.batch_alter_table('tournament_status_options', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_tournament_status_options_name'), ['name'], unique=False) - - op.create_table('users', - sa.Column('firebase_uid', sa.String(), nullable=False), - sa.Column('full_name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('telegram', sa.String(), nullable=True), - sa.Column('github', sa.String(), nullable=True), - sa.Column('discord', sa.String(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('firebase_uid') - ) - op.create_table('news', - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('excerpt', sa.String(length=512), nullable=False), - sa.Column('body', sa.String(), nullable=False), - sa.Column('is_important', sa.Boolean(), nullable=False), - sa.Column('category_name', sa.String(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['category_name'], ['news_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('notifications', - sa.Column('body', sa.String(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('role_requests', - sa.Column('role_name', sa.String(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('task_requirement_options', - sa.Column('category_id', sa.String(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('display_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['category_id'], ['task_requirement_categories.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('name') - ) - op.create_table('tasks', - sa.Column('title', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.Column('start_time', sa.DateTime(), nullable=False), - sa.Column('end_time', sa.DateTime(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('status_id', sa.String(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['status_id'], ['task_statuses.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], name='fk_task_tournament', ondelete='CASCADE', use_alter=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('tournaments', - sa.Column('title', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('start_date', sa.DateTime(), nullable=False), - sa.Column('reg_start', sa.DateTime(), nullable=False), - sa.Column('reg_end', sa.DateTime(), nullable=False), - sa.Column('min_people_in_team', sa.Integer(), nullable=False), - sa.Column('max_people_in_team', sa.Integer(), nullable=False), - sa.Column('max_teams', sa.Integer(), nullable=False), - sa.Column('status_id', sa.String(), nullable=False), - sa.Column('creator_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['status_id'], ['tournament_status_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('user_roles', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role_name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['role_name'], ['roles.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('user_id', 'role_name') - ) - op.create_table('role_request_info', - sa.Column('request_id', sa.Integer(), nullable=False), - sa.Column('option_name', sa.String(), nullable=False), - sa.Column('value', sa.String(length=4096), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['option_name'], ['role_request_info_options.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['request_id'], ['role_requests.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('task_requirements', - sa.Column('task_id', sa.Integer(), nullable=False), - sa.Column('requirement_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['requirement_id'], ['task_requirement_options.name'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('task_id', 'requirement_id') - ) - op.create_table('team_members', - sa.Column('full_name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('telegram', sa.String(), nullable=False), - sa.Column('educational_institution', sa.String(), nullable=True), - sa.Column('team_id', sa.Integer(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['team_id'], ['teams.id'], name='fk_teammember_team', ondelete='CASCADE', use_alter=True), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('team_id', 'email', name='uq_team_member_email'), - sa.UniqueConstraint('team_id', 'telegram', name='uq_team_member_telegram'), - sa.UniqueConstraint('tournament_id', 'email', name='uq_tournament_member_email'), - sa.UniqueConstraint('tournament_id', 'telegram', name='uq_tournament_member_telegram') - ) - op.create_table('tournament_juries', - sa.Column('jury_id', sa.Integer(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['jury_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('jury_id', 'tournament_id') - ) - op.create_table('teams', - sa.Column('name', sa.String(), nullable=False), - sa.Column('team_email', sa.String(), nullable=False), - sa.Column('contact_info', sa.String(), nullable=False), - sa.Column('tournament_id', sa.Integer(), nullable=False), - sa.Column('captain_id', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['captain_id'], ['team_members.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tournament_id'], ['tournaments.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('contact_info'), - sa.UniqueConstraint('contact_info', 'tournament_id'), - sa.UniqueConstraint('name'), - sa.UniqueConstraint('team_email') - ) - op.create_table('submissions', - sa.Column('team_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('team_id') - ) - op.create_table('evaluations', - sa.Column('submission_id', sa.Integer(), nullable=False), - sa.Column('jury_id', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['jury_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['submission_id'], ['submissions.team_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('submission_id', 'jury_id') - ) - op.create_table('submission_urls', - sa.Column('submission_id', sa.Integer(), nullable=False), - sa.Column('url_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['submission_id'], ['submissions.team_id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['url_id'], ['submission_url_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('submission_id', 'url_id') - ) - op.create_table('requirement_evaluations', - sa.Column('evaluation_id', sa.Integer(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['evaluation_id'], ['evaluations.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('evaluation_requirements', - sa.Column('requirement_evaluation_id', sa.Integer(), nullable=False), - sa.Column('requirement_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['requirement_evaluation_id'], ['requirement_evaluations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['requirement_id'], ['task_requirement_options.name'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('requirement_evaluation_id', 'requirement_id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('evaluation_requirements') - op.drop_table('requirement_evaluations') - op.drop_table('submission_urls') - op.drop_table('evaluations') - op.drop_table('submissions') - op.drop_table('teams') - op.drop_table('tournament_juries') - op.drop_table('team_members') - op.drop_table('task_requirements') - op.drop_table('role_request_info') - op.drop_table('user_roles') - op.drop_table('tournaments') - op.drop_table('tasks') - op.drop_table('task_requirement_options') - op.drop_table('role_requests') - op.drop_table('notifications') - op.drop_table('news') - op.drop_table('users') - with op.batch_alter_table('tournament_status_options', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_tournament_status_options_name')) - - op.drop_table('tournament_status_options') - op.drop_table('task_statuses') - op.drop_table('task_requirement_categories') - op.drop_table('submission_url_options') - op.drop_table('roles') - op.drop_table('role_request_info_options') - with op.batch_alter_table('news_categories', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_news_categories_name')) - - op.drop_table('news_categories') - # ### end Alembic commands ### diff --git a/backend/alembic/versions/d146519cd8df_remove_is_global_column_from_.py b/backend/alembic/versions/d146519cd8df_remove_is_global_column_from_.py deleted file mode 100644 index 8c7d57a..0000000 --- a/backend/alembic/versions/d146519cd8df_remove_is_global_column_from_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Remove is_global column from notifications table - -Revision ID: d146519cd8df -Revises: 6150229200ba -Create Date: 2026-05-03 20:18:01.936401 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'd146519cd8df' -down_revision: Union[str, Sequence[str], None] = '6150229200ba' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - pass - - -def downgrade() -> None: - """Downgrade schema.""" - pass diff --git a/backend/app/models/evaluation.py b/backend/app/models/evaluation.py index 0cd3aae..2b5158c 100644 --- a/backend/app/models/evaluation.py +++ b/backend/app/models/evaluation.py @@ -1,4 +1,3 @@ -from datetime import datetime from sqlalchemy import ForeignKey, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -25,7 +24,7 @@ class JuryAssignment(Base, PKMixin, DatetimeMixin): task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE")) submission_id: Mapped[int] = mapped_column( - ForeignKey("submissions.team_id", ondelete="CASCADE"), nullable=False + ForeignKey("submissions.id", ondelete="CASCADE"), nullable=False ) jury_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) status_id: Mapped[str] = mapped_column( @@ -64,7 +63,7 @@ class SubmissionEvaluation(Base, PKMixin, DatetimeMixin): ForeignKey("jury_assignments.id", ondelete="CASCADE"), nullable=False ) submission_id: Mapped[int] = mapped_column( - ForeignKey("submissions.team_id", ondelete="CASCADE"), nullable=False + ForeignKey("submissions.id", ondelete="CASCADE"), nullable=False ) jury_id: Mapped[int] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=False diff --git a/backend/app/models/submission.py b/backend/app/models/submission.py index ec78c89..56542bc 100644 --- a/backend/app/models/submission.py +++ b/backend/app/models/submission.py @@ -1,20 +1,22 @@ from .base import Base -from .mixin import OptionMixin +from .mixin import OptionMixin, PKMixin from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy import ForeignKey +from sqlalchemy import ForeignKey, UniqueConstraint -class Submission(Base): +class Submission(Base, PKMixin): __tablename__ = "submissions" + __table_args__ = (UniqueConstraint("team_id", "task_id"),) + team_id: Mapped[int] = mapped_column( - ForeignKey("teams.id", ondelete="CASCADE"), primary_key=True + ForeignKey("teams.id", ondelete="CASCADE"), nullable=False ) task_id: Mapped[int] = mapped_column( ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False ) team: Mapped["Team"] = relationship( - back_populates="submission", single_parent=True, lazy="selectin" + back_populates="submissions", lazy="selectin" ) task: Mapped["Task"] = relationship(lazy="selectin") urls: Mapped[list["SubmissionUrl"]] = relationship( @@ -28,16 +30,17 @@ class Submission(Base): ) def __repr__(self): - return f"" + return f"" -class SubmissionUrl(Base): +class SubmissionUrl(Base, PKMixin): __tablename__ = "submission_urls" + __table_args__ = (UniqueConstraint("submission_id", "url_id"),) submission_id: Mapped[int] = mapped_column( - ForeignKey("submissions.team_id", ondelete="CASCADE"), primary_key=True + ForeignKey("submissions.id", ondelete="CASCADE"), nullable=False ) url_id: Mapped[int] = mapped_column( - ForeignKey("submission_url_options.name", ondelete="CASCADE"), primary_key=True + ForeignKey("submission_url_options.name", ondelete="CASCADE"), nullable=False ) value: Mapped[str] @@ -45,7 +48,7 @@ class SubmissionUrl(Base): url: Mapped["SubmissionUrlOption"] = relationship(lazy="selectin") def __repr__(self): - return f"" + return f"" class SubmissionUrlOption(Base, OptionMixin): diff --git a/backend/app/models/team.py b/backend/app/models/team.py index 8a8a019..3ed554e 100644 --- a/backend/app/models/team.py +++ b/backend/app/models/team.py @@ -29,7 +29,7 @@ class Team(Base, PKMixin): "TeamMember", foreign_keys="Team.captain_id", post_update=True ) - submission: Mapped["Submission"] = relationship( + submissions: Mapped[list["Submission"]] = relationship( back_populates="team", cascade="all, delete-orphan" ) diff --git a/backend/app/routes/submissions.py b/backend/app/routes/submissions.py index 37e1db5..9c843ea 100644 --- a/backend/app/routes/submissions.py +++ b/backend/app/routes/submissions.py @@ -1,20 +1,19 @@ from fastapi import status, HTTPException from fastapi.routing import APIRouter -from sqlalchemy import select, update +from sqlalchemy import select from sqlalchemy.orm import selectinload -from sqlalchemy.exc import IntegrityError from app.dependencies import SessionDep -from app.models import Submission, SubmissionUrl, SubmissionUrlOption, Team -from app.schemas import SubmissionModel, SubmissionUrlOptionModel, SubmissionUrlModel +from app.models import Submission, SubmissionUrl +from app.schemas import SubmissionModel router = APIRouter(prefix="/submissions", tags=["submissions"]) -async def get_submission(team_id: int, session: SessionDep) -> Submission: +async def get_submission(submission_id: int, session: SessionDep) -> Submission: statement = ( select(Submission) - .where(Submission.team_id == team_id) + .where(Submission.id == submission_id) .options(selectinload(Submission.urls).selectinload(SubmissionUrl.url)) ) result = await session.execute(statement) @@ -35,9 +34,9 @@ async def submissions(session: SessionDep): return users.scalars().all() -@router.get("/{team_id}/", response_model=SubmissionModel, status_code=status.HTTP_200_OK) -async def submission(team_id: int, session: SessionDep): - return await get_submission(team_id, session) +@router.get("/{submission_id}/", response_model=SubmissionModel, status_code=status.HTTP_200_OK) +async def submission(submission_id: int, session: SessionDep): + return await get_submission(submission_id, session) # Not ready yet @@ -69,15 +68,15 @@ async def submission(team_id: int, session: SessionDep): # @router.patch( -# "/{team_id}/", response_model=SubmissionModel, status_code=status.HTTP_200_OK +# "/{submission_id}/", response_model=SubmissionModel, status_code=status.HTTP_200_OK # ) # async def update_submission(): # pass -@router.delete("/{team_id}/", status_code=status.HTTP_204_NO_CONTENT) -async def delete_submission(team_id: int, session: SessionDep): - submission = await get_submission(team_id, session) +@router.delete("/{submission_id}/", status_code=status.HTTP_204_NO_CONTENT) +async def delete_submission(submission_id: int, session: SessionDep): + submission = await get_submission(submission_id, session) await session.delete(submission) await session.commit() diff --git a/backend/app/routes/tasks.py b/backend/app/routes/tasks.py index 8c1427c..3a43531 100644 --- a/backend/app/routes/tasks.py +++ b/backend/app/routes/tasks.py @@ -1,11 +1,10 @@ from fastapi import status, HTTPException from fastapi.routing import APIRouter from sqlalchemy import select -from sqlalchemy.orm import selectinload from app.config import settings -from app.dependencies import SessionDep, TournamentOwnerDep, TaskOwnerDep, current_user_dependency -from app.models import Task, TaskEvaluationCriterion, Submission, SubmissionUrl, Team, Tournament +from app.dependencies import SessionDep, TaskOwnerDep +from app.models import Task, TaskEvaluationCriterion, Submission, SubmissionUrl from app.schemas import TaskCreate, TaskUpdate, TaskPublic, SubmissionCreate, SubmissionModel from app.utils import TaskStatus, update_tasks_status, get_requirements, get_task, get_team @@ -162,12 +161,16 @@ async def create_submission( await get_team(submission_data.team_id, tournament_id, session) new_submission = Submission(team_id=submission_data.team_id, task_id=task_id) + session.add(new_submission) + await session.flush() - await session.refresh(new_submission, ["urls"]) for item in submission_data.urls: - new_submission.urls.append(SubmissionUrl(url_id=item.url_id, value=item.value)) + session.add(SubmissionUrl( + submission_id=new_submission.id, + url_id=item.url_id, + value=item.value, + )) - session.add(new_submission) await session.commit() await session.refresh(new_submission, ["urls", "team"]) diff --git a/backend/app/schemas/submission.py b/backend/app/schemas/submission.py index 68e25d8..53856d4 100644 --- a/backend/app/schemas/submission.py +++ b/backend/app/schemas/submission.py @@ -29,6 +29,7 @@ class SubmissionCreate(BaseModel): class SubmissionModel(BaseModel): model_config = ConfigDict(from_attributes=True) + id: int = Field(...) team_id: int = Field(...) task_id: int = Field(...) team: TeamPublic | None = None diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index 09630db..3f91419 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -237,8 +237,8 @@ async def test_tournament_cascade_delete_children(db_session, create): "task": task.id, "team": team.id, "member": member.id, - "submission": submission.team_id, - "url": (url.submission_id, url.url_id), + "submission": submission.id, + "url": url.id, "evaluation": evaluation.id, "criterion_score": criterion_score.id, } @@ -505,8 +505,8 @@ async def test_submission_urls_belong_to_submission(create): url1 = await create(SubmissionUrlFactory, submission=submission) url2 = await create(SubmissionUrlFactory, submission=submission) - assert url1.submission_id == submission.team_id - assert url2.submission_id == submission.team_id + assert url1.submission_id == submission.id + assert url2.submission_id == submission.id async def test_submission_url_has_submission(db_session, create): From 06d046bf641c8d19676e0e2c70085d4ea513458f Mon Sep 17 00:00:00 2001 From: AnnPoshtak Date: Fri, 15 May 2026 16:25:07 +0300 Subject: [PATCH 02/19] feat: add darl team and i18 to all modals --- frontend/src/components/ui/CustomSelect.tsx | 22 +- frontend/src/components/ui/DateTimePicker.tsx | 66 +++--- frontend/src/components/ui/Modal.tsx | 202 ++++++++++++++++++ frontend/src/components/ui/index.ts | 1 + frontend/src/i18n/config.ts | 5 + frontend/src/locales/en/modals.json | 119 +++++++++++ frontend/src/locales/uk/modals.json | 119 +++++++++++ .../JuryPanel/components/EvaluationsModal.tsx | 2 +- .../OrganizerPanel/CreateTournamentModal.tsx | 108 +++++----- .../OrganizerPanel/EditTournamentModal.tsx | 80 +++---- .../components/JurySelectionModal.tsx | 143 ++++++------- .../components/TaskManagementModal.test.tsx | 77 +++---- .../components/TaskManagementModal.tsx | 58 ++--- .../components/TournamentInfoModal.tsx | 150 ++++++------- .../TournamentInfoModal.test.tsx.snap | 128 +++++------ .../src/pages/Profile/EditProfileModal.tsx | 2 +- .../components/LookingForTeamModal.test.tsx | 20 +- .../components/LookingForTeamModal.tsx | 92 ++++---- frontend/src/setupTests.ts | 3 + 19 files changed, 933 insertions(+), 464 deletions(-) create mode 100644 frontend/src/components/ui/Modal.tsx create mode 100644 frontend/src/locales/en/modals.json create mode 100644 frontend/src/locales/uk/modals.json diff --git a/frontend/src/components/ui/CustomSelect.tsx b/frontend/src/components/ui/CustomSelect.tsx index d14c8b1..76910c6 100644 --- a/frontend/src/components/ui/CustomSelect.tsx +++ b/frontend/src/components/ui/CustomSelect.tsx @@ -1,4 +1,5 @@ import React, { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; import { Listbox, Transition } from '@headlessui/react'; import { ChevronDown, Check, type LucideIcon } from 'lucide-react'; import { useFloating, offset, flip, shift, autoUpdate, size } from '@floating-ui/react'; @@ -23,6 +24,7 @@ interface CustomSelectProps { } const CustomSelect: React.FC = ({ options, value, onChange, label, icon: Icon, disabled }) => { + const { t } = useTranslation("modals"); const { refs, floatingStyles } = useFloating({ placement: 'bottom-start', whileElementsMounted: autoUpdate, @@ -43,7 +45,7 @@ const CustomSelect: React.FC = ({ options, value, onChange, l return (
- + {label} @@ -51,13 +53,13 @@ const CustomSelect: React.FC = ({ options, value, onChange, l
- {Icon && } - {value?.label || 'Виберіть варіант'} + {Icon && } + {value?.label || t("custom_select.placeholder")}
- +
= ({ options, value, onChange, l {options.map((group) => (
-
+
{group.category}
{group.items.map((option) => ( @@ -82,8 +84,8 @@ const CustomSelect: React.FC = ({ options, value, onChange, l value={option} className={({ active, selected }) => ` relative cursor-pointer select-none py-2 px-3 rounded-xl transition-all mb-0.5 last:mb-0 - ${active ? 'bg-slate-50 text-[#6D72F1]' : 'text-slate-600'} - ${selected ? 'bg-[#6D72F1]/5 text-[#6D72F1]' : ''} + ${active ? 'bg-bg-body text-primary' : 'text-text-muted'} + ${selected ? 'bg-primary/5 text-primary' : ''} `} > {({ selected }) => ( @@ -92,7 +94,7 @@ const CustomSelect: React.FC = ({ options, value, onChange, l {option.label} {selected && ( - + )}
)} diff --git a/frontend/src/components/ui/DateTimePicker.tsx b/frontend/src/components/ui/DateTimePicker.tsx index ee6fbd7..b8ed85c 100644 --- a/frontend/src/components/ui/DateTimePicker.tsx +++ b/frontend/src/components/ui/DateTimePicker.tsx @@ -1,4 +1,5 @@ import React, { useState, Fragment } from "react"; +import { useTranslation } from "react-i18next"; import { format, addMonths, @@ -14,7 +15,7 @@ import { setMinutes, parseISO, } from "date-fns"; -import { uk } from "date-fns/locale"; +import { uk, enGB } from "date-fns/locale"; import { Popover, Transition } from "@headlessui/react"; import { Calendar as CalendarIcon, @@ -43,6 +44,17 @@ const DateTimePicker: React.FC = ({ onChange, label, }) => { + const { t, i18n } = useTranslation("modals"); + const dateLocale = i18n.language === "en" ? enGB : uk; + const weekdayLabels = [ + t("date_time_picker.weekdays.mon"), + t("date_time_picker.weekdays.tue"), + t("date_time_picker.weekdays.wed"), + t("date_time_picker.weekdays.thu"), + t("date_time_picker.weekdays.fri"), + t("date_time_picker.weekdays.sat"), + t("date_time_picker.weekdays.sun"), + ]; const dateValue = value ? parseISO(value) : new Date(); const [currentMonth, setCurrentMonth] = useState(new Date(dateValue)); @@ -107,7 +119,7 @@ const DateTimePicker: React.FC = ({ type="button" onClick={() => handleDateClick(cloneDay)} className={`w-8 h-8 flex items-center justify-center rounded-xl text-[11px] font-bold transition-all - ${!isCurrentMonth ? "text-slate-200" : isSelected ? "bg-[#6D72F1] text-white shadow-lg shadow-[#6D72F1]/30" : "text-slate-600 hover:bg-slate-50 hover:text-[#6D72F1]"} + ${!isCurrentMonth ? "text-text-muted/30" : isSelected ? "bg-primary text-white shadow-lg shadow-primary/30" : "text-text-muted hover:bg-bg-body hover:text-primary"} `} > {format(day, "d")} @@ -126,23 +138,23 @@ const DateTimePicker: React.FC = ({ return (
- + {label}
- + - {value ? format(dateValue, "dd.MM.yyyy") : "Виберіть дату"} + {value ? format(dateValue, "dd.MM.yyyy") : t("date_time_picker.pick_date")}
-
- +
+ {format(dateValue, "HH:mm")}
@@ -159,35 +171,35 @@ const DateTimePicker: React.FC = ({ -
+
- - {format(currentMonth, "LLLL yyyy", { locale: uk })} + + {format(currentMonth, "LLLL yyyy", { locale: dateLocale })}
- {["Пн", "Вв", "Ср", "Чт", "Пт", "Сб", "Нд"].map((d) => ( + {weekdayLabels.map((d) => (
{d}
@@ -196,9 +208,9 @@ const DateTimePicker: React.FC = ({ {renderCalendar()}
-
- - Час +
+ + {t("date_time_picker.time")}
@@ -206,39 +218,39 @@ const DateTimePicker: React.FC = ({ -
+
{format(dateValue, "HH")}
- : + :
-
+
{format(dateValue, "mm")}
@@ -255,7 +267,7 @@ const DateTimePicker: React.FC = ({ setMinutes(setHours(new Date(dateValue), h), 0), ) } - className="px-2 py-1 bg-white border border-slate-200 rounded-lg text-[9px] font-black text-slate-500 hover:border-[#6D72F1] hover:text-[#6D72F1] transition-all" + className="px-2 py-1 bg-bg-card border border-border rounded-lg text-[9px] font-black text-text-muted hover:border-primary hover:text-primary transition-all" > {h}:00 diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx new file mode 100644 index 0000000..56bd18e --- /dev/null +++ b/frontend/src/components/ui/Modal.tsx @@ -0,0 +1,202 @@ +import { useEffect, type ReactNode, type MouseEvent } from "react"; +import { createPortal } from "react-dom"; +import { useTranslation } from "react-i18next"; +import { X } from "lucide-react"; +import { cn } from "@/utils/cn"; + +/** Shared modal class tokens for modals with custom layout */ +export const modalClass = { + overlay: + "fixed inset-0 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm dark:bg-black/70 transition-colors", + backdrop: "absolute inset-0", + panel: + "relative w-full bg-bg-card border border-border shadow-2xl overflow-hidden flex flex-col transition-colors duration-300", + panelLg: "rounded-[2.5rem]", + panelMd: "rounded-2xl", + body: "flex-1 overflow-y-auto bg-bg-body p-6 md:p-8", + bodyInset: "flex-1 overflow-y-auto bg-bg-body/80 dark:bg-bg-body/50 p-6", + footer: "p-6 bg-bg-card border-t border-border flex gap-4 shrink-0", + header: "flex items-center justify-between p-6 border-b border-border shrink-0 bg-bg-card", + headerTitle: "text-lg font-bold text-text-main", + headerSubtitle: "text-xs text-text-muted mt-0.5", + brandedHeader: + "bg-gradient-to-r from-primary via-primary to-indigo-500 p-6 md:p-8 text-white shrink-0 relative", + brandedHeaderTitle: "text-xl md:text-2xl font-black uppercase tracking-tight", + label: "text-[10px] font-black text-text-muted uppercase tracking-widest", + input: + "w-full px-5 py-4 bg-bg-card border-2 border-border rounded-2xl outline-none font-bold text-text-main placeholder:text-text-muted/60 focus:border-primary transition-all", + textarea: + "w-full px-5 py-4 bg-bg-card border-2 border-border rounded-2xl outline-none font-medium text-text-main resize-none focus:border-primary transition-all", + card: "bg-bg-card p-5 rounded-2xl border border-border shadow-sm", + closeBtn: + "p-2 hover:bg-bg-body rounded-lg transition-colors text-text-muted hover:text-text-main", + closeBtnBranded: + "p-2 hover:bg-white/10 rounded-full transition-all text-white", + iconBox: + "w-20 h-20 bg-primary/10 text-primary rounded-[2rem] flex items-center justify-center mx-auto mb-6 shadow-inner", + primaryAction: + "py-4 bg-primary text-white rounded-2xl font-bold uppercase text-[10px] tracking-[0.2em] shadow-lg shadow-primary/20 hover:brightness-110 active:scale-[0.98] disabled:bg-border disabled:text-text-muted disabled:shadow-none transition-all", + secondaryAction: + "py-4 font-bold text-text-muted uppercase text-[10px] tracking-widest hover:text-text-main transition-colors", + ghostAction: + "w-full py-4 bg-bg-body text-text-muted rounded-2xl font-bold uppercase text-[11px] tracking-[0.15em] transition-all hover:bg-border hover:text-text-main active:scale-[0.98]", +} as const; + +interface ModalProps { + isOpen: boolean; + onClose?: () => void; + children: ReactNode; + zIndex?: number; + usePortal?: boolean; + lockScroll?: boolean; + className?: string; + closeOnBackdrop?: boolean; +} + +export function Modal({ + isOpen, + onClose, + children, + zIndex = 200, + usePortal = true, + lockScroll = true, + className, + closeOnBackdrop = true, +}: ModalProps) { + useEffect(() => { + if (!isOpen || !lockScroll) return; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = "unset"; + }; + }, [isOpen, lockScroll]); + + if (!isOpen) return null; + + const handleBackdrop = (e: MouseEvent) => { + if (closeOnBackdrop && e.target === e.currentTarget) onClose?.(); + }; + + const el = ( +
+ {children} +
+ ); + + return usePortal ? createPortal(el, document.body) : el; +} + +interface ModalPanelProps { + children: ReactNode; + className?: string; + size?: "sm" | "md" | "lg" | "xl" | "full"; + rounded?: "lg" | "md"; + onClick?: (e: MouseEvent) => void; +} + +const sizeClass = { + sm: "max-w-md", + md: "max-w-2xl", + lg: "max-w-4xl", + xl: "max-w-7xl", + full: "max-w-[96vw]", +}; + +export function ModalPanel({ + children, + className, + size = "md", + rounded = "lg", + onClick, +}: ModalPanelProps) { + return ( +
+ {children} +
+ ); +} + +export function ModalBackdrop({ onClose }: { onClose?: () => void }) { + return
; +} + +interface ModalBrandedHeaderProps { + title: ReactNode; + onClose?: () => void; + children?: ReactNode; + className?: string; +} + +export function ModalBrandedHeader({ + title, + onClose, + children, + className, +}: ModalBrandedHeaderProps) { + const { t } = useTranslation("modals"); + return ( +
+
+

{title}

+ {onClose && ( + + )} +
+ {children} +
+ ); +} + +export function ModalBody({ + children, + className, + inset = false, +}: { + children: ReactNode; + className?: string; + inset?: boolean; +}) { + return
{children}
; +} + +export function ModalFooter({ children, className }: { children: ReactNode; className?: string }) { + return
{children}
; +} + +export function ModalSimpleHeader({ + title, + subtitle, + onClose, +}: { + title: string; + subtitle?: ReactNode; + onClose: () => void; +}) { + const { t } = useTranslation("modals"); + return ( +
+
+

{title}

+ {subtitle ?

{subtitle}

: null} +
+ +
+ ); +} diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index d950c22..9562190 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -1,4 +1,5 @@ export * from "./Button"; +export * from "./Modal"; export * from "./BrandingPanel"; export * from "./Ticket3D"; // export * from "./Card"; diff --git a/frontend/src/i18n/config.ts b/frontend/src/i18n/config.ts index b5117f0..7a56998 100644 --- a/frontend/src/i18n/config.ts +++ b/frontend/src/i18n/config.ts @@ -16,6 +16,7 @@ import ukProfile from "../locales/uk/profile.json"; import ukRoleRequest from "../locales/uk/roleRequest.json"; import ukJury from "../locales/uk/jury.json"; import ukNews from "../locales/uk/news.json"; +import ukModals from "../locales/uk/modals.json"; import enCommon from "../locales/en/common.json"; import enAuth from "../locales/en/auth.json"; @@ -30,6 +31,7 @@ import enRules from "../locales/en/rules.json"; import enProfile from "../locales/en/profile.json"; import enRoleRequest from "../locales/en/roleRequest.json"; import enJury from "../locales/en/jury.json"; +import enModals from "../locales/en/modals.json"; const resources = { uk: { @@ -47,6 +49,7 @@ const resources = { roleRequest: ukRoleRequest, jury: ukJury, news: ukNews, + modals: ukModals, }, en: { common: enCommon, @@ -62,6 +65,7 @@ const resources = { profile: enProfile, roleRequest: enRoleRequest, jury: enJury, + modals: enModals, }, }; @@ -86,6 +90,7 @@ i18n "profile", "roleRequest", "jury", + "modals", ], defaultNS: "common", debug: import.meta.env.DEV, diff --git a/frontend/src/locales/en/modals.json b/frontend/src/locales/en/modals.json new file mode 100644 index 0000000..f1df62c --- /dev/null +++ b/frontend/src/locales/en/modals.json @@ -0,0 +1,119 @@ +{ + "common": { + "cancel": "Cancel", + "back": "Back", + "next": "Next", + "confirm": "Confirm", + "close": "Close", + "close_aria": "Close", + "tournament_label": "Tournament:", + "selected": "Selected", + "experts": "experts", + "no_description": "No description provided.", + "dash": "—", + "people_short": "people", + "user_initial": "U" + }, + "fields": { + "title": "Title", + "description": "Description", + "name": "Name", + "start": "Start", + "deadline": "Deadline", + "registration": "Registration", + "opening": "Opening", + "closing": "Closing", + "start_date_time": "Start date & time", + "start_date": "Start date" + }, + "date_time_picker": { + "pick_date": "Select date", + "time": "Time", + "weekdays": { + "mon": "Mon", + "tue": "Tue", + "wed": "Wed", + "thu": "Thu", + "fri": "Fri", + "sat": "Sat", + "sun": "Sun" + } + }, + "custom_select": { + "placeholder": "Select an option" + }, + "looking_for_team": { + "title": "Find a team", + "heading": "Looking for a team?", + "description": "Contact us — we'll help you find like-minded people and join a tournament today!", + "contact_cta": "Go to contact page" + }, + "jury_selection": { + "title": "Select jury", + "empty": "No users found" + }, + "task_management": { + "create_title": "New task", + "edit_title": "Update task", + "tech_stack": "Tech stack", + "add_requirement": "Add requirement", + "criteria": "Scoring criteria", + "criterion_placeholder": "Add criterion…", + "save": "Save task", + "validation": { + "required": "Required field" + } + }, + "tournament_info": { + "default_status": "Tournament", + "about": "About the event", + "tasks": "Stages (Tasks)", + "active": "Active", + "no_tasks": "No tasks yet", + "teams": "Participating teams", + "teams_total": "Total: {{count}}", + "no_teams": "No teams have joined yet", + "timing": "Timing", + "registration_period": "Registration", + "event_period": "Event", + "configuration": "Configuration", + "max_teams": "Max teams", + "team_size": "Team size", + "jury": "Jury", + "no_jury": "No jury assigned", + "close": "Got it, close", + "contact_telegram": "TG:", + "contact_email": "Email:", + "contact_github": "GH:" + }, + "create_tournament": { + "title": "New tournament", + "success_title": "Ready to launch", + "success_description": "Tournament created successfully. Add tasks and configure scoring in the organizer panel.", + "success_cta": "Go to dashboard", + "title_placeholder": "e.g. Winter Coding Cup 2024", + "title_tooltip": "Choose a clear name participants will see", + "description_tooltip": "Briefly describe rules and winning conditions", + "auto_start_hint": "The tournament will start automatically", + "min_team": "Min per team", + "max_team": "Max per team", + "team_limit": "Team limit", + "add_judges": "Add judges", + "judges_selected": "Selected: {{count}}", + "creating": "Creating…", + "create": "Create tournament" + }, + "edit_tournament": { + "title": "Edit tournament", + "title_label": "Tournament name", + "title_tooltip": "Changing the name affects how it appears in all lists", + "description_tooltip": "You can update rules or tournament details", + "max_teams": "Teams (max)", + "min_players": "Players (min)", + "max_players": "Players (max)", + "jury_team": "Jury team", + "jury_count": "Experts assigned: {{count}}", + "saving": "Saving…", + "save": "Save changes" + } +} diff --git a/frontend/src/locales/uk/modals.json b/frontend/src/locales/uk/modals.json new file mode 100644 index 0000000..d4f46f0 --- /dev/null +++ b/frontend/src/locales/uk/modals.json @@ -0,0 +1,119 @@ +{ + "common": { + "cancel": "Скасувати", + "back": "Назад", + "next": "Далі", + "confirm": "Підтвердити", + "close": "Закрити", + "close_aria": "Закрити", + "tournament_label": "Турнір:", + "selected": "Обрано", + "experts": "експертів", + "no_description": "Опис не вказано.", + "dash": "—", + "people_short": "чол.", + "user_initial": "U" + }, + "fields": { + "title": "Назва", + "description": "Опис", + "name": "Назва", + "start": "Старт", + "deadline": "Дедлайн", + "registration": "Реєстрація", + "opening": "Відкриття", + "closing": "Закриття", + "start_date_time": "Дата та час старту", + "start_date": "Дата старту" + }, + "date_time_picker": { + "pick_date": "Виберіть дату", + "time": "Час", + "weekdays": { + "mon": "Пн", + "tue": "Вв", + "wed": "Ср", + "thu": "Чт", + "fri": "Пт", + "sat": "Сб", + "sun": "Нд" + } + }, + "custom_select": { + "placeholder": "Виберіть варіант" + }, + "looking_for_team": { + "title": "Пошук команди", + "heading": "Шукаєш команду?", + "description": "Зв'яжись з нами — ми допоможемо тобі знайти однодумців та приєднатися до турніру вже сьогодні!", + "contact_cta": "Перейти на сторінку контактів" + }, + "jury_selection": { + "title": "Вибір журі", + "empty": "Користувачів не знайдено" + }, + "task_management": { + "create_title": "Нове завдання", + "edit_title": "Оновити таск", + "tech_stack": "Стек технологій", + "add_requirement": "Додати вимогу", + "criteria": "Критерії оцінювання", + "criterion_placeholder": "Додати критерій...", + "save": "Зберегти завдання", + "validation": { + "required": "Обов'язкове поле" + } + }, + "tournament_info": { + "default_status": "Турнір", + "about": "Про івент", + "tasks": "Етапи (Таски)", + "active": "Активне", + "no_tasks": "Таски відсутні", + "teams": "Команди-учасники", + "teams_total": "Всього: {{count}}", + "no_teams": "Команди ще не приєдналися", + "timing": "Таймінг", + "registration_period": "Реєстрація", + "event_period": "Проведення", + "configuration": "Конфігурація", + "max_teams": "Макс команд", + "team_size": "В команді", + "jury": "Журі", + "no_jury": "Журі не призначено", + "close": "Зрозуміло, закрити", + "contact_telegram": "TG:", + "contact_email": "Email:", + "contact_github": "GH:" + }, + "create_tournament": { + "title": "Новий турнір", + "success_title": "Готово до запуску", + "success_description": "Турнір створено успішно. Тепер додайте завдання та налаштуйте бали у панелі керування.", + "success_cta": "До керування", + "title_placeholder": "Наприклад: Winter Coding Cup 2024", + "title_tooltip": "Вкажіть зрозумілу назву, яку бачитимуть учасники", + "description_tooltip": "Коротко про правила та умови перемоги", + "auto_start_hint": "Турнір розпочнеться автоматично", + "min_team": "Мін. у команді", + "max_team": "Макс. у команді", + "team_limit": "Ліміт команд", + "add_judges": "Додати суддів", + "judges_selected": "Зараз обрано: {{count}}", + "creating": "Створення...", + "create": "Створити турнір" + }, + "edit_tournament": { + "title": "Редагування", + "title_label": "Назва турніру", + "title_tooltip": "Зміна назви вплине на відображення в усіх списках", + "description_tooltip": "Ви можете оновити правила або деталі турніру", + "max_teams": "Команд (Макс)", + "min_players": "Гравців (Мін)", + "max_players": "Гравців (Макс)", + "jury_team": "Суддівська команда", + "jury_count": "Залучено фахівців: {{count}}", + "saving": "Збереження...", + "save": "Зберегти зміни" + } +} diff --git a/frontend/src/pages/JuryPanel/components/EvaluationsModal.tsx b/frontend/src/pages/JuryPanel/components/EvaluationsModal.tsx index 0018703..ddf7d7a 100644 --- a/frontend/src/pages/JuryPanel/components/EvaluationsModal.tsx +++ b/frontend/src/pages/JuryPanel/components/EvaluationsModal.tsx @@ -51,7 +51,7 @@ function EvaluationsModalInner({ assignment, onClose }: { assignment: JuryAssign return (
(
-
?
-
+
?
+
{text}
-
+
); export const CreateTournamentModal = ({ isOpen, onClose, onCreate }: any) => { + const { t } = useTranslation("modals"); const initialState: FormData = { title: "", description: "", @@ -120,20 +122,20 @@ export const CreateTournamentModal = ({ isOpen, onClose, onCreate }: any) => { if (isSuccess) { return createPortal( -
-
+
+
-

Готово до запуску

-

- Турнір створено успішно. Тепер додайте завдання та налаштуйте бали у панелі керування. +

{t("create_tournament.success_title")}

+

+ {t("create_tournament.success_description")}

, document.body @@ -141,87 +143,87 @@ export const CreateTournamentModal = ({ isOpen, onClose, onCreate }: any) => { } return createPortal( -
-
+
+
-
+

- Новий турнір + {t("create_tournament.title")}

-
-
= 1 ? "bg-white" : "bg-white/20"}`} /> -
= 2 ? "bg-white" : "bg-white/20"}`} /> +
= 1 ? "bg-bg-card" : "bg-bg-card/20"}`} /> +
= 2 ? "bg-bg-card" : "bg-bg-card/20"}`} />
-
+
{currentStep === 1 ? (
-
-