0
# Transition Management
1
2
Decorator for marking methods as state transitions with comprehensive configuration options and utility functions for checking transition availability and permissions.
3
4
## Capabilities
5
6
### Transition Decorator
7
8
The core decorator that marks model methods as state transitions, enabling declarative finite state machine behavior.
9
10
```python { .api }
11
def transition(field,
12
source="*",
13
target=None,
14
on_error=None,
15
conditions=[],
16
permission=None,
17
custom={}):
18
"""
19
Method decorator to mark allowed state transitions.
20
21
Parameters:
22
- field: FSM field instance or field name string
23
- source: Source state(s) - string, list, tuple, set, "*" (any), or "+" (any except target)
24
- target: Target state, State instance (GET_STATE, RETURN_VALUE), or None for validation-only
25
- on_error: State to transition to if method raises exception
26
- conditions: List of functions that must return True for transition to proceed
27
- permission: Permission string or callable for authorization checking
28
- custom: Dictionary of custom metadata for the transition
29
30
Returns:
31
Decorated method that triggers state transitions when called
32
"""
33
```
34
35
### Basic Transition Usage
36
37
Simple state transitions with single source and target states:
38
39
```python
40
from django.db import models
41
from django_fsm import FSMField, transition
42
43
class Order(models.Model):
44
state = FSMField(default='pending')
45
46
@transition(field=state, source='pending', target='confirmed')
47
def confirm(self):
48
# Business logic here
49
self.confirmed_at = timezone.now()
50
51
@transition(field=state, source='confirmed', target='shipped')
52
def ship(self):
53
# Generate tracking number, notify customer, etc.
54
pass
55
```
56
57
### Multiple Source States
58
59
Transitions can be triggered from multiple source states:
60
61
```python
62
class BlogPost(models.Model):
63
state = FSMField(default='draft')
64
65
@transition(field=state, source=['draft', 'review'], target='published')
66
def publish(self):
67
self.published_at = timezone.now()
68
69
@transition(field=state, source=['draft', 'published'], target='archived')
70
def archive(self):
71
pass
72
```
73
74
### Wildcard Source States
75
76
Use special source values for flexible transitions:
77
78
```python
79
class Document(models.Model):
80
state = FSMField(default='new')
81
82
# Can be called from any state
83
@transition(field=state, source='*', target='error')
84
def mark_error(self):
85
pass
86
87
# Can be called from any state except 'deleted'
88
@transition(field=state, source='+', target='deleted')
89
def delete(self):
90
pass
91
```
92
93
### Conditional Transitions
94
95
Add conditions that must be met before transition can proceed:
96
97
```python
98
def can_publish(instance):
99
return instance.content and instance.title
100
101
def has_approval(instance):
102
return instance.approved_by is not None
103
104
class Article(models.Model):
105
state = FSMField(default='draft')
106
content = models.TextField()
107
title = models.CharField(max_length=200)
108
approved_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
109
110
@transition(
111
field=state,
112
source='draft',
113
target='published',
114
conditions=[can_publish, has_approval]
115
)
116
def publish(self):
117
self.published_at = timezone.now()
118
```
119
120
### Permission-Based Transitions
121
122
Control access to transitions using Django's permission system:
123
124
```python
125
class Document(models.Model):
126
state = FSMField(default='draft')
127
128
# String permission
129
@transition(
130
field=state,
131
source='review',
132
target='approved',
133
permission='myapp.can_approve_document'
134
)
135
def approve(self):
136
pass
137
138
# Callable permission
139
def can_reject(instance, user):
140
return user.is_staff or user == instance.author
141
142
@transition(
143
field=state,
144
source='review',
145
target='rejected',
146
permission=can_reject
147
)
148
def reject(self):
149
pass
150
```
151
152
### Error Handling in Transitions
153
154
Specify fallback states when transitions raise exceptions:
155
156
```python
157
class PaymentOrder(models.Model):
158
state = FSMField(default='pending')
159
160
@transition(
161
field=state,
162
source='pending',
163
target='paid',
164
on_error='failed'
165
)
166
def process_payment(self):
167
# If this raises an exception, state becomes 'failed'
168
result = payment_gateway.charge(self.amount)
169
if not result.success:
170
raise PaymentException("Payment failed")
171
```
172
173
### Validation-Only Transitions
174
175
Set target to None to validate state without changing it:
176
177
```python
178
class Contract(models.Model):
179
state = FSMField(default='draft')
180
181
@transition(field=state, source='draft', target=None)
182
def validate_draft(self):
183
# Perform validation logic
184
# State remains 'draft' if successful
185
if not self.is_valid():
186
raise ValidationError("Contract is invalid")
187
```
188
189
### Custom Transition Metadata
190
191
Store additional metadata with transitions:
192
193
```python
194
class Workflow(models.Model):
195
state = FSMField(default='new')
196
197
@transition(
198
field=state,
199
source='new',
200
target='processing',
201
custom={
202
'priority': 'high',
203
'requires_notification': True,
204
'estimated_time': 300
205
}
206
)
207
def start_processing(self):
208
pass
209
```
210
211
## Transition Objects
212
213
### Transition Class
214
215
The Transition class represents individual state transitions and provides metadata and permission checking capabilities.
216
217
```python { .api }
218
class Transition(object):
219
def __init__(self, method, source, target, on_error, conditions, permission, custom):
220
"""
221
Represents a single state machine transition.
222
223
Parameters:
224
- method: The transition method
225
- source: Source state for the transition
226
- target: Target state for the transition
227
- on_error: Error state (optional)
228
- conditions: List of condition functions
229
- permission: Permission string or callable
230
- custom: Custom metadata dictionary
231
232
Attributes:
233
- name: Name of the transition method
234
- source: Source state
235
- target: Target state
236
- on_error: Error fallback state
237
- conditions: Transition conditions
238
- permission: Permission configuration
239
- custom: Custom transition metadata
240
"""
241
242
@property
243
def name(self):
244
"""Return the name of the transition method."""
245
246
def has_perm(self, instance, user):
247
"""
248
Check if user has permission to execute this transition.
249
250
Parameters:
251
- instance: Model instance
252
- user: User to check permissions for
253
254
Returns:
255
bool: True if user has permission
256
"""
257
```
258
259
Transition objects are created automatically by the @transition decorator and can be accessed through various utility functions:
260
261
```python
262
from django_fsm import FSMField, transition
263
264
class Order(models.Model):
265
state = FSMField(default='pending')
266
267
@transition(field=state, source='pending', target='confirmed')
268
def confirm(self):
269
pass
270
271
# Access transition objects
272
order = Order()
273
transitions = order.get_available_state_transitions()
274
for transition in transitions:
275
print(f"Transition: {transition.name}")
276
print(f"From {transition.source} to {transition.target}")
277
print(f"Custom metadata: {transition.custom}")
278
```
279
280
### FSMMeta Class
281
282
The FSMMeta class manages transition metadata for individual methods, storing all configured transitions and their conditions.
283
284
```python { .api }
285
class FSMMeta(object):
286
def __init__(self, field, method):
287
"""
288
Initialize FSM metadata for a transition method.
289
290
Parameters:
291
- field: FSM field instance
292
- method: Transition method
293
294
Attributes:
295
- field: Associated FSM field
296
- transitions: Dictionary mapping source states to Transition objects
297
"""
298
299
def add_transition(self, method, source, target, on_error=None, conditions=[], permission=None, custom={}):
300
"""
301
Add a transition configuration to this method.
302
303
Parameters:
304
- method: Transition method
305
- source: Source state
306
- target: Target state
307
- on_error: Error state (optional)
308
- conditions: List of condition functions
309
- permission: Permission string or callable
310
- custom: Custom metadata dictionary
311
"""
312
313
def has_transition(self, state):
314
"""
315
Check if any transition exists from the given state.
316
317
Parameters:
318
- state: Current state to check
319
320
Returns:
321
bool: True if transition is possible
322
"""
323
324
def conditions_met(self, instance, state):
325
"""
326
Check if all transition conditions are satisfied.
327
328
Parameters:
329
- instance: Model instance
330
- state: Current state
331
332
Returns:
333
bool: True if all conditions are met
334
"""
335
336
def has_transition_perm(self, instance, state, user):
337
"""
338
Check if user has permission for transition from given state.
339
340
Parameters:
341
- instance: Model instance
342
- state: Current state
343
- user: User to check permissions for
344
345
Returns:
346
bool: True if user has permission
347
"""
348
```
349
350
Access FSMMeta through transition methods:
351
352
```python
353
# Access FSM metadata
354
order = Order()
355
meta = order.confirm._django_fsm # FSMMeta instance
356
357
# Check transition availability
358
if meta.has_transition(order.state):
359
print("Confirm transition is available")
360
361
# Check conditions
362
if meta.conditions_met(order, order.state):
363
print("All conditions are satisfied")
364
```
365
366
## Transition Utility Functions
367
368
### can_proceed
369
370
Check if a transition method can be called from the current state:
371
372
```python { .api }
373
def can_proceed(bound_method, check_conditions=True):
374
"""
375
Returns True if model state allows calling the bound transition method.
376
377
Parameters:
378
- bound_method: Bound method with @transition decorator
379
- check_conditions: Whether to check transition conditions (default: True)
380
381
Returns:
382
bool: True if transition is allowed
383
384
Raises:
385
TypeError: If method is not a transition
386
"""
387
```
388
389
Usage example:
390
391
```python
392
from django_fsm import can_proceed
393
394
order = Order.objects.get(pk=1)
395
396
# Check without conditions
397
if can_proceed(order.ship, check_conditions=False):
398
print("Ship transition is possible from current state")
399
400
# Check with conditions
401
if can_proceed(order.ship):
402
order.ship()
403
order.save()
404
else:
405
print("Cannot ship order yet")
406
```
407
408
### has_transition_perm
409
410
Check if a user has permission to execute a transition:
411
412
```python { .api }
413
def has_transition_perm(bound_method, user):
414
"""
415
Returns True if model state allows calling method and user has permission.
416
417
Parameters:
418
- bound_method: Bound method with @transition decorator
419
- user: User instance to check permissions for
420
421
Returns:
422
bool: True if user can execute transition
423
424
Raises:
425
TypeError: If method is not a transition
426
"""
427
```
428
429
Usage example:
430
431
```python
432
from django_fsm import has_transition_perm
433
434
def process_approval(request, document_id):
435
document = Document.objects.get(pk=document_id)
436
437
if has_transition_perm(document.approve, request.user):
438
document.approve()
439
document.save()
440
return redirect('success')
441
else:
442
return HttpResponseForbidden("You don't have permission to approve")
443
```
444
445
## Advanced Transition Patterns
446
447
### Multi-Field State Machines
448
449
Different fields can have their own independent state machines:
450
451
```python
452
class Order(models.Model):
453
payment_state = FSMField(default='unpaid')
454
fulfillment_state = FSMField(default='pending')
455
456
@transition(field=payment_state, source='unpaid', target='paid')
457
def process_payment(self):
458
pass
459
460
@transition(field=fulfillment_state, source='pending', target='shipped')
461
def ship_order(self):
462
pass
463
```
464
465
### Cascading Transitions
466
467
Transitions can trigger other transitions:
468
469
```python
470
class Order(models.Model):
471
state = FSMField(default='new')
472
473
@transition(field=state, source='new', target='processing')
474
def start_processing(self):
475
pass
476
477
@transition(field=state, source='processing', target='completed')
478
def complete(self):
479
# Automatically trigger another action
480
self.notify_customer()
481
482
def notify_customer(self):
483
# Send notification logic
484
pass
485
```
486
487
### State-Dependent Method Behavior
488
489
Use current state to determine method behavior:
490
491
```python
492
class Document(models.Model):
493
state = FSMField(default='draft')
494
495
def save(self, *args, **kwargs):
496
if self.state == 'published':
497
# Extra validation for published documents
498
self.full_clean()
499
super().save(*args, **kwargs)
500
501
@transition(field=state, source='draft', target='published')
502
def publish(self):
503
self.published_at = timezone.now()
504
```