Skip to content

Instantly share code, notes, and snippets.

@ckinsey
Last active December 29, 2015 01:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ckinsey/7590678 to your computer and use it in GitHub Desktop.
Save ckinsey/7590678 to your computer and use it in GitHub Desktop.
STATUS_CANCELLED = 'cancelled'
STATUS_NEW = 'new'
STATUS_PENDING = 'pending'
STATUS_PAYMENT_DUE = 'payment-due'
STATUS_PAYMENT_PENDING = 'payment-pending'
STATUS_PAID = 'paid'
NEGOTIABLE_STATUS_CHOICES = (
(STATUS_CANCELLED, 'Cancelled'), # Cancelled, duh
(STATUS_NEW, 'New'), # Default state of request
(STATUS_PENDING, 'Pending'), # Negotiation in process
(STATUS_PAYMENT_DUE, 'Payment Due'), # Waiting for someone to lay down payment
(STATUS_PAYMENT_PENDING, 'Payment Pending'), # This is a "reserved" state, payment is recorded but not charged
(STATUS_PAID, 'Paid'), # This is a "reserved" state, payments is fulfilled.
)
# For each target state, define the valid source states that can transition to it
NEGOTIABLE_STATUS_MAP = {
STATUS_CANCELLED: [STATUS_PENDING, STATUS_NEW, STATUS_PAYMENT_DUE, STATUS_PAYMENT_PENDING, STATUS_PAID],
STATUS_NEW: [],
STATUS_PENDING: [STATUS_NEW],
STATUS_PAYMENT_DUE: [STATUS_PENDING],
STATUS_PAYMENT_PENDING: [STATUS_PAYMENT_DUE],
STATUS_PAID: [STATUS_PAYMENT_PENDING],
}
class StateMachineObject(models.Model):
status = models.CharField(max_length=32, choices=NEGOTIABLE_STATUS_CHOICES, default=STATUS_NEW)
def transition(self, target_status, **kwargs):
"""
Transitions an object from one state to another. Some transitions need specific contextual metadata (like
cancel w/ or w/o credit which can be passed in via kwargs for validation/side effects
"""
source_status = self.status
self._transition_valid(source_status, target_status, **kwargs)
# Go ahead and run any side effects of this transition
self._handle_side_effects(source_status, target_status, **kwargs)
# Finally, update the status
self.status = target_status
def _transition_valid(self, source_status, target_status, **kwargs):
"""
Checks first if a transition respects the defined workflow, and second validates that the instance's data is in
a valid state to make the transition
"""
if not source_status in NEGOTIABLE_STATUS_MAP[target_status]:
raise NotImplementedError('Transition from %s to %s not allowed' % (source_status, target_status))
# State specific validation rules
if target_status == STATUS_PAYMENT_PENDING:
if not self.get_payment_method():
raise ValueError("Cannot transition object %s to %s without a payment_method recorded" % (self, target_status))
# Cancellation validation
if target_status == STATUS_CANCELLED:
if source_status in [STATUS_PAYMENT_PENDING, STATUS_PAID]:
# We are cancelling an item that either has been paid or has a payment pending. We need to know if
# credit should be issued!
if not 'credit' in kwargs:
raise ValueError("Cannot transition object in status %s to %s without a 'credit' kwarg" %
(source_status, target_status))
def _handle_side_effects(self, source_status, target_status, **kwargs):
"""
Handles any side effects that take place when transitioning from one state to another. Probably should contain
logic that is base-class specific. Global side effects go here, and child class side effects should always
call this via super()
"""
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment