django-flows

django-flows keeps state and position in complicated flows of logic, allowing optional branches and complicated paths through a series of individual user actions.


Keywords
django
License
BSD-3-Clause
Install
pip install django-flows==1.1.1

Documentation

django-flows

Build Status Code Quality

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'.

Example

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.

Concepts

Flows

A 'flow' is considered to be a series of user actions required to perform some functionality.

Actions

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.

In django-flows, an 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.

Scaffolds

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

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.

Transitions

A 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.

An 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 Transition object.

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 Scaffold.

Settings

For all defaults, see flows.config.

  • FLOWS_STATE_STORE

    Default: flows.statestore.django_store

    Flows keep state between requests using a persistant storage mechanism. This mechanism is configurable by changing the FLOWS_STATE_STORE setting. The default is to use django models to store the task state.

    Available options built in:

    • flows.statestore.django_store

      This will store state on django models.

    • flows.statestore.redis_store

      This will store state in a redis database. Additional configuration options are available here, with sensible defaults:

      • FLOWS_REDIS_STATE_STORE_DB

        The Redis DB number to use. Defaults to 0

      • FLOWS_REDIS_STATE_STORE_HOST

        The host running the redis server to use. Defaults to localhost

      • FLOWS_REDIS_STATE_STORE_PORT

        The port to connect to on the redis server. Defaults to 6379

      • FLOWS_REDIS_STATE_STORE_PASSWORD

        The password to use when connecting to the redis server. Defaults to empty.

    • flows.statestore.tmpfile_store

      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 StateStore which extends BaseStateStore in flows.statestore.base and implement the two methods get_state(self, task_id) and put_state(self, task_id, state). Then change the FLOWS_STATE_STORE setting to the module you created.

  • FLOWS_TASK_IDLE_TIMEOUT

    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.

  • FLOWS_TASK_ID_PARAM

    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 _id.

  • FLOWS_TASK_BINDER

    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.

Misc

Flow Graph Visualisation

If settings.DEBUG is 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:

  • django-admin.py cleanupflows

    This is a command which will delete the expired state from the database. You can run it manually or as part of a cronjob.

  • flows.additional.celery.cleanup_task

    If you are using Celery then you can use this provided task to clean up old task state every 5 minutes.