808ef93a52
- Модель search_operations: title, case_id (unique — операция на карточку),
status (planned/active/paused/completed/archived, CheckConstraint),
unit_id, contour_operation_id (заготовка B20-E4), created_by, closed_at.
- Alembic 009_e3_operations.
- backend/routers/operations.py: POST (создание; unit по умолчанию =
подразделение создателя; скоуп-проверка), GET список (скоуп-фильтр),
GET /{id} (403 вне скоупа), GET /summary (active/total/completed_24h),
PATCH (title/unit/status со статусной машиной: planned→active⇄paused→
completed→archived; недопустимые переходы 400; completed ставит closed_at).
Всё с аудитом operation_create/operation_update.
- Скоуп: unit_id ∈ visible_unit_ids (своё+подчинённые); РЦУ РЧС — все.
- Тесты E3 (12): создание с юнитом/дефолт/дубль кейса/cross-unit 403,
скоуп списков, статусная машина, summary, аудит, cross-scope 403.
222 passed, 5 skipped.
47 lines
1.9 KiB
Python
47 lines
1.9 KiB
Python
"""B21/E3: поисковая операция (мультипоиск).
|
|
|
|
Revision ID: 009_e3_operations
|
|
Revises: 008_e2_audit
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql as pg
|
|
|
|
revision = '009_e3_operations'
|
|
down_revision = '008_e2_audit'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'search_operations',
|
|
sa.Column('id', pg.UUID(as_uuid=True), primary_key=True,
|
|
server_default=sa.text('gen_random_uuid()')),
|
|
sa.Column('title', sa.String(255), nullable=False),
|
|
sa.Column('case_id', pg.UUID(as_uuid=True),
|
|
sa.ForeignKey('cases.id', ondelete='CASCADE'), nullable=False, unique=True),
|
|
sa.Column('status', sa.String(20), nullable=False, server_default='active'),
|
|
sa.Column('unit_id', pg.UUID(as_uuid=True),
|
|
sa.ForeignKey('mchs_units.id', ondelete='SET NULL')),
|
|
sa.Column('contour_operation_id', pg.UUID(as_uuid=True)),
|
|
sa.Column('created_by', pg.UUID(as_uuid=True),
|
|
sa.ForeignKey('users.id', ondelete='SET NULL')),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()')),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()')),
|
|
sa.Column('closed_at', sa.DateTime(timezone=True)),
|
|
sa.CheckConstraint(
|
|
"status IN ('planned','active','paused','completed','archived')",
|
|
name='ck_search_operations_status',
|
|
),
|
|
)
|
|
op.create_index('idx_search_operations_status', 'search_operations', ['status'])
|
|
op.create_index('idx_search_operations_unit', 'search_operations', ['unit_id'])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index('idx_search_operations_unit', table_name='search_operations')
|
|
op.drop_index('idx_search_operations_status', table_name='search_operations')
|
|
op.drop_table('search_operations') |