""" FSM Transitions for Annotation model. This module defines declarative transitions for the Annotation entity. Annotation transitions can update related task states via post_transition_hooks. """ from typing import Any, Dict, Optional from fsm.registry import register_state_transition from fsm.state_choices import AnnotationStateChoices from fsm.transitions import ModelChangeTransition, StateModelType, TransitionContext @register_state_transition('annotation', 'annotation_submitted', triggers_on_create=True, triggers_on_update=False) class AnnotationSubmittedTransition(ModelChangeTransition): """ Transition when an annotation is submitted. This is the default transition for newly created annotations. Trigger: Automatically on creation only (triggers_on_create=True, triggers_on_update=False) """ def get_target_state(self, context: Optional[TransitionContext] = None) -> str: return AnnotationStateChoices.SUBMITTED def get_reason(self, context: TransitionContext) -> str: """Return detailed reason for annotation submission.""" return 'Annotation submitted for review' def transition(self, context: TransitionContext) -> Dict[str, Any]: """Execute annotation submission transition.""" annotation = context.entity return { 'task_id': annotation.task_id, 'project_id': annotation.project_id, 'completed_by_id': annotation.completed_by_id, 'lead_time': annotation.lead_time, } def post_transition_hook(self, context: TransitionContext, state_record: StateModelType) -> None: """ Post-transition hook for annotation submission. Updates task state to COMPLETED when annotation is submitted. Then updates project state based on task completion status. Handles "cold start" scenarios where task may not have state record yet. """ from fsm.project_transitions import update_project_state_after_task_change from fsm.state_choices import TaskStateChoices from fsm.state_manager import StateManager from fsm.utils import get_or_initialize_state annotation = context.entity task = annotation.task project = annotation.project # Get current task state (initialize if needed) current_task_state = StateManager.get_current_state_value(task) if current_task_state is None: # Task has no state record - initialize it # Since annotation was just submitted, task should be COMPLETED current_task_state = get_or_initialize_state( task, user=context.current_user, inferred_state=TaskStateChoices.COMPLETED ) # Transition task to COMPLETED if not already if current_task_state != TaskStateChoices.COMPLETED: StateManager.execute_transition(entity=task, transition_name='task_completed', user=context.current_user) # Update project state based on task changes update_project_state_after_task_change(project, user=context.current_user) @register_state_transition( 'annotation', 'annotation_updated', triggers_on_create=False, triggers_on_update=True, force_state_record=True ) class AnnotationUpdatedTransition(ModelChangeTransition): """ Transition when an annotation is updated. Updates keep the annotation in SUBMITTED state but create audit trail records. Trigger: On update (triggers_on_create=False, triggers_on_update=True, force_state_record=True) """ def get_target_state(self, context: Optional[TransitionContext] = None) -> str: return AnnotationStateChoices.SUBMITTED def get_reason(self, context: TransitionContext) -> str: """Return detailed reason for annotation update.""" return 'Annotation updated' def transition(self, context: TransitionContext) -> Dict[str, Any]: """Execute annotation update transition.""" annotation = context.entity return { 'task_id': annotation.task_id, 'project_id': annotation.project_id, 'updated_by_id': getattr(annotation, 'updated_by_id', None), 'changed_fields': list(self.changed_fields.keys()) if self.changed_fields else [], } def post_transition_hook(self, context: TransitionContext, state_record: StateModelType) -> None: """Post-transition hook for annotation updates.""" pass