feat(db): all domain models, initial Alembic migration (32 tables), seed script (8 districts, 89 regions, roles)
This commit is contained in:
1 parent
71460ab56a
commit
4a610ba94a
19 files changed
+1290
-2
No files matched your search
@@ -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"}
|
||||||
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 ###
|
||||||
@@ -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 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",
|
||||||
|
]
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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"),)
|
||||||
@@ -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"
|
||||||
@@ -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))
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
|
)
|
||||||
@@ -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())
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
@@ -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],
|
||||||
|
)
|
||||||
@@ -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))
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user