
    	]jW                     z    d Z ddlZddlmZmZ ddlmZ  ej                  e      Z	 G d dej                        Zy)z
FSM Base Model for Label Studio.

This module contains only FsmHistoryStateModel - the base class that models
inherit from to get FSM integration. State model definitions are in state_models.py
to avoid registration issues in LSE.
    N)AnyDict)modelsc                        e Zd ZdZ G d d      Z fdZe fd       Z fdZde	e
ef   fdZdd	ed
edefdZde	e
ef   fdZdefdZ fdZde
d	ed
e	e
ef   fdZ xZS )FsmHistoryStateModela  
    FSM History State Model - Base class for models that participate in FSM state tracking.

    This class provides explicit FSM integration through model lifecycle hooks,
    replacing the implicit signal-based approach with predictable, testable behavior.

    Key features:
    - Intercepts save operations to trigger FSM transitions
    - Tracks field changes for transition logic
    - Maintains CurrentContext for user/org tracking
    - Provides explicit transition determination
    - Fails gracefully - FSM errors don't break saves

    Usage:
        class Task(FsmHistoryStateModel):
            # ... model fields ...

            def _determine_fsm_transition(self) -> Optional[str]:
                if self._state.adding:  # Creating new instance
                    return 'task_created'

                changed = self._get_changed_fields()
                if 'is_labeled' in changed and changed['is_labeled'][1]:
                    return 'task_labeled'

                return None

            def _get_fsm_transition_data(self) -> Dict[str, Any]:
                return {
                    'project_id': self.project_id,
                    'overlap': self.overlap
                }
    c                       e Zd ZdZy)FsmHistoryStateModel.MetaTN)__name__
__module____qualname__abstract     A/root/env/lib/python3.12/site-packages/label_studio/fsm/models.pyMetar	   9   s    r   r   c                 2    t        |   |i | i | _        y )N)super__init___original_values)selfargskwargs	__class__s      r   r   zFsmHistoryStateModel.__init__<   s    $)&) !#r   c                 ^    t         |   |||      }t        t        ||            |_        |S )a  
        Override from_db to store raw DB values for lazy capture.

        Django calls this method instead of __init__ when loading models from the database.

        PERFORMANCE: We store the raw field values here without processing them.
        This avoids accessing any ForeignKey fields (which would trigger queries).
        We only process these values into _original_values in save() when we actually
        need them for change detection.
        )r   from_dbdictzipr   )clsdbfield_namesvaluesinstancer   s        r   r   zFsmHistoryStateModel.from_dbB   s0     7?2{F;$([&)A$B!r   c                     t         |   |      }t        |      dk\  rIt        |d   t              r6|d   j                         }|j                  dd       |d   |d   |f|dd z   S |S )a"  
        Override serialization to exclude internal FSM tracking fields.

        Django's serialization uses pickle which calls __reduce_ex__.
        We exclude FSM tracking fields since they're only needed for runtime
        change detection, not for serialization/restoration.
              r   Nr      )r   __reduce_ex__len
isinstancer   copypop)r   protocol	reductionstater   s       r   r'   z"FsmHistoryStateModel.__reduce_ex__T   sv     G)(3	 y>Q:ilD#AaL%%'EII($/aL)A,612FFr   returnc                 f   t        | d      r| j                  si S i }| j                  j                  D ]{  }|j                  | j                  vr|j
                  r|j                  r5| j                  |j                     }t        | |j                  d      }||k7  sk||f||j                  <   } |S )a  
        Get fields that changed since the last load/save.

        Returns:
            Dict mapping field names to (old_value, new_value) tuples
            Note: For ForeignKey fields, old_value will be the PK, new_value will be the object

        Example:
            changed = self._get_changed_fields()
            if 'is_labeled' in changed:
                old_val, new_val = changed['is_labeled']
                if not old_val and new_val:
                    # Task became labeled
                    pass
        r   N)hasattrr   _metafieldsattnameis_relationmany_to_manygetattr)r   changedfield	old_value	new_values        r   _get_changed_fieldsz(FsmHistoryStateModel._get_changed_fieldsj   s    $ t/08M8MIZZ&& 	@E }}D$9$99  U%7%7--emm<IemmT:II%*3Y)?&	@ r   is_creatingchanged_fieldsc                    ddl m} | j                  j                  }|| j                  j
                  }||ri n| j                         }|dk(  rB|s@t        j                  d| | j                  |t        |j                               |d       |j                  |      }|sg S g }|j                         D ]M  \  }}d}	|rt        |d	d      rd
}	n0|s.t        |dd
      r!t        |dg       }
|
sd
}	n|
D ]
  }||v sd
}	 n |	r	 ddlm} |j                         D ci c]  \  }}||d   |d   d }}} |||      }ddlm} t        t%        |      dd      }t        |dd      }|r||k7  r || ddddt        | dd            }|j'                  |      } || ddd|t        | dd            }|j)                  |      s+d}	t        j                  d| d|| j                  |d       |	s=|j/                  |       P |S c c}}w # t*        $ r?}t        j                  d| d| || j                  |t-        |      d       Y d}~cd}~ww xY w)a	  
        Determine which FSM transitions should be triggered based on model state.

        This method automatically discovers registered transitions for this entity
        and checks which ones should execute based on their trigger metadata.

        Args:
            is_creating: Whether this is a creation. If None, checks self._state.adding
            changed_fields: Dict of changed fields. If None, computes them.

        Override this method ONLY if you need custom transition logic beyond
        what the declarative triggers provide.

        Returns:
            List of transition names to execute (in order)

        Note:
            In most cases, you don't need to override this. Just register transitions
            with appropriate trigger metadata using @register_state_transition decorator.

        Example of custom override (if needed):
            def _determine_fsm_transitions(self, is_creating=None, changed_fields=None) -> list:
                # Get default transitions
                transitions = super()._determine_fsm_transitions(is_creating, changed_fields)

                # Add custom logic
                if self.some_complex_condition():
                    transitions.append('custom_transition')

                return transitions
        r   )transition_registryNprojectz!FSM: Determining transitions for )	entity_idr=   r>   changed_fields_detailextraF_triggers_on_createT_triggers_on_update_trigger_fields)TransitionContextr&   oldnewr=   r>   )BaseTransitionshould_executeorganization_id)entitycurrent_usercurrent_state_objectcurrent_statetarget_staterP   zFSM: Transition z! filtered out by should_execute())entity_typerB   transition_namez'FSM: Error checking should_execute for : )rV   rB   rW   error)fsm.registryr@   r2   
model_name_stateaddingr<   loggerdebugpklistkeysget_transitions_for_entityitemsr7   fsm.transitionsrI   rN   typeget_target_staterO   	Exceptionstrappend)r   r=   r>   r@   entity_nameregistered_transitionstransitions_to_executerW   transition_classrO   trigger_fieldsr9   rI   kvformatted_changed_fieldstemp_transitionrN   should_execute_methodbase_should_executeminimal_contextrU   contextes                           r   _determine_fsm_transitionsz/FsmHistoryStateModel._determine_fsm_transitions   s   @ 	5jj++ ++,,K !#.RD4L4L4NN )#KLL3K=A!%#.&*>+>+>+@&A-;	   "5!O!OP[!\%I!#1G1M1M1O c	?-O-"N w'79NPUV!% !W-=?TVZ%[!()9;Lb!Q & &*N "0 " N2-1N!" F B [iZnZnZp/qRVRSUV1Q4!3M0M/q,/q '7$/@X'O ?,3D4IK[]a,b)*1.BRTX*Y' -1FJ]1] +<#')-15*.)-,3D:KT,R+ (7'G'G'X #4#')-15*.)5,3D:KT,R#  /==gF-2N"LL"2?2CCd e3>157F'" ) * &--o>Gc	?J &%I 0rh !  LLA/ARRTUVTWX+6)-/>%(V	 !  s+   H H6B<HH	I5IIc                     i S )a<  
        Get data to pass to the FSM transition.

        Override in subclasses to provide transition-specific data that should
        be stored in the state record's context_data field.

        Returns:
            Dictionary of data to pass to transition

        Example:
            def _get_fsm_transition_data(self) -> Dict[str, Any]:
                return {
                    'project_id': self.project_id,
                    'completed_by_id': self.completed_by_id,
                    'annotation_count': self.annotations.count()
                }
        r   )r   s    r   _get_fsm_transition_dataz-FsmHistoryStateModel._get_fsm_transition_data6  s	    $ 	r   c                 J    t        | dd      ryddlm} |j                         S )a[  
        Check if FSM processing should be executed.

        Returns False if:
        - Feature flag is disabled (cached at request level)
        - Manually disabled via set_fsm_disabled() (for tests/bulk operations)
        - Explicitly skipped via instance attribute

        Returns:
            True if FSM should execute, False otherwise

        PERFORMANCE: Uses cached FSM enabled state from CurrentContext that was set
        once per request when user was initialized. This is a simple boolean check
        instead of repeated feature flag lookups and user authentication checks.
        	_skip_fsmFr   CurrentContext)r7   core.current_requestr   is_fsm_enabled)r   r   s     r   _should_execute_fsmz(FsmHistoryStateModel._should_execute_fsmJ  s'    " 4e, 	8,,..r   c                    ddl m} |j                  d|j                               }| j                  j
                  }|ri n| j                         }t        |    |i |}i | _	        | j                  j                  D ]I  }|j                  r|j                  rt        | |j                  d      | j                  |j                  <   K | xr | j!                         }	t"        j%                  d| j&                  j(                   d| j*                   d| d|	 | j&                  j(                  | j*                  ||	d	
       |s|	r	 | j-                  ||      }
t"        j%                  d| j&                  j(                   d| j*                   d|
 | j&                  j(                  | j*                  |
|d
       |
D ]  }	 | j/                  |||        	 |S |S # t0        $ rw}t"        j3                  d| d| j&                  j(                   d| j*                   d| j&                  j(                  | j*                  |t5        |      |dd       Y d}~d}~ww xY w# t0        $ rt}t"        j3                  d| j&                  j(                   d| j*                   d| j&                  j(                  | j*                  t5        |      |dd       Y d}~|S d}~ww xY w)a  
        Override save to trigger FSM transitions based on model changes.

        This method:
        1. Captures the current state (creating vs updating)
        2. Performs the actual database save
        3. Determines if an FSM transition is needed
        4. Executes the transition if needed
        5. Gracefully handles FSM errors without breaking the save

        Args:
            *args: Positional arguments passed to super().save()
            **kwargs: Keyword arguments passed to super().save()
                     Special kwarg: skip_fsm=True to bypass FSM processing

        Returns:
            Whatever super().save() returns
        r   r~   skip_fsmNzFSM check for  z: skip_fsm=z, should_execute=)rV   rB   r   rO   rD   rM   zFSM transitions determined for rX   )rV   rB   transitionsr=   )rW   r=   r>   zFSM transition z failed for zfsm.transition_failed_on_save)eventrV   rB   rW   rY   r=   T)rE   exc_infoz$FSM transition discovery failed for zfsm.transition_discovery_failed)r   rV   rB   rY   r=   )r   r   r+   is_fsm_disabledr\   r]   r<   r   saver   r2   r3   r5   r6   r7   r4   r   r^   r_   r   r
   r`   ry   _execute_fsm_transitionrh   rY   ri   )r   r   r   r   r   r=   r>   resultr9   rO   r   rW   rx   r   s                r   r   zFsmHistoryStateModel.saved  s   & 	8 ::j.*H*H*JK kk((  +0H0H0J t.v.
 !#ZZ&& 	VE  U%7%73:4PT3UD!!%--0	V &D$*B*B*DT^^445Qtwwi{8*Tefteuv#~~66!WW$"0	 	 	
 N+"==+ft=u5dnn6M6M5NaPTPWPWyXZ[fZgh'+~~'>'>%)WW'2'2	   (3 O44,;es 5 B v9 % -o->l4>>KbKbJccdeieleldmn)H/3~~/F/F-1WW3B),Q/:# &* %    :4>>;R;R:SSTUYU\U\T]^!B'+~~'>'>%)WW!$Q'2 "  
 
 sE   A9I  GI  	I&A-II  II   	K)A)KKrW   c                    ddl m} ddlm}  |       }|j	                         }|j                         }| j                         }	|	j                  ||j                         D 
ci c]  \  }
}|
|d   |d   d c}}
d       t        j                  d| j                  j                   d| j                  j                  | j                  |||r|j                  nd	|d
       	 |j                  | ||	||       t        j                  d| j                  j                   d| j                  j                  | j                  ||r|j                  nd	|d       y	c c}}
w # t         $ r  w xY w)aq  
        Execute an FSM transition.

        This method handles the actual transition execution, including:
        - Getting current context (user, org_id)
        - Preparing transition data
        - Calling the state manager

        Args:
            transition_name: Name of the registered transition to execute
            is_creating: Whether this is a new model creation
            changed_fields: Dict of changed fields (field_name -> (old, new))

        Note:
            This is only called after _should_execute_fsm() returns True,
            so CurrentContext should be available with a valid user.
        r   r~   )get_state_managerr&   rJ   rM   zExecuting FSM transition for zfsm.transition_executingN)r   rV   rB   rW   r=   user_idrP   rD   )rQ   rW   transition_datauserrP   z)FSM transition executed successfully for zfsm.transition_success)r   rV   rB   rW   r   rP   )r   r   fsm.state_managerr   get_userget_organization_idr{   updaterd   r^   infor   r
   r`   idexecute_transitionrh   )r   rW   r=   r>   r   r   StateManagerr   org_idr   rp   rq   s               r   r   z,FsmHistoryStateModel._execute_fsm_transition  st   $ 	87(* &&(335 779 	*M[MaMaMc"dTQ1ad1Q4&@#@"d	
 	+DNN,C,C+DE3#~~66!WW#2*&*477#) 	 	
	++ / / & ,  KK;DNN<S<S;TU5#'>>#:#:!%'6*.twwD'-  
7 #eL  		s   #E&A7E$ $E/)NN)r
   r   r   __doc__r   r   classmethodr   r'   r   ri   tupler<   boolr   ra   ry   r   r{   r   r   r   __classcell__)r   s   @r   r   r      s     D #  ",$T#u*%5 $Ld&d d&SW d&cg d&L$sCx. (/T /4dLJs J J_cdgindn_o Jr   r   )r   loggingtypingr   r   	django.dbr   	getLoggerr
   r^   Modelr   r   r   r   <module>r      s9      			8	$~6<< ~r   