I need to implement a finite-state-machine in order to keep track of a few of my Django project models. I already have a similar app doing that but it's heavily coupled with the others apps models and not reusable in any way. So I decided to re-factor it.
After a few hours, this is what I came up with:
class StateMachine(models.Model):
name = models.CharField(max_length=50, help_text="Must be an unique name")
template = models.BooleanField(default=True)
current_state = models.ForeignKey('State', blank=True, null=True, related_name='current_state')
initial_state = models.ForeignKey('State', blank=True, null=True, related_name='initial_state')
def get_valid_actions(self):
return Action.objects.filter(machine=self, from_state=self.current_state)
def copy(self):
...
class State(models.Model):
name = models.CharField(max_length=50)
machine = models.ForeignKey(StateMachine)
def enter_hook(self, machine=None, action=None, state=None):
pass
def exit_hook(self, machine=None, action=None, state=None):
pass
def _copy(self, machine):
...
class Action(models.Model):
name = models.CharField(max_length=50)
machine = models.ForeignKey(StateMachine)
from_state = models.ForeignKey(State, related_name='from_state')
to_state = models.ForeignKey(State, blank=True, null=True, related_name='to_state')
def is_valid(self):
if self.machine.current_state == self.from_state:
return True
else:
return False
def act(self):
if self.is_valid():
self.from_state.exit_hook(machine=self.machine, action=self, state=self.from_state)
self.machine.current_state = self.to_state
self.machine.save()
self.to_state.enter_hook(machine=self.machine, action=self, state=self.to_state)
else:
raise ActionNotApplicable()
return self.machine
def _copy(self, machine):
...
I am happy with the results. It does what a state machine is expected to do and nothing else. In my models I am using it like this:
class SampleModel(models.Model):
machine = models.ForeignKey(StateMachine, null=True)
def setup_machine(self):
self.machine = StateMachine.objects.get(template=True, name='bla bla bla').copy()
self.save()
I basically create a "template" machine using the admin interface and then I run copy method which copies the state machine model and sets the template to False.
Now, here comes my questions :)
Is using a ForeignKey in the SampleModel the best way to attach the StateMachine to it? I've read about Generic Relations in Django but I've never used it before. Using it would be beneficial in my scenario?
I am trying to follow the "Do one thing and do it well" philosophy, but I've other business requirements to implement. For example, I need to send to send an e-mail to a specific group every time a state changes, and this group varies from state to state. The first solution I came up was to add an "email" field in the state model. But this would violate what this app proposes to do which is simply to keep track of a state machine. What would be the best way to solve this problem?
I also include some hook functions in the model so that someone could attach custom behavior later, is this the best way to do it? (I think Django was already a signal system)
Any other ideas/comments? I am new to this reusable apps thing :)
Thanks!