feat(db): all domain models, initial Alembic migration (32 tables), seed script (8 districts, 89 regions, roles)

This commit is contained in:
apuc committed 2026-09-26 19:46:44 +03:00
1 parent 71460ab56a
commit 4a610ba94a
19 files changed
+1290 -2

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
View File
Whitespace-only changes.
@@ -0,0 +1,444 @@
"""initial schema
Revision ID: fbd00c295888
Revises:
Create Date: 2026-09-26 19:45:54.541490
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = 'fbd00c295888'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('broadcast_templates',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=64), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('type', sa.Enum('info', 'education', 'rating', name='broadcasttype', native_enum=False, length=20), nullable=False),
sa.Column('body_template', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.create_table('federal_districts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('code', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code'),
sa.UniqueConstraint('name')
)
op.create_table('goals',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('description', sa.String(length=1000), nullable=True),
sa.Column('target_value', sa.Numeric(precision=14, scale=2), nullable=True),
sa.Column('unit', sa.String(length=32), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('knowledge_categories',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.Enum('admin', 'coordinator', 'region_head', 'member', 'deputy', name='rolecode', native_enum=False, length=20), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('full_name', sa.String(length=200), nullable=True),
sa.Column('messenger_type', sa.Enum('telegram', 'max', name='messengertype', native_enum=False, length=20), nullable=False),
sa.Column('messenger_id', sa.String(length=64), nullable=False),
sa.Column('phone', sa.String(length=32), nullable=True),
sa.Column('status', sa.Enum('active', 'blocked', 'pending', name='userstatus', native_enum=False, length=20), nullable=False),
sa.Column('member_type', sa.Enum('member', 'resident', name='membertype', native_enum=False, length=20), nullable=True),
sa.Column('joined_at', sa.Date(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('messenger_type', 'messenger_id', name='uq_user_messenger')
)
op.create_table('assistant_actions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('owner_user_id', sa.Integer(), nullable=False),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('entity_type', sa.String(length=100), nullable=True),
sa.Column('entity_id', sa.String(length=100), nullable=True),
sa.Column('payload', sa.JSON(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('assistant_configs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('owner_user_id', sa.Integer(), nullable=False),
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('permissions', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('owner_user_id')
)
op.create_table('audit_log',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('actor_user_id', sa.Integer(), nullable=True),
sa.Column('actor_type', sa.Enum('user', 'assistant', 'system', name='actortype', native_enum=False, length=20), nullable=False),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('entity_type', sa.String(length=100), nullable=True),
sa.Column('entity_id', sa.String(length=100), nullable=True),
sa.Column('payload', sa.JSON(), nullable=True),
sa.Column('ip', sa.String(length=64), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['actor_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('broadcasts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('template_id', sa.Integer(), nullable=True),
sa.Column('type', sa.Enum('info', 'education', 'rating', name='broadcasttype', native_enum=False, length=20), nullable=False),
sa.Column('subject', sa.String(length=300), nullable=True),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('audience_filter', sa.JSON(), nullable=True),
sa.Column('status', sa.Enum('draft', 'scheduled', 'sending', 'sent', 'failed', name='broadcaststatus', native_enum=False, length=20), nullable=False),
sa.Column('scheduled_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['template_id'], ['broadcast_templates.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('data_uploads',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('goal_id', sa.Integer(), nullable=False),
sa.Column('file_key', sa.String(length=500), nullable=False),
sa.Column('uploaded_by', sa.Integer(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('events',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=300), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('starts_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('ends_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('location', sa.String(length=300), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('knowledge_articles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=300), nullable=False),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('category_id', sa.Integer(), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['category_id'], ['knowledge_categories.id'], ),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('platform_history',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=300), nullable=False),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('event_date', sa.Date(), nullable=True),
sa.Column('is_public', sa.Boolean(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('regions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('federal_district_id', sa.Integer(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['federal_district_id'], ['federal_districts.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('sessions',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token_hash', sa.String(length=128), nullable=False),
sa.Column('messenger_type', sa.Enum('telegram', 'max', name='messengertype', native_enum=False, length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token_hash')
)
op.create_table('snapshots',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('goal_id', sa.Integer(), nullable=False),
sa.Column('snapshot_date', sa.Date(), nullable=False),
sa.Column('kind', sa.Enum('point0', 'current', 'history', name='snapshotkind', native_enum=False, length=20), nullable=False),
sa.Column('source', sa.String(length=32), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('advices',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=False),
sa.Column('goal_id', sa.Integer(), nullable=True),
sa.Column('author_user_id', sa.Integer(), nullable=True),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['author_user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('broadcast_recipients',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('broadcast_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('status', sa.Enum('pending', 'sent', 'failed', name='recipientstatus', native_enum=False, length=20), nullable=False),
sa.Column('sent_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('error', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['broadcast_id'], ['broadcasts.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('broadcast_id', 'user_id', name='uq_broadcast_user')
)
op.create_table('event_reminders',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('event_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('remind_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('sent_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['event_id'], ['events.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('login_tokens',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('token', sa.String(length=128), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('region_id', sa.Integer(), nullable=True),
sa.Column('issued_by', sa.Integer(), nullable=True),
sa.Column('status', sa.Enum('active', 'used', 'expired', 'revoked', name='logintokenstatus', native_enum=False, length=20), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('used_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['issued_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token')
)
op.create_table('region_card_history',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=False),
sa.Column('field', sa.String(length=100), nullable=False),
sa.Column('old_value', sa.String(length=1000), nullable=True),
sa.Column('new_value', sa.String(length=1000), nullable=True),
sa.Column('changed_by', sa.Integer(), nullable=True),
sa.Column('changed_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['changed_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('region_cards',
sa.Column('region_id', sa.Integer(), nullable=False),
sa.Column('population', sa.Integer(), nullable=True),
sa.Column('head_user_id', sa.Integer(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['head_user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('region_id')
)
op.create_table('region_goal_values',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('snapshot_id', sa.Integer(), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=False),
sa.Column('value', sa.Numeric(precision=14, scale=2), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['snapshot_id'], ['snapshots.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('snapshot_id', 'region_id', name='uq_snapshot_region')
)
op.create_table('region_links',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=False),
sa.Column('type', sa.Enum('community', 'chat', name='linktype', native_enum=False, length=20), nullable=False),
sa.Column('title', sa.String(length=200), nullable=True),
sa.Column('url', sa.String(length=500), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('requests',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('type', sa.Enum('join', 'access', 'event', 'support', name='requesttype', native_enum=False, length=20), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('region_id', sa.Integer(), nullable=True),
sa.Column('subject', sa.String(length=300), nullable=True),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('status', sa.Enum('new', 'in_progress', 'closed', name='requeststatus', native_enum=False, length=20), nullable=False),
sa.Column('assigned_to', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['assigned_to'], ['users.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=300), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due_date', sa.Date(), nullable=True),
sa.Column('priority', sa.Enum('low', 'medium', 'high', name='taskpriority', native_enum=False, length=20), nullable=False),
sa.Column('status', sa.Enum('new', 'in_progress', 'review', 'done', name='taskstatus', native_enum=False, length=20), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=True),
sa.Column('assignee_user_id', sa.Integer(), nullable=True),
sa.Column('assignee_role', sa.Enum('admin', 'coordinator', 'region_head', 'member', 'deputy', name='rolecode', native_enum=False, length=20), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=True),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['assignee_user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user_regions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=False),
sa.Column('is_primary', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'region_id', name='uq_user_region')
)
op.create_table('user_roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('region_id', sa.Integer(), nullable=True),
sa.Column('granted_by', sa.Integer(), nullable=True),
sa.Column('granted_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['granted_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'role_id', 'region_id', name='uq_user_role')
)
op.create_table('artifacts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=300), nullable=True),
sa.Column('file_key', sa.String(length=500), nullable=False),
sa.Column('file_name', sa.String(length=300), nullable=False),
sa.Column('mime_type', sa.String(length=100), nullable=True),
sa.Column('size_bytes', sa.BigInteger(), nullable=True),
sa.Column('region_id', sa.Integer(), nullable=True),
sa.Column('goal_id', sa.Integer(), nullable=True),
sa.Column('task_id', sa.Integer(), nullable=True),
sa.Column('uploaded_by', sa.Integer(), nullable=True),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
sa.ForeignKeyConstraint(['region_id'], ['regions.id'], ),
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ),
sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('request_comments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('request_id', sa.Integer(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=True),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('task_comments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.Integer(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=True),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('task_comments')
op.drop_table('request_comments')
op.drop_table('artifacts')
op.drop_table('user_roles')
op.drop_table('user_regions')
op.drop_table('tasks')
op.drop_table('requests')
op.drop_table('region_links')
op.drop_table('region_goal_values')
op.drop_table('region_cards')
op.drop_table('region_card_history')
op.drop_table('login_tokens')
op.drop_table('event_reminders')
op.drop_table('broadcast_recipients')
op.drop_table('advices')
op.drop_table('snapshots')
op.drop_table('sessions')
op.drop_table('regions')
op.drop_table('platform_history')
op.drop_table('knowledge_articles')
op.drop_table('events')
op.drop_table('data_uploads')
op.drop_table('broadcasts')
op.drop_table('audit_log')
op.drop_table('assistant_configs')
op.drop_table('assistant_actions')
op.drop_table('users')
op.drop_table('roles')
op.drop_table('knowledge_categories')
op.drop_table('goals')
op.drop_table('federal_districts')
op.drop_table('broadcast_templates')
# ### end Alembic commands ###
+47 -2
View File
@@ -1,4 +1,49 @@
"""Модели данных."""
"""Модели данных (регистрация для Alembic)."""
from app.models.advice import Advice
from app.models.artifact import Artifact
from app.models.audit import AssistantAction, AssistantConfig, AuditLog
from app.models.broadcast import Broadcast, BroadcastRecipient, BroadcastTemplate
from app.models.event import Event, EventReminder
from app.models.goal import DataUpload, Goal, RegionGoalValue, Snapshot
from app.models.history import PlatformHistory
from app.models.knowledge import KnowledgeArticle, KnowledgeCategory
from app.models.region import FederalDistrict, Region
from app.models.region_card import RegionCard, RegionCardHistory, RegionLink
from app.models.request import Request, RequestComment
from app.models.task import Task, TaskComment
from app.models.user import LoginToken, Role, Session, User, UserRegion, UserRole
__all__ = ["FederalDistrict", "Region"]
__all__ = [
"Advice",
"Artifact",
"AssistantAction",
"AssistantConfig",
"AuditLog",
"Broadcast",
"BroadcastRecipient",
"BroadcastTemplate",
"DataUpload",
"Event",
"EventReminder",
"FederalDistrict",
"Goal",
"KnowledgeArticle",
"KnowledgeCategory",
"LoginToken",
"PlatformHistory",
"Region",
"RegionCard",
"RegionCardHistory",
"RegionGoalValue",
"RegionLink",
"Request",
"RequestComment",
"Role",
"Session",
"Snapshot",
"Task",
"TaskComment",
"User",
"UserRegion",
"UserRole",
]
+19
View File
@@ -0,0 +1,19 @@
"""Модели советов от регионов."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Advice(Base):
__tablename__ = "advices"
id: Mapped[int] = mapped_column(primary_key=True)
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
goal_id: Mapped[int | None] = mapped_column(ForeignKey("goals.id"))
author_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
body: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(20), default="published")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+24
View File
@@ -0,0 +1,24 @@
"""Модели артефактов (файлов)."""
from datetime import datetime
from sqlalchemy import BigInteger, DateTime, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Artifact(Base):
__tablename__ = "artifacts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str | None] = mapped_column(String(300))
file_key: Mapped[str] = mapped_column(String(500))
file_name: Mapped[str] = mapped_column(String(300))
mime_type: Mapped[str | None] = mapped_column(String(100))
size_bytes: Mapped[int | None] = mapped_column(BigInteger)
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
goal_id: Mapped[int | None] = mapped_column(ForeignKey("goals.id"))
task_id: Mapped[int | None] = mapped_column(ForeignKey("tasks.id"))
uploaded_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+49
View File
@@ -0,0 +1,49 @@
"""Модели аудита и AI-ассистента."""
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, JSON, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
from app.models.enums import ActorType
from app.models.types import enum_col
class AuditLog(Base):
__tablename__ = "audit_log"
id: Mapped[int] = mapped_column(primary_key=True)
actor_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
actor_type: Mapped[ActorType] = mapped_column(enum_col(ActorType))
action: Mapped[str] = mapped_column(String(100))
entity_type: Mapped[str | None] = mapped_column(String(100))
entity_id: Mapped[str | None] = mapped_column(String(100))
payload: Mapped[dict | None] = mapped_column(JSON)
ip: Mapped[str | None] = mapped_column(String(64))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class AssistantConfig(Base):
__tablename__ = "assistant_configs"
id: Mapped[int] = mapped_column(primary_key=True)
owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
permissions: Mapped[dict | None] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class AssistantAction(Base):
__tablename__ = "assistant_actions"
id: Mapped[int] = mapped_column(primary_key=True)
owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
action: Mapped[str] = mapped_column(String(100))
entity_type: Mapped[str | None] = mapped_column(String(100))
entity_id: Mapped[str | None] = mapped_column(String(100))
payload: Mapped[dict | None] = mapped_column(JSON)
status: Mapped[str] = mapped_column(String(20), default="ok")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+65
View File
@@ -0,0 +1,65 @@
"""Модели рассылок."""
from datetime import datetime
from sqlalchemy import (
DateTime,
ForeignKey,
JSON,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
from app.models.enums import BroadcastStatus, BroadcastType, RecipientStatus
from app.models.types import enum_col
class BroadcastTemplate(Base):
__tablename__ = "broadcast_templates"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[str] = mapped_column(String(64), unique=True)
name: Mapped[str] = mapped_column(String(200))
type: Mapped[BroadcastType] = mapped_column(enum_col(BroadcastType))
body_template: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class Broadcast(Base):
__tablename__ = "broadcasts"
id: Mapped[int] = mapped_column(primary_key=True)
template_id: Mapped[int | None] = mapped_column(ForeignKey("broadcast_templates.id"))
type: Mapped[BroadcastType] = mapped_column(enum_col(BroadcastType))
subject: Mapped[str | None] = mapped_column(String(300))
body: Mapped[str | None] = mapped_column(Text)
audience_filter: Mapped[dict | None] = mapped_column(JSON)
status: Mapped[BroadcastStatus] = mapped_column(
enum_col(BroadcastStatus), default=BroadcastStatus.DRAFT
)
scheduled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class BroadcastRecipient(Base):
__tablename__ = "broadcast_recipients"
id: Mapped[int] = mapped_column(primary_key=True)
broadcast_id: Mapped[int] = mapped_column(ForeignKey("broadcasts.id", ondelete="CASCADE"))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
status: Mapped[RecipientStatus] = mapped_column(
enum_col(RecipientStatus), default=RecipientStatus.PENDING
)
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
error: Mapped[str | None] = mapped_column(Text)
__table_args__ = (UniqueConstraint("broadcast_id", "user_id", name="uq_broadcast_user"),)
+96
View File
@@ -0,0 +1,96 @@
"""Перечисления предметной области."""
from enum import StrEnum
class MessengerType(StrEnum):
TELEGRAM = "telegram"
MAX = "max"
class UserStatus(StrEnum):
ACTIVE = "active"
BLOCKED = "blocked"
PENDING = "pending"
class RoleCode(StrEnum):
ADMIN = "admin"
COORDINATOR = "coordinator"
REGION_HEAD = "region_head"
MEMBER = "member"
DEPUTY = "deputy"
class LoginTokenStatus(StrEnum):
ACTIVE = "active"
USED = "used"
EXPIRED = "expired"
REVOKED = "revoked"
class SnapshotKind(StrEnum):
POINT0 = "point0"
CURRENT = "current"
HISTORY = "history"
class MemberType(StrEnum):
MEMBER = "member"
RESIDENT = "resident"
class TaskStatus(StrEnum):
NEW = "new"
IN_PROGRESS = "in_progress"
REVIEW = "review"
DONE = "done"
class TaskPriority(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class RequestType(StrEnum):
JOIN = "join"
ACCESS = "access"
EVENT = "event"
SUPPORT = "support"
class RequestStatus(StrEnum):
NEW = "new"
IN_PROGRESS = "in_progress"
CLOSED = "closed"
class BroadcastType(StrEnum):
INFO = "info"
EDUCATION = "education"
RATING = "rating"
class BroadcastStatus(StrEnum):
DRAFT = "draft"
SCHEDULED = "scheduled"
SENDING = "sending"
SENT = "sent"
FAILED = "failed"
class RecipientStatus(StrEnum):
PENDING = "pending"
SENT = "sent"
FAILED = "failed"
class ActorType(StrEnum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
class LinkType(StrEnum):
COMMUNITY = "community"
CHAT = "chat"
+33
View File
@@ -0,0 +1,33 @@
"""Модели календаря событий."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Event(Base):
__tablename__ = "events"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
description: Mapped[str | None] = mapped_column(Text)
starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
location: Mapped[str | None] = mapped_column(String(300))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class EventReminder(Base):
__tablename__ = "event_reminders"
id: Mapped[int] = mapped_column(primary_key=True)
event_id: Mapped[int] = mapped_column(ForeignKey("events.id", ondelete="CASCADE"))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
remind_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
+75
View File
@@ -0,0 +1,75 @@
"""Модели целей, срезов и метрик."""
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Numeric,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import SnapshotKind
from app.models.types import enum_col
class Goal(Base):
__tablename__ = "goals"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(200))
description: Mapped[str | None] = mapped_column(String(1000))
target_value: Mapped[float | None] = mapped_column(Numeric(14, 2))
unit: Mapped[str | None] = mapped_column(String(32))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
snapshots: Mapped[list["Snapshot"]] = relationship(back_populates="goal")
class Snapshot(Base):
__tablename__ = "snapshots"
id: Mapped[int] = mapped_column(primary_key=True)
goal_id: Mapped[int] = mapped_column(ForeignKey("goals.id", ondelete="CASCADE"))
snapshot_date: Mapped[date] = mapped_column(Date)
kind: Mapped[SnapshotKind] = mapped_column(enum_col(SnapshotKind))
source: Mapped[str | None] = mapped_column(String(32))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
goal: Mapped[Goal] = relationship(back_populates="snapshots")
values: Mapped[list["RegionGoalValue"]] = relationship(back_populates="snapshot")
class RegionGoalValue(Base):
__tablename__ = "region_goal_values"
id: Mapped[int] = mapped_column(primary_key=True)
snapshot_id: Mapped[int] = mapped_column(ForeignKey("snapshots.id", ondelete="CASCADE"))
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
value: Mapped[float] = mapped_column(Numeric(14, 2))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (UniqueConstraint("snapshot_id", "region_id", name="uq_snapshot_region"),)
snapshot: Mapped[Snapshot] = relationship(back_populates="values")
class DataUpload(Base):
__tablename__ = "data_uploads"
id: Mapped[int] = mapped_column(primary_key=True)
goal_id: Mapped[int] = mapped_column(ForeignKey("goals.id"))
file_key: Mapped[str] = mapped_column(String(500))
uploaded_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
status: Mapped[str] = mapped_column(String(20), default="uploaded")
processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+19
View File
@@ -0,0 +1,19 @@
"""Модели истории платформы."""
from datetime import date, datetime
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class PlatformHistory(Base):
__tablename__ = "platform_history"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
body: Mapped[str | None] = mapped_column(Text)
event_date: Mapped[date | None] = mapped_column(Date)
is_public: Mapped[bool] = mapped_column(Boolean, default=True)
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+28
View File
@@ -0,0 +1,28 @@
"""Модели базы знаний."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class KnowledgeCategory(Base):
__tablename__ = "knowledge_categories"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(200), unique=True)
class KnowledgeArticle(Base):
__tablename__ = "knowledge_articles"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
body: Mapped[str] = mapped_column(Text)
category_id: Mapped[int | None] = mapped_column(ForeignKey("knowledge_categories.id"))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
+46
View File
@@ -0,0 +1,46 @@
"""Модели карточки региона."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
from app.models.enums import LinkType
from app.models.types import enum_col
class RegionCard(Base):
__tablename__ = "region_cards"
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"), primary_key=True)
population: Mapped[int | None] = mapped_column(Integer)
head_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class RegionLink(Base):
__tablename__ = "region_links"
id: Mapped[int] = mapped_column(primary_key=True)
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
type: Mapped[LinkType] = mapped_column(enum_col(LinkType))
title: Mapped[str | None] = mapped_column(String(200))
url: Mapped[str] = mapped_column(String(500))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class RegionCardHistory(Base):
__tablename__ = "region_card_history"
id: Mapped[int] = mapped_column(primary_key=True)
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
field: Mapped[str] = mapped_column(String(100))
old_value: Mapped[str | None] = mapped_column(String(1000))
new_value: Mapped[str | None] = mapped_column(String(1000))
changed_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
changed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+42
View File
@@ -0,0 +1,42 @@
"""Модели запросов (заявок)."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import RequestStatus, RequestType
from app.models.types import enum_col
class Request(Base):
__tablename__ = "requests"
id: Mapped[int] = mapped_column(primary_key=True)
type: Mapped[RequestType] = mapped_column(enum_col(RequestType))
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
subject: Mapped[str | None] = mapped_column(String(300))
body: Mapped[str | None] = mapped_column(Text)
status: Mapped[RequestStatus] = mapped_column(
enum_col(RequestStatus), default=RequestStatus.NEW
)
assigned_to: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
comments: Mapped[list["RequestComment"]] = relationship(back_populates="request")
class RequestComment(Base):
__tablename__ = "request_comments"
id: Mapped[int] = mapped_column(primary_key=True)
request_id: Mapped[int] = mapped_column(ForeignKey("requests.id", ondelete="CASCADE"))
author_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
body: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
request: Mapped[Request] = relationship(back_populates="comments")
+47
View File
@@ -0,0 +1,47 @@
"""Модели задач (трекера)."""
from datetime import date, datetime
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import RoleCode, TaskPriority, TaskStatus
from app.models.types import enum_col
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
description: Mapped[str | None] = mapped_column(Text)
due_date: Mapped[date | None] = mapped_column(Date)
priority: Mapped[TaskPriority] = mapped_column(
enum_col(TaskPriority), default=TaskPriority.MEDIUM
)
status: Mapped[TaskStatus] = mapped_column(
enum_col(TaskStatus), default=TaskStatus.NEW
)
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
assignee_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
assignee_role: Mapped[RoleCode | None] = mapped_column(enum_col(RoleCode))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
comments: Mapped[list["TaskComment"]] = relationship(back_populates="task")
class TaskComment(Base):
__tablename__ = "task_comments"
id: Mapped[int] = mapped_column(primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"))
author_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
body: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
task: Mapped[Task] = relationship(back_populates="comments")
+17
View File
@@ -0,0 +1,17 @@
"""Вспомогательные типы для моделей."""
from enum import Enum as PyEnum
from sqlalchemy import Enum as SAEnum
def enum_col(enum_cls: type[PyEnum], length: int = 20) -> SAEnum:
"""Enum-колонка, хранящая значения (value), а не имена членов.
Например, BroadcastType.INFO хранится как 'info', а не 'INFO'.
"""
return SAEnum(
enum_cls,
native_enum=False,
length=length,
values_callable=lambda obj: [e.value for e in obj],
)
+108
View File
@@ -0,0 +1,108 @@
"""Модели пользователей, ролей и доступа."""
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import LoginTokenStatus, MemberType, MessengerType, RoleCode, UserStatus
from app.models.types import enum_col
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
full_name: Mapped[str | None] = mapped_column(String(200))
messenger_type: Mapped[MessengerType] = mapped_column(enum_col(MessengerType))
messenger_id: Mapped[str] = mapped_column(String(64))
phone: Mapped[str | None] = mapped_column(String(32))
status: Mapped[UserStatus] = mapped_column(
enum_col(UserStatus), default=UserStatus.ACTIVE
)
member_type: Mapped[MemberType | None] = mapped_column(enum_col(MemberType))
joined_at: Mapped[date | None] = mapped_column(Date)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
__table_args__ = (UniqueConstraint("messenger_type", "messenger_id", name="uq_user_messenger"),)
regions: Mapped[list["UserRegion"]] = relationship(back_populates="user")
roles: Mapped[list["UserRole"]] = relationship(
back_populates="user", foreign_keys="UserRole.user_id"
)
class UserRegion(Base):
__tablename__ = "user_regions"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
is_primary: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (UniqueConstraint("user_id", "region_id", name="uq_user_region"),)
user: Mapped[User] = relationship(back_populates="regions")
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[RoleCode] = mapped_column(enum_col(RoleCode), unique=True)
name: Mapped[str] = mapped_column(String(100))
class UserRole(Base):
__tablename__ = "user_roles"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
granted_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (UniqueConstraint("user_id", "role_id", "region_id", name="uq_user_role"),)
user: Mapped[User] = relationship(back_populates="roles", foreign_keys=[user_id])
class LoginToken(Base):
__tablename__ = "login_tokens"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
token: Mapped[str] = mapped_column(String(128), unique=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
issued_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
status: Mapped[LoginTokenStatus] = mapped_column(
enum_col(LoginTokenStatus), default=LoginTokenStatus.ACTIVE
)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Session(Base):
__tablename__ = "sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
token_hash: Mapped[str] = mapped_column(String(128), unique=True)
messenger_type: Mapped[MessengerType] = mapped_column(enum_col(MessengerType))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
+106
View File
@@ -0,0 +1,106 @@
"""Наполнение справочников: федеральные округа, регионы, роли.
Запуск:
docker compose exec api python -m scripts.seed
"""
from sqlalchemy import select
from app.db.session import SessionLocal
from app.models.enums import RoleCode
from app.models.region import FederalDistrict, Region
from app.models.user import Role
DISTRICTS: dict[str, list[str]] = {
"ЦФО": [
"Белгородская область", "Брянская область", "Владимирская область",
"Воронежская область", "Ивановская область", "Калужская область",
"Костромская область", "Курская область", "Липецкая область",
"Московская область", "Орловская область", "Рязанская область",
"Смоленская область", "Тамбовская область", "Тверская область",
"Тульская область", "Ярославская область", "Москва",
],
"СЗФО": [
"Республика Карелия", "Республика Коми", "Архангельская область",
"Вологодская область", "Калининградская область", "Ленинградская область",
"Мурманская область", "Новгородская область", "Псковская область",
"Санкт-Петербург", "Ненецкий автономный округ",
],
"ЮФО": [
"Республика Адыгея", "Республика Калмыкия", "Республика Крым",
"Краснодарский край", "Астраханская область", "Волгоградская область",
"Ростовская область", "Севастополь",
"Донецкая Народная Республика", "Луганская Народная Республика",
"Запорожская область", "Херсонская область",
],
"СКФО": [
"Республика Дагестан", "Республика Ингушетия",
"Кабардино-Балкарская Республика", "Карачаево-Черкесская Республика",
"Республика Северная Осетия — Алания", "Чеченская Республика",
"Ставропольский край",
],
"ПФО": [
"Республика Башкортостан", "Республика Марий Эл", "Республика Мордовия",
"Республика Татарстан", "Удмуртская Республика", "Чувашская Республика",
"Пермский край", "Кировская область", "Нижегородская область",
"Оренбургская область", "Пензенская область", "Самарская область",
"Саратовская область", "Ульяновская область",
],
"УФО": [
"Курганская область", "Свердловская область", "Тюменская область",
"Челябинская область", "Ханты-Мансийский автономный округ — Югра",
"Ямало-Ненецкий автономный округ",
],
"СФО": [
"Республика Алтай", "Республика Тыва", "Республика Хакасия",
"Алтайский край", "Красноярский край", "Кемеровская область — Кузбасс",
"Иркутская область", "Новосибирская область", "Омская область",
"Томская область",
],
"ДФО": [
"Республика Бурятия", "Республика Саха (Якутия)", "Забайкальский край",
"Камчатский край", "Приморский край", "Хабаровский край",
"Амурская область", "Магаданская область", "Сахалинская область",
"Еврейская автономная область", "Чукотский автономный округ",
],
}
ROLES: list[tuple[RoleCode, str]] = [
(RoleCode.ADMIN, "Администратор платформы"),
(RoleCode.COORDINATOR, "Координатор"),
(RoleCode.REGION_HEAD, "Руководитель регионального отделения"),
(RoleCode.MEMBER, "Член Организации"),
(RoleCode.DEPUTY, "Заместитель"),
]
def seed() -> None:
db = SessionLocal()
try:
# Роли
for code, name in ROLES:
if not db.scalar(select(Role).where(Role.code == code)):
db.add(Role(code=code, name=name))
# Округа и регионы
for district_name, regions in DISTRICTS.items():
district = db.scalar(
select(FederalDistrict).where(FederalDistrict.name == district_name)
)
if not district:
district = FederalDistrict(name=district_name, code=district_name)
db.add(district)
db.flush()
for region_name in regions:
if not db.scalar(select(Region).where(Region.name == region_name)):
db.add(Region(name=region_name, federal_district_id=district.id))
db.commit()
regions_count = len(db.scalars(select(Region)).all())
districts_count = len(db.scalars(select(FederalDistrict)).all())
print(f"Справочники наполнены: округов — {districts_count}, регионов — {regions_count}.")
finally:
db.close()
if __name__ == "__main__":
seed()