State machines and django

When working on a django project, there are a number of must-have third-party libraries if you don't want to endlessly reinvent the wheel. A tool for debugging sql queries (debug-toolbar, silk, --print-sql from django-extensions), something for storing tree structures, periodic / deferred tasks (by the way, uswgi has a cron-like interface . EAV is still needed , although it can often be replaced by jsonfield. And one of these extremely useful things, but for some reason less often discussed on the net is FSM. For some reason I don't often come across them in someone else's code.





Almost every record in the database has some state. For example, for a comment, it can be - published / deleted / deleted by a moderator. To order in a store - issued / paid / delivered / returned, etc. Moreover, the transition from one state to another is often smeared over the code and there is business logic in it, which must be abundantly covered with tests (you still have to, but you can avoid testing elementary things, for example, that an order can go to the "refund" state only after being in "paid" condition.





It would be quite logical to describe such transitions more declaratively and in one place. Along with the necessary logic and access verification.





Here is a sample code from django-fsm library tests





class BlogPost(models.Model):
    """
    Test workflow
    """
    state = FSMField(default='new', protected=True)

    def can_restore(self, user):
        return user.is_superuser or user.is_staff

    @transition(field=state, source='new', target='published',
                on_error='failed', permission='testapp.can_publish_post')
    def publish(self):
        pass

    @transition(field=state, source='published')
    def notify_all(self):
        pass

    @transition(field=state, source='published', target='hidden', on_error='failed',)
    def hide(self):
        pass

    @transition(
        field=state,
        source='new',
        target='removed',
        on_error='failed',
        permission=lambda self, u: u.has_perm('testapp.can_remove_post'))
    def remove(self):
        raise Exception('No rights to delete %s' % self)

    @transition(field=state, source='new', target='restored',
                on_error='failed', permission=can_restore)
    def restore(self):
        pass

    @transition(field=state, source=['published', 'hidden'], target='stolen')
    def steal(self):
        pass

    @transition(field=state, source='*', target='moderated')
    def moderate(self):
        pass

    class Meta:
        permissions = [
            ('can_publish_post', 'Can publish post'),
            ('can_remove_post', 'Can remove post'),
        ]
      
      



This is great for rest api, among other things. We can create endpoints for transitions between states automatically. For example, the request / orders / id / cancel looks like a perfectly logical action for a viewset. And we already have the necessary information to verify access! And also for buttons in the admin panel, and the ability to draw beautiful charts with workflow :) There are even visual workflow editors, i.e. non-programmers can describe business processes





The more declarative and generic code we write, the more reliable it is. Less code, less duplication, fewer bugs. Testing is partially shifted to the author of the library, and you can focus on the business logic unique to the project.








All Articles