- Event-based triggers (Phase 1): - Trigger/TriggerLog models with field_change type - TriggerService for condition evaluation and action execution - Trigger CRUD API endpoints - Task integration (status, assignee, priority changes) - Frontend: TriggerList, TriggerForm components - Weekly reports (Phase 2): - ScheduledReport/ReportHistory models - ReportService for stats generation - APScheduler for Friday 16:00 job - Report preview/generate/history API - Frontend: WeeklyReportPreview, ReportHistory components - Tests: 23 new tests (14 triggers + 9 reports) - OpenSpec: add-automation change archived 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
97 lines
4.5 KiB
Python
97 lines
4.5 KiB
Python
"""Create automation tables
|
|
|
|
Revision ID: 007
|
|
Revises: 006
|
|
Create Date: 2024-12-29
|
|
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision = '007'
|
|
down_revision = '006'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# Create triggers table
|
|
op.create_table(
|
|
'pjctrl_triggers',
|
|
sa.Column('id', sa.String(36), primary_key=True),
|
|
sa.Column('project_id', sa.String(36), sa.ForeignKey('pjctrl_projects.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('name', sa.String(200), nullable=False),
|
|
sa.Column('description', sa.Text, nullable=True),
|
|
sa.Column('trigger_type', sa.Enum('field_change', 'schedule', name='trigger_type_enum'), nullable=False),
|
|
sa.Column('conditions', sa.JSON, nullable=False),
|
|
sa.Column('actions', sa.JSON, nullable=False),
|
|
sa.Column('is_active', sa.Boolean, server_default='1', nullable=False),
|
|
sa.Column('created_by', sa.String(36), sa.ForeignKey('pjctrl_users.id', ondelete='SET NULL'), nullable=True),
|
|
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create index for triggers
|
|
op.create_index('idx_trigger_project', 'pjctrl_triggers', ['project_id', 'is_active'])
|
|
|
|
# Create trigger_logs table
|
|
op.create_table(
|
|
'pjctrl_trigger_logs',
|
|
sa.Column('id', sa.String(36), primary_key=True),
|
|
sa.Column('trigger_id', sa.String(36), sa.ForeignKey('pjctrl_triggers.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('task_id', sa.String(36), sa.ForeignKey('pjctrl_tasks.id', ondelete='SET NULL'), nullable=True),
|
|
sa.Column('executed_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
|
sa.Column('status', sa.Enum('success', 'failed', name='trigger_log_status_enum'), nullable=False),
|
|
sa.Column('details', sa.JSON, nullable=True),
|
|
sa.Column('error_message', sa.Text, nullable=True),
|
|
)
|
|
|
|
# Create indexes for trigger_logs
|
|
op.create_index('idx_trigger_log_trigger', 'pjctrl_trigger_logs', ['trigger_id', 'executed_at'])
|
|
op.create_index('idx_trigger_log_task', 'pjctrl_trigger_logs', ['task_id'])
|
|
|
|
# Create scheduled_reports table
|
|
op.create_table(
|
|
'pjctrl_scheduled_reports',
|
|
sa.Column('id', sa.String(36), primary_key=True),
|
|
sa.Column('report_type', sa.Enum('weekly', name='report_type_enum'), nullable=False),
|
|
sa.Column('recipient_id', sa.String(36), sa.ForeignKey('pjctrl_users.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('is_active', sa.Boolean, server_default='1', nullable=False),
|
|
sa.Column('last_sent_at', sa.DateTime, nullable=True),
|
|
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create index for scheduled_reports
|
|
op.create_index('idx_scheduled_report_recipient', 'pjctrl_scheduled_reports', ['recipient_id', 'is_active'])
|
|
|
|
# Create report_history table
|
|
op.create_table(
|
|
'pjctrl_report_history',
|
|
sa.Column('id', sa.String(36), primary_key=True),
|
|
sa.Column('report_id', sa.String(36), sa.ForeignKey('pjctrl_scheduled_reports.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('generated_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
|
sa.Column('content', sa.JSON, nullable=False),
|
|
sa.Column('status', sa.Enum('sent', 'failed', name='report_history_status_enum'), nullable=False),
|
|
sa.Column('error_message', sa.Text, nullable=True),
|
|
)
|
|
|
|
# Create index for report_history
|
|
op.create_index('idx_report_history', 'pjctrl_report_history', ['report_id', 'generated_at'])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index('idx_report_history', table_name='pjctrl_report_history')
|
|
op.drop_table('pjctrl_report_history')
|
|
op.drop_index('idx_scheduled_report_recipient', table_name='pjctrl_scheduled_reports')
|
|
op.drop_table('pjctrl_scheduled_reports')
|
|
op.drop_index('idx_trigger_log_task', table_name='pjctrl_trigger_logs')
|
|
op.drop_index('idx_trigger_log_trigger', table_name='pjctrl_trigger_logs')
|
|
op.drop_table('pjctrl_trigger_logs')
|
|
op.drop_index('idx_trigger_project', table_name='pjctrl_triggers')
|
|
op.drop_table('pjctrl_triggers')
|
|
op.execute("DROP TYPE IF EXISTS trigger_type_enum")
|
|
op.execute("DROP TYPE IF EXISTS trigger_log_status_enum")
|
|
op.execute("DROP TYPE IF EXISTS report_type_enum")
|
|
op.execute("DROP TYPE IF EXISTS report_history_status_enum")
|