django-flows can best be described as 'wizards on steroids'. Its purpose is to keep state and position in complicated flows of logic, allowing optional branches and complicated paths through a series of individual user actions.
django-flows makes it possible to specify subsections of functionality and group them together later. It recognises that, at the core, there are several user actions such as logging in, or entering a credit card number, and that the web application needs to group these actions in such a way that all state required to make a purchase, for example, is obtained. It also seeks to make these actions reusable, and to group related actions together into larger 'user flows'.
The actual use case which caused this library to be written is the following: say you want to sell something to the user. This requires showing the user what they are buying, how much it costs etc. If the user chooses to purchase, then they have to be logged in. However if they have no account, they have to register. Or, perhaps they forgot their password. Either way, once that is done, then you need the user to choose a method of paying. Either they have already saved a credit card, for example, or they need to enter a new method of payment. This could take various forms - required credit card information is different to bank account information. Once that is collected, then a final confirmation step is required.
The overall process is quite straightforward to explain:
- Do you want to buy this?
- Tell us who you are
- Tell us how you want to pay
- Confirm everything
However each of those steps have various substeps. The actual tree of possible interaction looks something like this:
+-- Do you want to buy this? | +-+ Tell us who you are | | | +-- Log in to existing account OR | | | +-- Create new account OR | | | +-+ I forgot my password | | | +-- Enter your email address | | | +-- Enter your confirmation code | | | +-- Reset your password | +-+ Choose how to pay | | | +-- Choose a payment method I already saved OR | | | +-+ Enter a new payment method | | | +-- Credit card number OR | | | +-+ Bank account details | | | | | + Account address | | | +-- PayPal | +-- Confirm and pay
At each point, there are several options, each of which could branch into more options, which could have sub-options, and so on.
Each of these branches are subsets of functionality which standalone, too. Entering your credit card information is a piece of functionality, just as 'choose a payment method' is.
django-flows seeks to decouple these pieces so that they are reusable, and enable these pieces to be interchanged, reordered and mixed together in flows at will.
A 'flow' is considered to be a series of user actions required to perform some functionality.
At its heart, any flow is just a series of actions taken by the user. These can be as simple as clicking 'next' after reading something informational, to entering large amounts of details to register a new user account.
Action is basically identical to a Django
FormView (and in fact, inherits from it). It will be shown to the user on a
GET request, and the form will be processed on
POST. If the form validates, then the
Action is complete.
When an action is complete, it can either explicitly send the user to another action, or it can simply return
COMPLETE to allow the flows framework to figure out where to send the user next.
A flow is a set of actions, which can branch at various points. Scaffolds group actions into logical collections (eg, 'Get the user account' has possible actions of 'login' and 'register'). These scaffolds can then be used themselves inside other scaffolds, leading to the tree-like structure which gives
django-flows its benefit over regular Django wizards.
class GetUserAccount(Scaffold): action_set = [LoginOrRegister, Register, ForgotPassword]
In the example above, the
GetUserAccount scaffold groups the actions which encapsulate all ways a user can become authenticated.
LoginOrRegister is an action with a login form, with a link to register for users who don't have an account.
Register is an action which allows the user to register.
ForgotPassword is itself a scaffold, which contains the actions required to reset the user's password if they have forgotten it.
Tasks and State
The main difficulty with simply using standard views is keeping the state between them. When user intentions can branch or loop back on themselves, trying to use URL parameters or session data can become tedious or even impossible. Additionally, if a user has multiple tabs or browser windows open, then using sessions will cause their interactions between tabs to interfere with each other, leading to unpredictable results.
django-flows has the concept of 'tasks'. This is basically an isolated set of state which allows multiple concurrent tasks to run. The specific task which the user is currently doing is kept using an
id parameter, which is automatically added to URLs and forms generated.
This identifier is used to retrieve state for the particular flow the user is currently working through.
Preconditions are various requirements of the current flow state which are checked before the current action is executed. These include things like ensuring that a field is present in the state. For example, if an action requires a 'purchase item' to have been set by a previous action, then a
RequiredState precondition to check its presence. Another example is
LoginRequired, which ensures the
Action will not be available to users who are not authenticated.
Scaffold is made up of a list of
Actions which represent some larger functionality. These can sometimes be several steps in a long signup process, or can be several possible branches the user can choose.
Action can either explicitly send the user, or, for maximum reusability, can simply return
COMPLETE and let the parent
Scaffold decide where to send the user next. The decision of where to go next is handled by the
The default behaviour is to expect the actions to handle their next destination themselves. However sometimes the steps are predictable - moving from step 1 to 2 to 3 - and in this case, using a
Linear transition will cause the flow to move between
Actions in the order specified in the
action_set attribute of the
For all defaults, see
Flows keep state between requests using a persistant storage mechanism. This mechanism is configurable by changing the
FLOWS_STATE_STOREsetting. The default is to use django models to store the task state.
Available options built in:
This will store state on django models.
This will store state in a redis database. Additional configuration options are available here, with sensible defaults:
The Redis DB number to use. Defaults to
The host running the redis server to use. Defaults to
The port to connect to on the redis server. Defaults to
The password to use when connecting to the redis server. Defaults to empty.
This stores state in temporary files on disk. It is not recommended for production use, because state timeout cannot be implemented, leading to stale task state. It can be useful in development however.
You can also create your own method of state storage. Simply create a module with a class called
flows.statestore.baseand implement the two methods
put_state(self, task_id, state). Then change the
FLOWS_STATE_STOREsetting to the module you created.
The time to allow a task to idle before it is removed. That is, how long the state will be kept without any interaction from the user. This value is in seconds, and defaults to 20 minutes.
The ID of the current flow is kept in the URL as a parameter to differentiate between different flows occurring concurrently for the same user in the same browser. This setting changes the name of the parameter. The default value is
Task IDs are appended to every URL. This URL can easily be shared. To prevent a user giving out their task state simply by sharing a URL, a task ID is bound to a value specific to the current user so that only the user who started a task can continue executing it.
This setting is a method which will be called to get the value to bind the task to. It is called with a single argument, the current request. The default behaviour is to bind the task to the session ID.
Flow Graph Visualisation
True, then the flow graph visualization is enabled. Under the root of a flow handler, use
.flowgraph as the path to see a graph of the actions and paths between them in the flows. This requires that the PyDot library is installed.
Eg: if a flow handler is installed under
/some/path/ then navigating to
/some/path/.flowgraph will show the layout of the flows of that hander.
Cleaning up expired task state in the database
If you are using the
DjangoStateStore backend (which is the default), then the task state will be stored as rows in the database. Although stale tasks will not be returned, the state will stay in the database and not be deleted. To clean it up, you have several options:
This is a command which will delete the expired state from the database. You can run it manually or as part of a cronjob.
If you are using Celery then you can use this provided task to clean up old task state every 5 minutes.