Улучшение системы оспариваний и исправления
- Оспаривания теперь требуют решения админа после 24ч голосования - Можно повторно оспаривать после разрешённых споров - Исправлены бонусные очки при перепрохождении после оспаривания - Сброс серии при невалидном пруфе - Колесо показывает только доступные игры - Rate limiting только через backend (RATE_LIMIT_ENABLED)
This commit is contained in:
@@ -26,8 +26,8 @@ def upgrade() -> None:
|
||||
|
||||
# Insert admin user (ignore if already exists)
|
||||
op.execute(f"""
|
||||
INSERT INTO users (login, password_hash, nickname, role, created_at)
|
||||
VALUES ('admin', '{password_hash}', 'Admin', 'admin', NOW())
|
||||
INSERT INTO users (login, password_hash, nickname, role, is_banned, created_at)
|
||||
VALUES ('admin', '{password_hash}', 'Admin', 'admin', false, NOW())
|
||||
ON CONFLICT (login) DO UPDATE SET
|
||||
password_hash = '{password_hash}',
|
||||
role = 'admin'
|
||||
|
||||
156
backend/alembic/versions/020_add_game_types.py
Normal file
156
backend/alembic/versions/020_add_game_types.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Add game types (playthrough/challenges) and bonus assignments
|
||||
|
||||
Revision ID: 020_add_game_types
|
||||
Revises: 019_add_marathon_cover
|
||||
Create Date: 2024-12-26
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '020_add_game_types'
|
||||
down_revision: Union[str, None] = '019_add_marathon_cover'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def column_exists(table_name: str, column_name: str) -> bool:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
def table_exists(table_name: str) -> bool:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# === Games table: добавляем поля для типа игры ===
|
||||
|
||||
# game_type - тип игры (playthrough/challenges)
|
||||
if not column_exists('games', 'game_type'):
|
||||
op.add_column('games', sa.Column(
|
||||
'game_type',
|
||||
sa.String(20),
|
||||
nullable=False,
|
||||
server_default='challenges'
|
||||
))
|
||||
|
||||
# playthrough_points - очки за прохождение
|
||||
if not column_exists('games', 'playthrough_points'):
|
||||
op.add_column('games', sa.Column(
|
||||
'playthrough_points',
|
||||
sa.Integer(),
|
||||
nullable=True
|
||||
))
|
||||
|
||||
# playthrough_description - описание прохождения
|
||||
if not column_exists('games', 'playthrough_description'):
|
||||
op.add_column('games', sa.Column(
|
||||
'playthrough_description',
|
||||
sa.Text(),
|
||||
nullable=True
|
||||
))
|
||||
|
||||
# playthrough_proof_type - тип пруфа для прохождения
|
||||
if not column_exists('games', 'playthrough_proof_type'):
|
||||
op.add_column('games', sa.Column(
|
||||
'playthrough_proof_type',
|
||||
sa.String(20),
|
||||
nullable=True
|
||||
))
|
||||
|
||||
# playthrough_proof_hint - подсказка для пруфа
|
||||
if not column_exists('games', 'playthrough_proof_hint'):
|
||||
op.add_column('games', sa.Column(
|
||||
'playthrough_proof_hint',
|
||||
sa.Text(),
|
||||
nullable=True
|
||||
))
|
||||
|
||||
# === Assignments table: добавляем поля для прохождений ===
|
||||
|
||||
# game_id - ссылка на игру (для playthrough)
|
||||
if not column_exists('assignments', 'game_id'):
|
||||
op.add_column('assignments', sa.Column(
|
||||
'game_id',
|
||||
sa.Integer(),
|
||||
sa.ForeignKey('games.id', ondelete='CASCADE'),
|
||||
nullable=True
|
||||
))
|
||||
op.create_index('ix_assignments_game_id', 'assignments', ['game_id'])
|
||||
|
||||
# is_playthrough - флаг прохождения
|
||||
if not column_exists('assignments', 'is_playthrough'):
|
||||
op.add_column('assignments', sa.Column(
|
||||
'is_playthrough',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default='false'
|
||||
))
|
||||
|
||||
# Делаем challenge_id nullable (для playthrough заданий)
|
||||
# SQLite не поддерживает ALTER COLUMN, поэтому проверяем dialect
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != 'sqlite':
|
||||
op.alter_column('assignments', 'challenge_id', nullable=True)
|
||||
|
||||
# === Создаём таблицу bonus_assignments ===
|
||||
|
||||
if not table_exists('bonus_assignments'):
|
||||
op.create_table(
|
||||
'bonus_assignments',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column('main_assignment_id', sa.Integer(),
|
||||
sa.ForeignKey('assignments.id', ondelete='CASCADE'),
|
||||
nullable=False, index=True),
|
||||
sa.Column('challenge_id', sa.Integer(),
|
||||
sa.ForeignKey('challenges.id', ondelete='CASCADE'),
|
||||
nullable=False, index=True),
|
||||
sa.Column('status', sa.String(20), nullable=False, server_default='pending'),
|
||||
sa.Column('proof_path', sa.String(500), nullable=True),
|
||||
sa.Column('proof_url', sa.Text(), nullable=True),
|
||||
sa.Column('proof_comment', sa.Text(), nullable=True),
|
||||
sa.Column('points_earned', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('completed_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Удаляем таблицу bonus_assignments
|
||||
if table_exists('bonus_assignments'):
|
||||
op.drop_table('bonus_assignments')
|
||||
|
||||
# Удаляем поля из assignments
|
||||
if column_exists('assignments', 'is_playthrough'):
|
||||
op.drop_column('assignments', 'is_playthrough')
|
||||
|
||||
if column_exists('assignments', 'game_id'):
|
||||
op.drop_index('ix_assignments_game_id', 'assignments')
|
||||
op.drop_column('assignments', 'game_id')
|
||||
|
||||
# Удаляем поля из games
|
||||
if column_exists('games', 'playthrough_proof_hint'):
|
||||
op.drop_column('games', 'playthrough_proof_hint')
|
||||
|
||||
if column_exists('games', 'playthrough_proof_type'):
|
||||
op.drop_column('games', 'playthrough_proof_type')
|
||||
|
||||
if column_exists('games', 'playthrough_description'):
|
||||
op.drop_column('games', 'playthrough_description')
|
||||
|
||||
if column_exists('games', 'playthrough_points'):
|
||||
op.drop_column('games', 'playthrough_points')
|
||||
|
||||
if column_exists('games', 'game_type'):
|
||||
op.drop_column('games', 'game_type')
|
||||
100
backend/alembic/versions/021_add_bonus_disputes.py
Normal file
100
backend/alembic/versions/021_add_bonus_disputes.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Add bonus assignment disputes support
|
||||
|
||||
Revision ID: 021_add_bonus_disputes
|
||||
Revises: 020_add_game_types
|
||||
Create Date: 2024-12-29
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '021_add_bonus_disputes'
|
||||
down_revision: Union[str, None] = '020_add_game_types'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def column_exists(table_name: str, column_name: str) -> bool:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
def constraint_exists(table_name: str, constraint_name: str) -> bool:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
constraints = inspector.get_unique_constraints(table_name)
|
||||
return any(c['name'] == constraint_name for c in constraints)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
# Add bonus_assignment_id column to disputes
|
||||
if not column_exists('disputes', 'bonus_assignment_id'):
|
||||
op.add_column('disputes', sa.Column(
|
||||
'bonus_assignment_id',
|
||||
sa.Integer(),
|
||||
nullable=True
|
||||
))
|
||||
op.create_foreign_key(
|
||||
'fk_disputes_bonus_assignment_id',
|
||||
'disputes',
|
||||
'bonus_assignments',
|
||||
['bonus_assignment_id'],
|
||||
['id'],
|
||||
ondelete='CASCADE'
|
||||
)
|
||||
op.create_index('ix_disputes_bonus_assignment_id', 'disputes', ['bonus_assignment_id'])
|
||||
|
||||
# Drop the unique index on assignment_id first (required before making nullable)
|
||||
if bind.dialect.name != 'sqlite':
|
||||
try:
|
||||
op.drop_index('ix_disputes_assignment_id', 'disputes')
|
||||
except Exception:
|
||||
pass # Index might not exist
|
||||
|
||||
# Make assignment_id nullable (PostgreSQL only, SQLite doesn't support ALTER COLUMN)
|
||||
if bind.dialect.name != 'sqlite':
|
||||
op.alter_column('disputes', 'assignment_id', nullable=True)
|
||||
|
||||
# Create a non-unique index on assignment_id
|
||||
try:
|
||||
op.create_index('ix_disputes_assignment_id_non_unique', 'disputes', ['assignment_id'])
|
||||
except Exception:
|
||||
pass # Index might already exist
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
# Remove non-unique index
|
||||
try:
|
||||
op.drop_index('ix_disputes_assignment_id_non_unique', table_name='disputes')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Make assignment_id not nullable again
|
||||
if bind.dialect.name != 'sqlite':
|
||||
op.alter_column('disputes', 'assignment_id', nullable=False)
|
||||
|
||||
# Recreate unique index
|
||||
try:
|
||||
op.create_index('ix_disputes_assignment_id', 'disputes', ['assignment_id'], unique=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Remove foreign key, index and column
|
||||
if column_exists('disputes', 'bonus_assignment_id'):
|
||||
try:
|
||||
op.drop_constraint('fk_disputes_bonus_assignment_id', 'disputes', type_='foreignkey')
|
||||
except Exception:
|
||||
pass
|
||||
op.drop_index('ix_disputes_bonus_assignment_id', table_name='disputes')
|
||||
op.drop_column('disputes', 'bonus_assignment_id')
|
||||
Reference in New Issue
Block a user