Package: django-fsa
Django friendly finite state machine supportThis is a friendly fork which aims to ensure that the tests are passing with
the new python and django versions, the original django-fsm was written by
Mikhail Podgurskiy (kmmbvnr@gmail.com),https://github.com/viewflow/django-fsmdjango-fsm adds simple declarative states management for django models.If you need parallel task execution, view and background task code reuse
over different flows - check my new project django-viewflow:https://github.com/viewflow/viewflowInstead of adding some state field to a django model, and managing its
values by hand, you could use FSMState field and mark model methods with
thetransitiondecorator. Your method could contain the side-effects
of the state change.Nice introduction is available here:https://gist.github.com/Nagyman/9502133You may also take a look at django-fsm-admin project containing a mixin
and template tags to integrate django-fsm state transitions into the
django admin.https://github.com/gadventures/django-fsm-adminTransition logging support could be achived with help of django-fsm-log
packagehttps://github.com/gizmag/django-fsm-logFSM really helps to structure the code, especially when a new developer
comes to the project. FSM is most effective when you use it for some
sequential steps.Installation$pipinstalldjango-fsmOr, for the latest git version$pipinstall-egit://github.com/kmmbvnr/django-fsm.git#egg=django-fsmThe library has full Python 3 supportUsageAdd FSMState field to your modelfromdjango_fsmimportFSMField,transitionclassBlogPost(models.Model):state=FSMField(default='new')Use thetransitiondecorator to annotate model methods@transition(field=state,source='new',target='published')defpublish(self):"""
    This function may contain side-effects,
    like updating caches, notifying users, etc.
    The return value will be discarded.
    """sourceparameter accepts a list of states, or an individual state.
You can use*for source, to allow switching totargetfrom any
state. Thefieldparameter accepts both a string attribute name or an
actual field instance.If calling publish() succeeds without raising an exception, the state
field will be changed, but not written to the database.fromdjango_fsmimportcan_proceeddefpublish_view(request,post_id):post=get_object__or_404(BlogPost,pk=post_id)ifnotcan_proceed(post.publish):raisePermissionDeniedpost.publish()post.save()returnredirect('/')If some conditions are required to be met before changing the state, use
theconditionsargument totransition.conditionsmust be a
list of functions taking one argument, the model instance. The function
must return eitherTrueorFalseor a value that evaluates toTrueorFalse. If all functions returnTrue, all conditions
are considered to be met and the transition is allowed to happen. If one
of the functions returnsFalse, the transition will not happen.
These functions should not have any side effects.You can use ordinary functionsdefcan_publish(instance):# No publishing after 17 hoursifdatetime.datetime.now().hour>17:returnFalsereturnTrueOr model methodsdefcan_destroy(self):returnself.is_under_investigation()Use the conditions like this:@transition(field=state,source='new',target='published',conditions=[can_publish])defpublish(self):"""
    Side effects galore
    """@transition(field=state,source='*',target='destroyed',conditions=[can_destroy])defdestroy(self):"""
    Side effects galore
    """You could instantiate a field with protected=True option, that prevents
direct state field modification.classBlogPost(models.Model):state=FSMField(default='new',protected=True)model=BlogPost()model.state='invalid'# Raises AttributeErrorNote that callingrefresh_from_dbon a model instance with a protected FSMField will cause an exception.targettargetstate parameter could point to a specific state ordjango_fsm.Stateimplementationfromdjango_fsmimportFSMField,transition,RETURN_VALUE,GET_STATE@transition(field=state,source='*',target=RETURN_VALUE('for_moderators','published'))defpublish(self,is_public=False):return'for_moderators'ifis_publicelse'published'@transition(field=state,source='for_moderators',target=GET_STATE(lambdaself,allowed:'published'ifallowedelse'rejected',states=['published','rejected']))defmoderate(self,allowed):self.allowed=allowedcustompropertiesCustom properties can be added by providing a dictionary to thecustomkeyword on thetransitiondecorator.@transition(field=state,source='*',target='onhold',custom=dict(verbose='Hold for legal reasons'))deflegal_hold(self):"""
    Side effects galore
    """on_errorstateIn case of transition method would raise exception, you can provide
specific target state@transition(field=state,source='new',target='published',on_error='failed')defpublish(self):"""
   Some exception could happen here
   """state_choicesInstead of passing two elements listchoicesyou could use three
elementsstate_choices, the last element states for string reference
to model proxy class.Base class instance would be dynamically changed to corresponding Proxy
class instance, depending on the state. Even for queryset results, you
will get Proxy class instances, even if QuerySet executed on base class.Check thetest
casefor example usage. Or read aboutimplementation
internalsPermissionsIt is common to have permissions attached to each model transition.django-fsmhandles this withpermissionkeyword on thetransitiondecorator.permissionaccepts a permission string, or
callable that expectsinstanceanduserarguments and returns
True if user can perform the transition.@transition(field=state,source='*',target='publish',permission=lambdainstance,user:notuser.has_perm('myapp.can_make_mistakes'))defpublish(self):pass@transition(field=state,source='*',target='publish',permission='myapp.can_remove_post')defremove(self):passYou can check permission withhas_transition_permissionmethodfromdjango_fsmimporthas_transition_permdefpublish_view(request,post_id):post=get_object_or_404(BlogPost,pk=post_id)ifnothas_transition_perm(post.publish,request.user):raisePermissionDeniedpost.publish()post.save()returnredirect('/')Model methodsget_all_FIELD_transitionsEnumerates all declared transitionsget_available_FIELD_transitionsReturns all transitions data
available in current stateget_available_user_FIELD_transitionsEnumerates all transitions data
available in current state for provided userForeign Key constraints supportIf you store the states in the db table you could use FSMKeyField to
ensure Foreign Key database integrity.In your model :classDbState(models.Model):id=models.CharField(primary_key=True,max_length=50)label=models.CharField(max_length=255)def__unicode__(self):returnself.labelclassBlogPost(models.Model):state=FSMKeyField(DbState,default='new')@transition(field=state,source='new',target='published')defpublish(self):passIn your fixtures/initial_data.json :[{"pk":"new","model":"myapp.dbstate","fields":{"label":"_NEW_"}},{"pk":"published","model":"myapp.dbstate","fields":{"label":"_PUBLISHED_"}}]Note : source and target parameters in @transition decorator use pk
values of DBState model as names, even if field “real” name is used,
without _id postfix, as field parameter.Integer Field supportYou can also useFSMIntegerField. This is handy when you want to use
enum style constants.classBlogPostStateEnum(object):NEW=10PUBLISHED=20HIDDEN=30classBlogPostWithIntegerField(models.Model):state=FSMIntegerField(default=BlogPostStateEnum.NEW)@transition(field=state,source=BlogPostStateEnum.NEW,target=BlogPostStateEnum.PUBLISHED)defpublish(self):passSignalsdjango_fsm.signals.pre_transitionanddjango_fsm.signals.post_transitionare called before and after
allowed transition. No signals on invalid transition are called.Arguments sent with these signals:senderThe model class.instanceThe actual instance being proceednameTransition namesourceSource model statetargetTarget model stateOptimistic lockingdjango-fsmprovides optimistic locking mixin, to avoid concurrent
model state changes. If model state was changed in databasedjango_fsm.ConcurrentTransitionexception would be raised on
model.save()fromdjango_fsmimportFSMField,ConcurrentTransitionMixinclassBlogPost(ConcurrentTransitionMixin,models.Model):state=FSMField(default='new')For guaranteed protection against race conditions caused by concurrently
executed transitions, make sure:Your transitions do not have any side effects except for changes in the database,You always run the save() method on the object withindjango.db.transaction.atomic()block.Following these recommendations, you can rely on
ConcurrentTransitionMixin to cause a rollback of all the changes that
have been executed in an inconsistent (out of sync) state, thus
practically negating their effect.Drawing transitionsRenders a graphical overview of your models states transitionsYou needpip installgraphviz>=0.4library and adddjango_fsmto
yourINSTALLED_APPS:INSTALLED_APPS=(...'django_fsm',...)# Create a dot file$./manage.pygraph_transitions>transitions.dot# Create a PNG image file only for specific model$./manage.pygraph_transitions-oblog_transitions.pngmyapp.BlogChangelogdjango-fsm 2.8.0 2019-12-08Add compatibility tests with Django 3.0Remove the dependency with django-guardians in the testschange some django deprecated functionsdjango-fsm 2.7.2 2019-05-01Add compatibility test with python 3.8Add compatibility test with Django 2.2
- Refactor the models in tests, a migrationdjango-fsm 2.7.1 2019-03-04Delivery on Pypidjango-fsm 2.7.0 2018-12-14Fix the travis testsAdd compatibility test with python 3.7, remove 2.6 and 3.3Add compatibility test with Django 2.0 and 2.1, remove 1.6, 1.8, 1.9 and 1.10django-fsm 2.6.0 2017-06-08Fix django 1.11 compatibilityFix TypeError ingraph_transitionscommand when using django’s lazy translationsdjango-fsm 2.5.0 2017-03-04graph_transition command fix for django 1.10graph_transition command supports GET_STATE targetssignal data extended with method args/kwargs and fieldsets allowed to be passed to the transition decoratordjango-fsm 2.4.0 2016-05-14graph_transition commnad now works with multiple  FSM’s per modelAdd ability to set target state from transition return value or callabledjango-fsm 2.3.0 2015-10-15Add source state shortcut ‘+’ to specify transitions from all states except the targetAdd object-level permission checksFix translated labels for graph of FSMIntegerFieldFix multiple signals for several transition decoratorsdjango-fsm 2.2.1 2015-04-27Improved exception message for unmet transition conditions.Don’t send post transition signal in case of no state changes on
exceptionAllow empty string as correct state valueImproved graphviz fsm visualisationClean django 1.8 warningsdjango-fsm 2.2.0 2014-09-03Support forclass
substitutionto proxy classes depending on the stateAdded ConcurrentTransitionMixin with optimistic locking supportDefault db_index=True for FSMIntegerField removedGraph transition code migrated to new graphviz library with python 3
supportAbility to change state on transition exceptiondjango-fsm 2.1.0 2014-05-15Support for attaching permission checks on model transitionsdjango-fsm 2.0.0 2014-03-15Backward incompatible releaseAll public code import moved directly to django_fsm packageCorrect support for several @transitions decorator with different
source states and conditions on same methodsave parameter from transition decorator removedget_available_FIELD_transitions return Transition data object
instead of tupleModels got get_available_FIELD_transitions, even if field
specified as string referenceNew get_all_FIELD_transitions method contributed to classdjango-fsm 1.6.0 2014-03-15FSMIntegerField and FSMKeyField supportdjango-fsm 1.5.1 2014-01-04Ad-hoc support for state fields from proxy and inherited modelsdjango-fsm 1.5.0 2013-09-17Python 3 compatibilitydjango-fsm 1.4.0 2011-12-21Add graph_transition command for drawing state transition picturedjango-fsm 1.3.0 2011-07-28Add direct field modification protectiondjango-fsm 1.2.0 2011-03-23Add pre_transition and post_transition signalsdjango-fsm 1.1.0 2011-02-22Add support for transition conditionsAllow multiple FSMField in one modelContribute get_available_FIELD_transitions for model classdjango-fsm 1.0.0 2010-10-12Initial public releasecopyright (c) 2010 Mikhail PodgurskiyPermission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.