I'm going to cover a simple, but effective, utility for managing state and transitions (aka workflow). We often need to store the state (status) of a model and it should only be in one state at a time.
- Publishing (Draft->Approved->Published->Expired->Deleted)
- Payments
- Account Authorization (New->Active->Suspended->Deleted)
- Membership (Trial->Paid->Cancelled)
- Quality Assurance, Games
- Anything with a series of steps
Booleans for states
- is_new
- is_active
- is_published
- is_draft
- is_deleted
- is_paid
- is_member
- is_*
Mutually exclusive states ... sort of finite, but the number of states increases with each bool:
- 2 bools = 2^2 = 4 states
- 3 bools = 2^3 = 8 states
- etc (2^N)
Brittle and too many states to check.
- finite list of states
- one state at a time; the current state
- transition state by triggering event or condition
The behavior of state machines can be observed in many devices in modern society which perform a predetermined sequence of actions depending on a sequence of events with which they are presented.
CharField with defined choices
state = CharField(
default=1,
choices=[(1, "draft"), (2, "approved"), (3, "published")]
)
Define methods to change state:
def publish(self):
self.state = 3
email_sombody(self)
self.save()
def approve(self):
self.state = 2
self.save()
Better, but ...
- not enforced
- Can I go from draft to published, skipping approval?
- What happens if I publish something that's already published?
- repetitive
- side-effects mix with transition code
- Safe, verifiable transitions between states
- Conditions for the transition
- Clear side effects from state transitions
- DRY
- declarative transitions and conditions (via decorators)
- specialized field to contain state
https://github.com/kmmbvnr/django-fsm
P.S. RoR has similar apps too
- Specialized CharField
- Set
protected=True
- to prevent direct/accidental manipulation
- forces use of transition methods
- raises an AttributeError "Direct state modification is not allowed"
state = FSMField(
default=State.DRAFT,
verbose_name='Publication State',
choices=State.CHOICES,
protected=True,
)
(alternatives FSMIntegerField
, FSMKeyField
)
@transition(field=state, source=[State.APPROVED, State.EXPIRED],
target=State.PUBLISHED,
conditions=[can_display])
def publish(self):
'''
Publish the object.
'''
email_the_team()
update_sitemap()
busta_cache()
What does this get us?
- defined source and target states (valid transitions)
- a method to complete the transition and define side-effects
- a list of conditions (aside from state), that must be met for the transition to occur
./manage.py graph_transitions -o example-graph.png fsm_example.PublishableModel
Something a bit more complex:
https://github.com/gadventures/django-fsm-admin
- submit row
- logging history
- noting what's required to change state (messages)
https://github.com/gizmag/django-fsm-log
If you'd like your state transitions stored in something other than the admin history.
Not much out there. django-fsm has the most activity.
Craig Nagy @nagyman G Adventures - Software Engineering, eComm Mgr.
Would love to see some explicit documentation, i.e. what is
source
? what istarget
? How do I change the state?It wasn't clear that I still need to call
.save()
after calling the helper fn, or that the helper fn doesn't detect a state change if I directly set the value; it only provides a home for side effects + enforces restrictions on state change directions.