
[](https://travis-ci.org/Borderless360/django-logic)
[](https://coveralls.io/github/Borderless360/django-logic?branch=master)
Django Logic is a workflow framework allowing developers to implement the business logic via pure functions.
It's designed based on [Finite-State-Machine (FSM)](https://en.wikipedia.org/wiki/Finite-state_machine) principles.
Therefore, it needs to define a `state` field for a model's object. Every change of the `state` is 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.
## Definitions
- **Transition** - 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 the `target` state. During the execution, the state changes to the `in_progress` state.
In case, if one of the functions interrupts the execution, then it changes to the `failed` state.
- **Callbacks** - class defines a set of functions that executing within one particular transition
after reaching the `target` state. 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 return `True` or `False` based on one particular requirement.
- **Permissions** - class defines a set of functions which receives an object and user, then returns `True` or
`False` based 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.
## Installation
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Django-Logic.
```bash
pip install django-logic
```
## Usage
0. Add to INSTALLED_APPS
```python
INSTALLED_APPS = (
...
'django_logic',
...
)
```
1. Define django model with one or more state fields.
```python
from django.db import models
MY_STATE_CHOICES = (
('draft', 'Draft'),
('approved', 'Approved'),
('paid', 'Paid'),
('void', 'Void'),
)
class Invoice(models.Model):
my_state = models.CharField(choices=MY_STATE_CHOICES, default='open', max_length=16, blank=True)
my_status = models.CharField(choices=MY_STATE_CHOICES, default='draft', max_length=16, blank=True)
```
2. Define a process class with some transitions.
```python
from django_logic import Process as BaseProcess, Transition, Action
from .choices import MY_STATE_CHOICES
class MyProcess(BaseProcess):
states = MY_STATE_CHOICES
transitions = [
Transition(action_name='approve', sources=['draft'], target='approved'),
Transition(action_name='pay', sources=['approve'], target='paid'),
Transition(action_name='void', sources=['draft', 'approved'], target='void'),
Action(action_name='update', side_effects=[update_data]),
]
```
3. Bind the process with a model.
```python
from django_logic import Process as BaseProcess, Transition, ProcessManager, Action
from .models import Invoice, MY_STATE_CHOICES
class MyProcess(BaseProcess):
states = MY_STATE_CHOICES
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]),
]
ProcessManager.bind_model_process(Invoice, MyProcess, state_field='my_state')
```
4. Advance your process with conditions, side-effects, and callbacks into the process. Use next_transition to automatically continue the process.
```python
class MyProcess(BaseProcess):
process_name = 'my_process'
permissions = [
is_accountant,
]
states = MY_STATE_CHOICES
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,
],
next_transition='pay'
),
Transition(
action_name='pay',
sources=['approved'],
target='paid',
side_effects=[
make_payment,
]
),
Transition(
action_name='void',
callbacks=[
send_void_invoice_email_to_accountant
],
sources=['approved'],
target='void'
),
Action(
action_name='update',
side_effects=[
update_data
],
),
]
```
5. 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.
6. Execute in the code:
```python
from invoices.models import Invoice
def approve_view(request, pk):
invoice = Invoice.objects.get(pk=pk)
invoice.my_process.approve(user=request.user, context={'my_var': 1})
```
Use context to pass data between side-effects and callbacks.
7. If you want to override the value of the state field, it must be done explicitly. For example:
```python
Invoice.objects.filter(status='draft').update(my_state='open')
# or
invoice = Invoice.objects.get(pk=pk)
invoice.my_state = 'open'
invoice.save(update_fields=['my_state'])
```
Save without `update_fields` won't update the value of the state field in order to protect the data from corrupting.
8. Error handling:
```python
from django_logic.exceptions import TransitionNotAllowed
try:
invoice.my_process.approve()
except TransitionNotAllowed:
logger.error('Approve is not allowed')
```
#### Display process
Drawing a process with the following elements:
- Process - a transparent rectangle
- Transition - a grey rectangle
- State - a transparent ellipse
- Process' conditions and permissions are defined inside of related process as a transparent diamond
- Transition' conditions and permissions are defined inside of related transition's process as a grey diamond
[![][diagram-img]][diagram-img]
From this diagram you can visually check that the following the business requirements have been implemented properly:
- Personnel involved: User and Staff
- Lock has to be available before any actions taken. It's defined by a condition `is_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.
## Django-Logic vs Django FSM
[Django FSM](https://github.com/viewflow/django-fsm) is 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 under [Django-Logic-Celery](https://github.com/Borderless360/django-logic-celery).
Finally, we want to provide a standard way on where to put the business logic in Django by using Django-Logic.
## Contributing
Pull 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.
## License
[MIT](https://choosealicense.com/licenses/mit/)
## Project status
Under development
[diagram-img]: https://user-images.githubusercontent.com/6745569/74101382-25c24680-4b74-11ea-8767-0eabd4f27ebc.png
Raw data
{
"_id": null,
"home_page": "https://github.com/Borderless360/django-logic",
"name": "django-logic",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.6",
"maintainer_email": null,
"keywords": "django",
"author": "Emil Balashov",
"author_email": "emil@borderless360.com",
"download_url": "https://files.pythonhosted.org/packages/d2/13/40a58edaf86d1be9f44d1d5ff03db9af1a3d86d07c293cad363ffea780ab/django_logic-0.1.6.tar.gz",
"platform": "any",
"description": "\n\n[](https://travis-ci.org/Borderless360/django-logic) \n[](https://coveralls.io/github/Borderless360/django-logic?branch=master)\n \nDjango Logic is a workflow framework allowing developers to implement the business logic via pure functions. \nIt's designed based on [Finite-State-Machine (FSM)](https://en.wikipedia.org/wiki/Finite-state_machine) principles. \nTherefore, it needs to define a `state` field for a model's object. Every change of the `state` is performed by a \ntransition and every transition could be grouped into a process. Also, you can define some side-effects that will be\nexecuted during the transition from one state to another and callbacks that will be run after. \nThis concept provides you a place for the business logic, rather than splitting it across the views, models, forms, \nserializers or even worse, in templates. \n\n## Definitions \n- **Transition** - class changes a state of an object from one to another. It also contains its own conditions,\n permissions, side-effects, callbacks, and failure callbacks. \n- **Action** - in contrast with the transition, the action does not change the state. \nBut it contains its own conditions, permissions, side-effects, callbacks, and failure callbacks. \n- **Side-effects** - class defines a set of functions that executing within one particular transition\n before reaching the `target` state. During the execution, the state changes to the `in_progress` state.\n In case, if one of the functions interrupts the execution, then it changes to the `failed` state.\n- **Callbacks** - class defines a set of functions that executing within one particular transition\n after reaching the `target` state. In case, if one of the functions interrupts the execution, it will log\n an exception and the execution will be stopped (without changing the state to failed). \n- **Failure callbacks** - class defines a set of functions that executing within one particular \ntransition in case if one of the side-effects has been failed to execute. \n- **Conditions** - class defines a set of functions which receives an object\n and return `True` or `False` based on one particular requirement.\n- **Permissions** - class defines a set of functions which receives an object and user, then returns `True` or \n`False` based on given permissions.\n- **Process** - class defines a set of transitions with some common conditions and permissions.\nIt also accepts nested processes that allow building the hierarchy.\n\n## Installation\n\nUse the package manager [pip](https://pip.pypa.io/en/stable/) to install Django-Logic.\n\n```bash\npip install django-logic\n```\n\n## Usage\n0. Add to INSTALLED_APPS\n```python\nINSTALLED_APPS = (\n ...\n 'django_logic',\n ...\n)\n```\n\n1. Define django model with one or more state fields. \n```python\nfrom django.db import models\n\n\nMY_STATE_CHOICES = (\n ('draft', 'Draft'),\n ('approved', 'Approved'),\n ('paid', 'Paid'),\n ('void', 'Void'),\n )\n\nclass Invoice(models.Model):\n my_state = models.CharField(choices=MY_STATE_CHOICES, default='open', max_length=16, blank=True) \n my_status = models.CharField(choices=MY_STATE_CHOICES, default='draft', max_length=16, blank=True)\n \n```\n\n2. Define a process class with some transitions.\n```python\nfrom django_logic import Process as BaseProcess, Transition, Action\nfrom .choices import MY_STATE_CHOICES\n\n\nclass MyProcess(BaseProcess):\n states = MY_STATE_CHOICES\n transitions = [\n Transition(action_name='approve', sources=['draft'], target='approved'),\n Transition(action_name='pay', sources=['approve'], target='paid'),\n Transition(action_name='void', sources=['draft', 'approved'], target='void'),\n Action(action_name='update', side_effects=[update_data]),\n ]\n```\n\n3. Bind the process with a model.\n```python\nfrom django_logic import Process as BaseProcess, Transition, ProcessManager, Action\nfrom .models import Invoice, MY_STATE_CHOICES\n\n\nclass MyProcess(BaseProcess):\n states = MY_STATE_CHOICES\n transitions = [\n Transition(action_name='approve', sources=['draft'], target='approved'),\n Transition(action_name='void', sources=['draft', 'approved'], target='void'),\n Action(action_name='update', side_effects=[update_data]),\n ]\n\nProcessManager.bind_model_process(Invoice, MyProcess, state_field='my_state')\n``` \n\n4. Advance your process with conditions, side-effects, and callbacks into the process. Use next_transition to automatically continue the process. \n```python \nclass MyProcess(BaseProcess):\n process_name = 'my_process' \n permissions = [\n is_accountant, \n ]\n states = MY_STATE_CHOICES\n transitions = [\n Transition(\n action_name='approve',\n sources=['draft'], \n target='approved',\n conditions=[\n is_customer_active, \n ]\n side_effects=[\n generate_pdf_invoice, \n ],\n callbacks=[\n send_approved_invoice_email_to_accountant, \n ],\n next_transition='pay' \n ),\n Transition(\n action_name='pay',\n sources=['approved'],\n target='paid',\n side_effects=[\n make_payment, \n ]\n ), \n Transition(\n action_name='void', \n callbacks=[\n send_void_invoice_email_to_accountant\n ],\n sources=['approved'],\n target='void'\n ),\n Action(\n action_name='update', \n side_effects=[\n update_data\n ],\n ),\n ]\n```\n\n5. This approval process defines the business logic where:\n- The user who performs the action must have accountant role (permission).\n- It shouldn't be possible to invoice inactive customers (condition). \n- Once the invoice record is approved, it should generate a PDF file and send it to \nan accountant via email. (side-effects and callbacks)\n- If the invoice voided it needs to notify the accountant about that.\nAs you see, these business requirements should not know about each other. Furthermore, it gives a simple way \nto test every function separately as Django-Logic takes care of connection them into the business process. \n\n6. Execute in the code:\n```python\nfrom invoices.models import Invoice\n\n\ndef approve_view(request, pk):\n invoice = Invoice.objects.get(pk=pk)\n invoice.my_process.approve(user=request.user, context={'my_var': 1})\n```\nUse context to pass data between side-effects and callbacks.\n\n7. If you want to override the value of the state field, it must be done explicitly. For example: \n```python\nInvoice.objects.filter(status='draft').update(my_state='open')\n# or \ninvoice = Invoice.objects.get(pk=pk)\ninvoice.my_state = 'open'\ninvoice.save(update_fields=['my_state'])\n```\nSave without `update_fields` won't update the value of the state field in order to protect the data from corrupting. \n\n8. Error handling:\n```python \nfrom django_logic.exceptions import TransitionNotAllowed\n\ntry:\n invoice.my_process.approve()\nexcept TransitionNotAllowed:\n logger.error('Approve is not allowed') \n```\n\n#### Display process\nDrawing a process with the following elements:\n- Process - a transparent rectangle \n- Transition - a grey rectangle \n- State - a transparent ellipse \n- Process' conditions and permissions are defined inside of related process as a transparent diamond\n- Transition' conditions and permissions are defined inside of related transition's process as a grey diamond\n \n[![][diagram-img]][diagram-img]\n\nFrom this diagram you can visually check that the following the business requirements have been implemented properly:\n- Personnel involved: User and Staff\n- Lock has to be available before any actions taken. It's defined by a condition `is_lock_available`. \n- User is able to lock and unlock an available locker. \n- Staff is able to lock, unlock and put a locker under maintenance if such was planned. \n\n\n## Django-Logic vs Django FSM \n[Django FSM](https://github.com/viewflow/django-fsm) is a parent package of Django-Logic. \nIt's been used in production for many years until the number of new ideas and requirements swamped us.\nTherefore, it's been decided to implement these ideas under a new package. For example, supporting Processes or \nbackground transitions which were implemented under [Django-Logic-Celery](https://github.com/Borderless360/django-logic-celery).\nFinally, we want to provide a standard way on where to put the business logic in Django by using Django-Logic. \n\n## Contributing\nPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.\n\nPlease make sure to update tests as appropriate.\n\n## License\n[MIT](https://choosealicense.com/licenses/mit/)\n\n## Project status\nUnder development\n\n\n[diagram-img]: https://user-images.githubusercontent.com/6745569/74101382-25c24680-4b74-11ea-8767-0eabd4f27ebc.png\n",
"bugtrack_url": null,
"license": "MIT License",
"summary": "Django Logic - easy way to implement state-based business logic",
"version": "0.1.6",
"project_urls": {
"Homepage": "https://github.com/Borderless360/django-logic"
},
"split_keywords": [
"django"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "b41be52e5633edffbfbced7e48a74f24f589b40aae01f274f2e88ea7ee9da708",
"md5": "74ee06928fd062d3b835925f02ed6b42",
"sha256": "79cb11e04d282916c8ead3a7b639404ce53b82c8a2f3665a74926edb253c800b"
},
"downloads": -1,
"filename": "django_logic-0.1.6-py3-none-any.whl",
"has_sig": false,
"md5_digest": "74ee06928fd062d3b835925f02ed6b42",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.6",
"size": 13833,
"upload_time": "2025-07-11T05:00:34",
"upload_time_iso_8601": "2025-07-11T05:00:34.030280Z",
"url": "https://files.pythonhosted.org/packages/b4/1b/e52e5633edffbfbced7e48a74f24f589b40aae01f274f2e88ea7ee9da708/django_logic-0.1.6-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "d21340a58edaf86d1be9f44d1d5ff03db9af1a3d86d07c293cad363ffea780ab",
"md5": "69bce9b34c624e494ae733abe7da839e",
"sha256": "fa7585cb0defe0b01fd60a0c7a1c81a8134d982700493d4b13e6762bf243f9b0"
},
"downloads": -1,
"filename": "django_logic-0.1.6.tar.gz",
"has_sig": false,
"md5_digest": "69bce9b34c624e494ae733abe7da839e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6",
"size": 18898,
"upload_time": "2025-07-11T05:00:35",
"upload_time_iso_8601": "2025-07-11T05:00:35.490921Z",
"url": "https://files.pythonhosted.org/packages/d2/13/40a58edaf86d1be9f44d1d5ff03db9af1a3d86d07c293cad363ffea780ab/django_logic-0.1.6.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-11 05:00:35",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Borderless360",
"github_project": "django-logic",
"travis_ci": true,
"coveralls": false,
"github_actions": false,
"requirements": [
{
"name": "django",
"specs": [
[
">=",
"2.0"
]
]
},
{
"name": "django-model-utils",
"specs": [
[
"==",
"4.5.1"
]
]
},
{
"name": "djangorestframework",
"specs": [
[
"==",
"3.15.2"
]
]
}
],
"lcname": "django-logic"
}