Last active
December 29, 2015 01:09
-
-
Save ckinsey/7590678 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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