Package: django-logic
Django Logic is a workflow framework allowing developers to implement the business logic via pure functions.
It's designed based onFinite-State-Machine (FSM)principles.
Therefore, it needs to define astatefield for a model's object. Every change of thestateis performed by a
transition and every transition could be grouped into a process. Also, you can define some side-effects that will be
executed during the transition from one state to another and callbacks that will be run after.
This concept provides you a place for the business logic, rather than splitting it across the views, models, forms,
serializers or even worse, in templates.DefinitionsTransition- class changes a state of an object from one to another. It also contains its own conditions,
permissions, side-effects, callbacks, and failure callbacks.Action- in contrast with the transition, the action does not change the state.
But it contains its own conditions, permissions, side-effects, callbacks, and failure callbacks.Side-effects- class defines a set of functions that executing within one particular transition
before reaching thetargetstate. During the execution, the state changes to thein_progressstate.
In case, if one of the functions interrupts the execution, then it changes to thefailedstate.Callbacks- class defines a set of functions that executing within one particular transition
after reaching thetargetstate. In case, if one of the functions interrupts the execution, it will log
an exception and the execution will be stopped (without changing the state to failed).Failure callbacks- class defines a set of functions that executing within one particular
transition in case if one of the side-effects has been failed to execute.Conditions- class defines a set of functions which receives an object
and returnTrueorFalsebased on one particular requirement.Permissions- class defines a set of functions which receives an object and user, then returnsTrueorFalsebased on given permissions.Process- class defines a set of transitions with some common conditions and permissions.
It also accepts nested processes that allow building the hierarchy.InstallationUse the package managerpipto install Django-Logic.pipinstalldjango-logicUsageAdd to INSTALLED_APPSINSTALLED_APPS=(...'django_logic',...)Define a process class with some transitions.fromdjango_logicimportProcessasBaseProcess,Transition,ProcessManager,ActionclassProcess(BaseProcess):states=(('draft','Draft'),('approved','Approved'),('void','Void'),)transitions=[Transition(action_name='approve',sources=['draft'],target='approved'),Transition(action_name='void',sources=['draft','approved'],target='void'),Action(action_name='update',side_effects=[update_data]),]Define a binding class and status field.ApprovalProcess=ProcessManager.bind_state_fields(status=Process)You can call the status or state field as you wish, just make sure it's defined in the model.
Furthermore, it supports several state fields. For example:ApprovalProcess=ProcessManager.bind_state_fields(my_status=ApprovalProcess,my_state=LockProcess)classInvoice(ApprovalProcess,models.Model):my_status=models.CharField(choices=ApprovalProcess.states,default='draft',max_length=16,blank=True)my_state=models.CharField(choices=ApprovalProcess.states,default='open',max_length=16,blank=True)Bind the process with a model by inheriting the binding class.fromdjango.dbimportmodelsfrom.processimportApprovalProcessclassInvoice(ApprovalProcess,models.Model):status=models.CharField(choices=ApprovalProcess.states,default='draft',max_length=16,blank=True)Advance your process with conditions, side-effects, and callbacks into the processclassProcess(BaseProcess):permissions=[is_accountant,]states=(('draft','Draft'),('approved','Approved'),('void','Void'),)transitions=[Transition(action_name='approve',sources=['draft'],target='approved',conditions=[is_customer_active,]side_effects=[generate_pdf_invoice,],callbacks=[send_approved_invoice_email_to_accountant,]),Transition(action_name='void',callbakcs=[send_void_invoice_email_to_accountant],sources=['approved'],target='void'),Action(action_name='update',side_effects=[update_data],),]This approval process defines the business logic where:The user who performs the action must have accountant role (permission).It shouldn't be possible to invoice inactive customers (condition).Once the invoice record is approved, it should generate a PDF file and send it to
an accountant via email. (side-effects  and callbacks)If the invoice voided it needs to notify the accountant about that.
As you see, these business requirements should not know about each other. Furthermore, it gives a simple way
to test every function separately as Django-Logic takes care of connection them into the business process.Execute in the code:frominvoices.modelsimportInvoicedefapprove_view(request,pk):invoice=Invoice.objects.get(pk=pk)invoice.process.approve(user=request.user)If you want to override the value of the state field, it must be done explicitly. For example:Invoice.objects.filter(status='draft').update(status='open')# orinvoice=Invoice.objects.get(pk=pk)invoice.status='open'invoice.save(update_fields=['status'])Save withoutupdate_fieldswon't update the value of the state field in order to protect the data from corrupting.Error handling:fromdjango_logic.exceptionsimportTransitionNotAllowedtry:invoice.process.approve()exceptTransitionNotAllowed:logger.error('Approve is not allowed')Display processDrawing a process with the following elements:Process - a transparent rectangleTransition - a grey rectangleState - a transparent ellipseProcess' conditions and permissions are defined inside of related process as a transparent diamondTransition' conditions and permissions are defined inside of related transition's process as a grey diamondFrom this diagram you can visually check that the following the business requirements have been implemented properly:Personnel involved: User and StaffLock has to be available before any actions taken. It's  defined by a conditionis_lock_available.User is able to lock and unlock an available locker.Staff is able to lock, unlock and put a locker under maintenance if such was planned.Drawing such diagram requires installing graphviz.pipinstallgraphvizRun this commandfromdjango_logic.displayimport*fromdemo.processimportLockerProcessdisplay_process(LockerProcess,state='open',skip_main_process=True)Django-Logic vs Django FSMDjango FSMis a parent package of Django-Logic.
It's been used in production for many years until the number of new ideas and requirements swamped us.
Therefore, it's been decided to implement these ideas under a new package. For example, supporting Processes or
background transitions which were implemented underDjango-Logic-Celery.
Finally, we want to provide a standard way on where to put the business logic in Django by using Django-Logic.ContributingPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.Please make sure to update tests as appropriate.LicenseMITProject statusUnder development