Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""
FSM Serializer Fields for Django Rest Framework.
 
Provides reusable DRF serializer fields for exposing FSM state in API responses.
These fields work seamlessly with the FSMStateQuerySetMixin annotations to prevent
N+1 queries.
 
Usage:
    from fsm.serializer_fields import FSMStateField
 
    class TaskSerializer(serializers.ModelSerializer):
        state = FSMStateField(read_only=True)
 
        class Meta:
            model = Task
            fields = ['id', 'data', 'state', ...]
 
Note:
    All state serialization functionality is guarded by TWO feature flags:
    1. 'fflag_feat_fit_568_finite_state_management' - Controls FSM background calculations
    2. 'fflag_feat_fit_710_fsm_state_fields' - Controls state field display in APIs
 
    When either flag is disabled, fields return None. This allows enabling FSM background
    work while keeping state fields hidden during incremental rollout and testing.
"""
 
from core.current_request import CurrentContext
from core.feature_flags import flag_set
from fsm.state_manager import StateManager
from rest_framework import serializers
 
 
class FSMStateField(serializers.ReadOnlyField):
    """
    Read-only DRF field for exposing FSM state.
 
    This field automatically uses the `state` or `current_state` annotation if present
    (preventing N+1 queries), or falls back to querying the state manager
    if the annotation is missing.
 
    Key features:
    - Works with annotated querysets (no N+1 queries)
    - Falls back to StateManager for single object retrievals
    - Always read-only (state changes through transitions only)
    - Returns None if FSM is disabled or no state exists
 
    Example with annotations (optimal):
        # In your viewset
        def get_queryset(self):
            return Task.objects.all().with_state()
 
        # In your serializer
        class TaskSerializer(serializers.ModelSerializer):
            state = FSMStateField()
 
            class Meta:
                model = Task
                fields = ['id', 'data', 'state']
 
        # Result: No N+1 queries, state comes from annotation
 
    Example without annotations (fallback):
        # Direct object retrieval
        task = Task.objects.get(id=123)
        serializer = TaskSerializer(task)
 
        # Result: Calls StateManager.get_current_state_value()
        # Still efficient due to StateManager caching
    """
 
    def __init__(self, **kwargs):
        # Set source='*' to pass the entire object instance to to_representation()
        # instead of a specific attribute, since we check multiple possible attributes
        kwargs.setdefault('source', '*')
        super().__init__(**kwargs)
 
    def to_representation(self, instance):
        """
        Serialize the FSM state to a string.
 
        Args:
            instance: The model instance being serialized
 
        Returns:
            str or None: The current state value (None if either feature flag disabled)
        """
        # Check both feature flags (works for both core and enterprise)
        # 1. General FSM functionality (background calculations)
        # 2. State field display control (API exposure)
        user = CurrentContext.get_user()
        if not (
            flag_set('fflag_feat_fit_568_finite_state_management', user=user)
            and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
        ):
            return None
 
        if instance is None:
            return None
 
        # Check if the instance has a state annotation
        # This can come from Data Manager's annotate_state() or FSMStateQuerySetMixin.with_state()
        if hasattr(instance, 'state'):
            # Use the annotated value (no additional query)
            return instance.state
        elif hasattr(instance, 'current_state'):
            # Fallback to current_state annotation from FSMStateQuerySetMixin
            return instance.current_state
 
        # Fallback: Query the state manager
        # This happens when the queryset wasn't annotated
        # StateManager has its own caching, so this is still efficient
        try:
            return StateManager.get_current_state_value(instance)
        except Exception:
            # If FSM is disabled or state model not found, return None
            return None
 
    def to_internal_value(self, data):
        """
        This field is read-only, so this should never be called.
        """
        raise NotImplementedError('FSMStateField is read-only. Use transitions to change state.')