"""
|
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.')
|