django-plotly-dash

Plotly Dash applications served up in Django templates using tags.

Contents

Introduction

The purpose of django-plotly-dash is to enable Plotly Dash applications to be served up as part of a Django application, in order to provide these features:

  • Multiple dash applications can be used on a single page
  • Separate instances of a dash application can persist along with internal state
  • Leverage user management and access control and other parts of the Django infrastructure
  • Consolidate into a single server process to simplify scaling

There is nothing here that cannot be achieved through expanding the Flask app around Plotly Dash, or indeed by using an alternative web framework. The purpose of this project is to enable the above features, given that the choice to use Django has already been made.

The source code can be found in this github repository. This repository also includes a self-contained demo application, which can also be viewed online.

Overview

django_plotly_dash works by wrapping around the dash.Dash object. The http endpoints exposed by the Dash application are mapped to Django ones, and an application is embedded into a webpage through the use of a template tag. Multiple Dash applications can be used in a single page.

A subset of the internal state of a Dash application can be persisted as a standard Django model instance, and the application with this internal state is then available at its own URL. This can then be embedded into one or more pages in the same manner as described above for stateless applications.

Also, an enhanced version of the Dash callback is provided, giving the callback access to the current User, the current session, and also the model instance associated with the application’s internal state.

This package is compatible with version 2.0 onwards of Django. Use of the live updating feature requires the Django Channels extension; in turn this requires a suitable messaging backend such as Redis.

Installation

The package requires version 3.2 or greater of Django, and a minimum Python version needed of 3.8.

Use pip to install the package, preferably to a local virtualenv:

pip install django_plotly_dash

Then, add django_plotly_dash to INSTALLED_APPS in the Django settings.py file:

INSTALLED_APPS = [
    ...
    'django_plotly_dash.apps.DjangoPlotlyDashConfig',
    ...
]

The project directory name django_plotly_dash can also be used on its own if preferred, but this will stop the use of readable application names in the Django admin interface.

Also, enable the use of frames within HTML documents by also adding to the settings.py file:

X_FRAME_OPTIONS = 'SAMEORIGIN'

Further, if the header and footer tags are in use then django_plotly_dash.middleware.BaseMiddleware should be added to MIDDLEWARE in the same file. This can be safely added now even if not used.

If assets are being served locally through the use of the global serve_locally or on a per-app basis, then django_plotly_dash.middleware.ExternalRedirectionMiddleware should be added, along with the whitenoise package whose middleware should also be added as per the instructions for that package. In addition, dpd_static_support should be added to the INSTALLED_APPS setting.

The application’s routes need to be registered within the routing structure by an appropriate include statement in a urls.py file:

urlpatterns = [
    ...
    path('django_plotly_dash/', include('django_plotly_dash.urls')),
]

The name within the URL is not important and can be changed.

For the final installation step, a migration is needed to update the database:

./manage.py migrate

It is important to ensure that any applications are registered using the DjangoDash class. This means that any python module containing the registration code has to be known to Django and loaded at the appropriate time.

Note

An easy way to register the Plotly app is to import it into views.py or urls.py as in the following example, which assumes the plotly_app module (plotly_app.py) is located in the same folder as views.py:

``from . import plotly_app``

Once your Plotly app is registered, plotly_app tag in the plotly_dash tag library can then be used to render it as a dash component. See Simple usage for a simple example.

Extra steps for live state

The live updating of application state uses the Django Channels project and a suitable message-passing backend. The included demonstration uses Redis:

pip install channels daphne redis django-redis channels-redis

A standard installation of the Redis package is required. Assuming the use of docker and the current production version:

docker pull redis:4
docker run -p 6379:6379 -d redis

The prepare_redis script in the root of the repository performs these steps.

This will launch a container running on the localhost. Following the channels documentation, as well as adding channels to the INSTALLED_APPS list, a CHANNEL_LAYERS entry in settings.py is also needed:

INSTALLED_APPS = [
    ...
    'django_plotly_dash.apps.DjangoPlotlyDashConfig',
    'channels',
    ...
    ]

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('127.0.0.1', 6379),],
        },
    },
}

The host and port entries in hosts should be adjusted to match the network location of the Redis instance.

Further configuration

Further configuration options can be specified through the optional PLOTLY_DASH settings variable. The available options are detailed in the configuration section.

This includes arranging for Dash assets to be served using the Django staticfiles functionality.

A checklist for using dash-bootstrap-components can be found in the bootstrap section.

Source code and demo

The source code repository contains a simple demo application.

To install and run it:

git clone https://github.com/GibbsConsulting/django-plotly-dash.git

cd django-plotly-dash

./make_env                # sets up a virtual environment
                          #   with direct use of the source
                          #   code for the package

./prepare_redis           # downloads a redis docker container
                          #   and launches it with default settings
                          #   *THIS STEP IS OPTIONAL*

./prepare_demo            # prepares and launches the demo
                          #   using the Django debug server
                          #   at http://localhost:8000

This will launch a simple Django application. A superuser account is also configured, with both username and password set to admin. If the prepare_redis step is skipped then the fourth demo page, exhibiting live updating, will not work.

More details on setting up a development environment, which is also sufficient for running the demo, can be found in the development section.

Note that the current demo, along with the codebase, is in a prerelease and very raw form. An overview can be found in the demonstration application section.`

Simple usage

To use existing dash applications, first register them using the DjangoDash class. This replaces the Dash class from the dash package.

Taking a simple example inspired by the excellent getting started guide:

import dash
from dash import dcc, html

from django_plotly_dash import DjangoDash

app = DjangoDash('SimpleExample')   # replaces dash.Dash

app.layout = html.Div([
    dcc.RadioItems(
        id='dropdown-color',
        options=[{'label': c, 'value': c.lower()} for c in ['Red', 'Green', 'Blue']],
        value='red'
    ),
    html.Div(id='output-color'),
    dcc.RadioItems(
        id='dropdown-size',
        options=[{'label': i,
                  'value': j} for i, j in [('L','large'), ('M','medium'), ('S','small')]],
        value='medium'
    ),
    html.Div(id='output-size')

])

@app.callback(
    dash.dependencies.Output('output-color', 'children'),
    [dash.dependencies.Input('dropdown-color', 'value')])
def callback_color(dropdown_value):
    return "The selected color is %s." % dropdown_value

@app.callback(
    dash.dependencies.Output('output-size', 'children'),
    [dash.dependencies.Input('dropdown-color', 'value'),
     dash.dependencies.Input('dropdown-size', 'value')])
def callback_size(dropdown_color, dropdown_size):
    return "The chosen T-shirt is a %s %s one." %(dropdown_size,
                                                  dropdown_color)

Note that the DjangoDash constructor requires a name to be specified. This name is then used to identify the dash app in templates:

{%load plotly_dash%}

{%plotly_app name="SimpleExample"%}

Direct use in this manner, without any application state or use of live updating, is equivalent to inserting an iframe containing the URL of a Dash application.

Note

The registration code needs to be in a location that will be imported into the Django process before any model or template tag attempts to use it. The example Django application in the demo subdirectory achieves this through an import in the main urls.py file, but any views.py would also be sufficient.

Django models and application state

The django_plotly_dash application defines DashApp and StatelessApp models.

The StatelessApp model

An instance of the StatelessApp model represents a single dash application. Every instantiation of a DjangoDash object is registered, and any object that is referenced through the DashApp model - this includes all template access as well as model instances themselves - causes a StatelessApp model instance to be created if one does not already exist.

class StatelessApp(models.Model):
    '''
    A stateless Dash app.

    An instance of this model represents a dash app without any specific state
    '''

    app_name = models.CharField(max_length=100, blank=False, null=False, unique=True)
    slug = models.SlugField(max_length=110, unique=True, blank=True)

    def as_dash_app(self):
        '''
        Return a DjangoDash instance of the dash application
        '''

The main role of a StatelessApp instance is to manage access to the associated DjangoDash object, as exposed through the as_dash_app member function.

In the Django admin, an action is provided to check all of the known stateless instances. Those that cannot be instantiated are logged; this is a useful quick check to see what apps are avalilable. Also, in the same admin an additional button is provided to create StatelessApp instances for any known instance that does not have an ORM entry.

The DashApp model

An instance of the DashApp model represents an instance of application state.

class DashApp(models.Model):
    '''
    An instance of this model represents a Dash application and its internal state
    '''
    stateless_app = models.ForeignKey(StatelessApp, on_delete=models.PROTECT,
                                      unique=False, null=False, blank=False)
    instance_name = models.CharField(max_length=100, unique=True, blank=True, null=False)
    slug = models.SlugField(max_length=110, unique=True, blank=True)
    base_state = models.TextField(null=False, default="{}")
    creation = models.DateTimeField(auto_now_add=True)
    update = models.DateTimeField(auto_now=True)
    save_on_change = models.BooleanField(null=False,default=False)

    ... methods, mainly for managing the Dash application state ...

    def current_state(self):
        '''
        Return the current internal state of the model instance
        '''

    def update_current_state(self, wid, key, value):
        '''
        Update the current internal state, ignorning non-tracked objects
        '''

    def populate_values(self):
        '''
        Add values from the underlying dash layout configuration
        '''

The stateless_app references an instance of the StatelessApp model described above. The slug field provides a unique identifier that is used in URLs to identify the instance of an application, and also its associated server-side state.

The persisted state of the instance is contained, serialised as JSON, in the base_state variable. This is an arbitrary subset of the internal state of the object. Whenever a Dash application requests its state (through the <app slug>_dash-layout url), any values from the underlying application that are present in base_state are overwritten with the persisted values.

The populate_values member function can be used to insert all possible initial values into base_state. This functionality is also exposed in the Django admin for these model instances, as a Populate app action.

From callback code, the update_current_state method can be called to change the initial value of any variable tracked within the base_state. Variables not tracked will be ignored. This function is automatically called for any callback argument and return value.

Finally, after any callback has finished, and after any result stored through update_current_state, then the application model instance will be persisted by means of a call to its save method, if any changes have been detected and the save_on_change flag is True.

Extended callback syntax

The DjangoDash class allows callbacks to request extra arguments when registered.

To do this, simply add to your callback function the extra arguments you would like to receive after the usual parameters for your Input and State. This will cause these callbacks registered with this application to receive extra parameters in addition to their usual callback parameters.

If you specify a kwargs in your callback, it will receive all possible extra parameters (see below for a list). If you specify explicitly extra parameters from the list below, only these will be passed to your callback.

For example, the plotly_apps.py example contains this dash application:

import dash
from dash import dcc, html

from django_plotly_dash import DjangoDash

a2 = DjangoDash("Ex2")

a2.layout = html.Div([
    dcc.RadioItems(id="dropdown-one",options=[{'label':i,'value':j} for i,j in [
    ("O2","Oxygen"),("N2","Nitrogen"),("CO2","Carbon Dioxide")]
    ],value="Oxygen"),
    html.Div(id="output-one")
    ])

@a2.callback(
    dash.dependencies.Output('output-one','children'),
    [dash.dependencies.Input('dropdown-one','value')]
    )
def callback_c(*args,**kwargs):
    da = kwargs['dash_app']
    return "Args are [%s] and kwargs are %s" %(",".join(args), kwargs)

The additional arguments, which are reported as the kwargs content in this example, include

callback_context:
 The Dash callback context. See the `documentation <https://dash.plotly.com/advanced-callbacks`_ on the content of this variable. This variable is provided as an argument to the callback as well as the dash.callback_context global variable.
dash_app:For stateful applications, the DashApp model instance
dash_app_id:The application identifier. For stateless applications, this is the (slugified) name given to the DjangoDash constructor. For stateful applications, it is the (slugified) unique identifier for the associated model instance.
request:The Django request object.
session_state:A dictionary of information, unique to this user session. Any changes made to its content during the callback are persisted as part of the Django session framework.
user:The Django User instance.

Possible alternatives to kwargs

@a2.callback(
    dash.dependencies.Output('output-one','children'),
    [dash.dependencies.Input('dropdown-one','value')]
    )
def callback_c(*args, dash_app):
    return "Args are [%s] and the extra parameter dash_app is %s" %(",".join(args), dash_app)

@a2.callback(
    dash.dependencies.Output('output-one','children'),
    [dash.dependencies.Input('dropdown-one','value')]
    )
def callback_c(*args, dash_app, **kwargs):
    return "Args are [%s], the extra parameter dash_app is %s and kwargs are %s" %(",".join(args), dash_app, kwargs)

The DashApp model instance can also be configured to persist itself on any change. This is discussed in the Django models and application state section.

The callback_context argument is provided in addition to the dash.callback_context global variable. As a rule, the use of global variables should generally be avoided. The context provided by django-plotly-dash is not the same as the one provided by the underlying Dash library, although its property values are the same and code that uses the content of this variable should work unchanged. The use of this global variable in any asychronous or multithreaded application is not supported, and the use of the callback argument is strongly recommended for all use cases.

Using session state

The walkthrough of the session state example details how the XXX demo interacts with a Django session.

Unless an explicit pipe is created, changes to the session state and other server-side objects are not automatically propagated to an application. Something in the front-end UI has to invoke a callback; at this point the latest version of these objects will be provided to the callback. The same considerations as in other Dash live updates apply.

The live updating section discusses how django-plotly-dash provides an explicit pipe that directly enables the updating of applications.

Live updating

Live updating is supported using additional Dash components and leveraging Django Channels to provide websocket endpoints.

Server-initiated messages are sent to all interested clients. The content of the message is then injected into the application from the client, and from that point it is handled like any other value passed to a callback function. The messages are constrained to be JSON serialisable, as that is how they are transmitted to and from the clients, and should also be as small as possible given that they travel from the server, to each interested client, and then back to the server again as an argument to one or more callback functions.

The round-trip of the message is a deliberate design choice, in order to enable the value within the message to be treated as much as possible like any other piece of data within a Dash application. This data is essentially stored on the client side of the client-server split, and passed to the server when each callback is invoked; note that this also encourages designs that keep the size of in-application data small. An alternative approach, such as directly invoking a callback in the server, would require the server to maintain its own copy of the application state.

Live updating requires a server setup that is considerably more complex than the alternative, namely use of the built-in Interval component. However, live updating can be used to reduce server load (as callbacks are only made when needed) and application latency (as callbacks are invoked as needed, not on the tempo of the Interval component).

Message channels

Messages are passed through named channels, and each message consists of a label and value pair. A Pipe component is provided that listens for messages and makes them available to Dash callbacks. Each message is sent through a message channel to all Pipe components that have registered their interest in that channel, and in turn the components will select messages by label.

A message channel exists as soon as a component signals that it is listening for messages on it. The message delivery requirement is ‘hopefully at least once’. In other words, applications should be robust against both the failure of a message to be delivered, and also for a message to be delivered multiple times. A design approach that has messages of the form ‘you should look at X and see if something should be done’ is strongly encouraged. The accompanying demo has messages of the form ‘button X at time T’, for example.

Sending messages from within Django

Messages can be easily sent from within Django, provided that they are within the ASGI server.

from django_plotly_dash.consumers import send_to_pipe_channel

# Send a message
#
# This function may return *before* the message has been sent
# to the pipe channel.
#
send_to_pipe_channel(channel_name="live_button_counter",
                     label="named_counts",
                     value=value)

# Send a message asynchronously
#
await async_send_to_pipe_channel(channel_name="live_button_counter",
                                 label="named_counts",
                                 value=value)

In general, making assumptions about the ordering of code between message sending and receiving is unsafe. The send_to_pipe function uses the Django Channels async_to_sync wrapper around a call to async_send_to_pipe and therefore may return before the asynchronous call is made (perhaps on a different thread). Furthermore, the transit of the message through the channels backend introduces another indeterminacy.

HTTP Endpoint

There is an HTTP endpoint, configured with the http_route option, that allows direct insertion of messages into a message channel. It is a direct equivalent of calling the send_to_pipe_channel function, and expects the channel_name, label and value arguments to be provided in a JSON-encoded dictionary.

curl -d '{"channel_name":"live_button_counter",
          "label":"named_counts",
          "value":{"click_colour":"cyan"}}'
          http://localhost:8000/dpd/views/poke/

This will cause the (JSON-encoded) value argument to be sent on the channel_name channel with the given label.

The provided endpoint skips any CSRF checks and does not perform any security checks such as authentication or authorisation, and should be regarded as a starting point for a more complete implementation if exposing this functionality is desired. On the other hand, if this endpoint is restricted so that it is only available from trusted sources such as the server itself, it does provide a mechanism for Django code running outside of the ASGI server, such as in a WSGI process or Celery worker, to push a message out to running applications.

The http_poke_enabled flag controls the availability of the endpoint. If false, then it is not registered at all and all requests will receive a 404 HTTP error code.

Deployment

The live updating feature needs both Redis, as it is the only supported backend at present for v2.0 and up of Channels, and Daphne or any other ASGI server for production use. It is also good practise to place the server(s) behind a reverse proxy such as Nginx; this can then also be configured to serve Django’s static files.

A further consideration is the use of a WSGI server, such as Gunicorn, to serve the non-asynchronous subset of the http routes, albeit at the expense of having to separately manage ASGI and WSGI servers. This can be easily achieved through selective routing at the reverse proxy level, and is the driver behind the ws_route configuration option.

In passing, note that the demo also uses Redis as the caching backend for Django.

Template tags

Template tags are provided in the plotly_dash library:

{%load plotly_dash%}

The plotly_app template tag

Importing the plotly_dash library provides the plotly_app template tag:

{%load plotly_dash%}

{%plotly_app name="SimpleExample"%}

This tag inserts a DjangoDash app within a page as a responsive iframe element.

The tag arguments are:

name = None:The name of the application, as passed to a DjangoDash constructor.
slug = None:The slug of an existing DashApp instance.
da = None:An existing django_plotly_dash.models.DashApp model instance.
ratio = 0.1:The ratio of height to width. The container will inherit its width as 100% of its parent, and then rely on this ratio to set its height.
use_frameborder = “0”:
 HTML element property of the iframe containing the application.
initial_arguments = None:
 Initial arguments overriding app defaults and saved state.

At least one of da, slug or name must be provided. An object identified by slug will always be used, otherwise any identified by name will be. If either of these arguments are provided, they must resolve to valid objects even if not used. If neither are provided, then the model instance in da will be used.

The initial_arguments are specified as a python dictionary. This can be the actual dict object, or a JSON-encoded string representation. Each entry in the dictionary has the id as key, and the corresponding value is a dictionary mapping property name keys to initial values.

The plotly_app_bootstrap template tag

This is a variant of the plotly_app template for use with responsive layouts using the Bootstrap library

{%load plotly_dash%}

{%plotly_app_bootstrap name="SimpleExample" aspect="16by9"%}

The tag arguments are similar to the plotly_app ones:

name = None:The name of the application, as passed to a DjangoDash constructor.
slug = None:The slug of an existing DashApp instance.
da = None:An existing django_plotly_dash.models.DashApp model instance.
aspect= “4by3”:The aspect ratio of the app. Should be one of 21by9, 16by9, 4by3 or 1by1.
initial_arguments = None:
 Initial arguments overriding app defaults and saved state.

At least one of da, slug or name must be provided. An object identified by slug will always be used, otherwise any identified by name will be. If either of these arguments are provided, they must resolve to valid objects even if not used. If neither are provided, then the model instance in da will be used.

The aspect ratio has to be one of the available ones from the Bootstrap framework.

The initial_arguments are specified as a python dictionary. This can be the actual dict object, or a JSON-encoded string representation. Each entry in the dictionary has the id as key, and the corresponding value is a dictionary mapping property name keys to initial values.

The plotly_direct template tag

This template tag allows the direct insertion of html into a template, instead of embedding it in an iframe.

{%load plotly_dash%}

{%plotly_direct name="SimpleExample"%}

The tag arguments are:

name = None:The name of the application, as passed to a DjangoDash constructor.
slug = None:The slug of an existing DashApp instance.
da = None:An existing django_plotly_dash.models.DashApp model instance.

These arguments are equivalent to the same ones for the plotly_app template tag. Note that initial_arguments are not currently supported, and as the app is directly injected into the page there are no arguments to control the size of the iframe.

This tag should not appear more than once on a page. This rule however is not enforced at present.

If this tag is used, then the header and footer tags should also be added to the template. Note that these tags in turn have middleware requirements.

The plotly_message_pipe template tag

This template tag has to be inserted on every page that uses live updating:

{%load plotly_dash%}

{%plotly_app ... DjangoDash instances using live updating ... %}

{%plotly_message_pipe%}

The tag inserts javascript needed for the Pipe component to operate. It can be inserted anywhere on the page, and its ordering relative to the Dash instances using updating is not important, so placing it in the page footer - to avoid delaying the main page load - along with other scripts is generally advisable.

The plotly_app_identifier template tag

This tag provides an identifier for an app, in a form that is suitable for use as a classname or identifier in HTML:

{%load plotly_dash%}

{%plotly_app_identifier name="SimpleExample"%}

{%plotly_app_identifier slug="liveoutput-2" postfix="A"%}

The identifier, if the tag is not passed a slug, is the result of passing the identifier of the app through the django.utils.text.slugify function.

The tag arguments are:

name = None:The name of the application, as passed to a DjangoDash constructor.
slug = None:The slug of an existing DashApp instance.
da = None:An existing django_plotly_dash.models.DashApp model instance.
postfix = None:An optional string; if specified it is appended to the identifier with a hyphen.

The validity rules for these arguments are the same as those for the plotly_app template tag. If supplied, the postfix argument should already be in a slug-friendly form, as no processing is performed on it.

The plotly_class template tag

Generate a string of class names, suitable for a div or other element that wraps around django-plotly-dash template content.

{%load plotly_dash%}

<div class="{%plotly_class slug="liveoutput-2" postfix="A"%}">
  {%plotly_app slug="liveoutput-2" ratio="0.5" %}
</div>

The identifier, if the tag is not passed a slug, is the result of passing the identifier of the app through the django.utils.text.slugify function.

The tag arguments are:

name = None:The name of the application, as passed to a DjangoDash constructor.
slug = None:The slug of an existing DashApp instance.
da = None:An existing django_plotly_dash.models.DashApp model instance.
prefix = None:Optional prefix to use in place of the text django-plotly-dash in each class name
postfix = None:An optional string; if specified it is appended to the app-specific identifier with a hyphen.
template_type = None:
 Optional text to use in place of iframe in the template-specific class name

The tag inserts a string with three class names in it. One is just the prefix argument, one has the template_type appended, and the final one has the app identifier (as generated by the plotly_app_identifier tag) and any postfix appended.

The validity rules for these arguments are the same as those for the plotly_app and plotly_app_identifier template tags. Note that none of the prefix, postfix and template_type arguments are modified and they should already be in a slug-friendly form, or otherwise fit for their intended purpose.

Dash components

The dpd-components package contains Dash components. This package is installed as a dependency of django-plotly-dash.

The Pipe component

Each Pipe component instance listens for messages on a single channel. The value member of any message on that channel whose label matches that of the component will be used to update the value property of the component. This property can then be used in callbacks like any other Dash component property.

An example, from the demo application:

import dpd_components as dpd

app.layout = html.Div([
    ...
    dpd.Pipe(id="named_count_pipe",               # ID in callback
             value=None,                          # Initial value prior to any message
             label="named_counts",                # Label used to identify relevant messages
             channel_name="live_button_counter"), # Channel whose messages are to be examined
    ...
    ])

The value of the message is sent from the server to all front ends with Pipe components listening on the given channel_name. This means that this part of the message should be small, and it must be JSON serialisable. Also, there is no guarantee that any callbacks will be executed in the same Python process as the one that initiated the initial message from server to front end.

The Pipe properties can be persisted like any other DashApp instance, although it is unlikely that continued persistence of state on each update of this component is likely to be useful.

This component requires a bidirectional connection, such as a websocket, to the server. Inserting a plotly_message_pipe template tag is sufficient.

Configuration options

The PLOTLY_DASH settings variable is used for configuring django-plotly-dash. Default values are shown below.

PLOTLY_DASH = {

    # Route used for the message pipe websocket connection
    "ws_route" :   "dpd/ws/channel",

    # Route used for direct http insertion of pipe messages
    "http_route" : "dpd/views",

    # Flag controlling existince of http poke endpoint
    "http_poke_enabled" : True,

    # Insert data for the demo when migrating
    "insert_demo_migrations" : False,

    # Timeout for caching of initial arguments in seconds
    "cache_timeout_initial_arguments": 60,

    # Name of view wrapping function
    "view_decorator": None,

    # Flag to control location of initial argument storage
    "cache_arguments": True,

    # Flag controlling local serving of assets
    "serve_locally": False,
}

Defaults are inserted for missing values. It is also permissible to not have any PLOTLY_DASH entry in the Django settings file.

The Django staticfiles infrastructure is used to serve all local static files for the Dash apps. This requires adding a setting for the specification of additional static file finders

# Staticfiles finders for locating dash app assets and related files

STATICFILES_FINDERS = [

    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',

    'django_plotly_dash.finders.DashAssetFinder',
    'django_plotly_dash.finders.DashComponentFinder',
    'django_plotly_dash.finders.DashAppDirectoryFinder',
]

and also providing a list of components used

# Plotly components containing static content that should
# be handled by the Django staticfiles infrastructure

PLOTLY_COMPONENTS = [

    # Common components (ie within dash itself) are automatically added

    # django-plotly-dash components
    'dpd_components',
    # static support if serving local assets
    'dpd_static_support',

    # Other components, as needed
    'dash_bootstrap_components',
]

This list should be extended with any additional components that the applications use, where the components have files that have to be served locally. The components that are part of the core dash package are automatically included and do not need to be provided in this list.

Furthermore, middleware should be added for redirection of external assets from underlying packages, such as dash-bootstrap-components. With the standard Django middleware, along with whitenoise, the entry within the settings.py file will look something like

# Standard Django middleware with the addition of both
# whitenoise and django_plotly_dash items

MIDDLEWARE = [

      'django.middleware.security.SecurityMiddleware',

      'whitenoise.middleware.WhiteNoiseMiddleware',

      'django.contrib.sessions.middleware.SessionMiddleware',
      'django.middleware.common.CommonMiddleware',
      'django.middleware.csrf.CsrfViewMiddleware',
      'django.contrib.auth.middleware.AuthenticationMiddleware',
      'django.contrib.messages.middleware.MessageMiddleware',

      'django_plotly_dash.middleware.BaseMiddleware',
      'django_plotly_dash.middleware.ExternalRedirectionMiddleware',

      'django.middleware.clickjacking.XFrameOptionsMiddleware',
  ]

Individual apps can set their serve_locally flag. However, it is recommended to use the equivalent global PLOTLY_DASH setting to provide a common approach for all static assets. See Local assets for more information on how local assets are configured and served as part of the standard Django staticfiles approach, along with details on the integration of other components and some known issues.

Endpoints

The websocket and direct http message endpoints are separately configurable. The configuration options exist to satisfy two requirements

  • Isolate paths that require serving with ASGI. This allows the asynchronous routes - essentially the websocket connections and any other ones from the rest of the application - to be served using daphne or similar, and the bulk of the (synchronous) routes to be served using a WSGI server such as gunicorn.
  • Isolate direct http posting of messages to restrict their use. The motivation behind this http endpoint is to provide a private service that allows other parts of the overall application to send notifications to Dash applications, rather than expose this functionality as part of the public API.

A reverse proxy front end, such as nginx, can route appropriately according to URL.

View decoration

Each view delegated through to plotly_dash can be wrapped using a view decoration function. This enables access to be restricted to logged-in users, or using a desired conditions based on the user and session state.

To restrict all access to logged-in users, use the login_required wrapper:

PLOTLY_DASH = {

    ...
    # Name of view wrapping function
    "view_decorator": "django_plotly_dash.access.login_required",
    ...
}

More information can be found in the view decoration section.

Initial arguments

Initial arguments are stored within the server between the specification of an app in a template tag and the invocation of the view functions for the app. This storage is transient and can be efficiently performed using Django’s caching framework. In some situations, however, a suitably configured cache is not available. For this use case, setting the cache_arguments flag to False will cause initial arguments to be placed inside the Django session.

Local assets

Local ploty dash assets are integrated into the standard Django staticfiles structure. This requires additional settings for both staticfiles finders and middleware, and also providing a list of the components used. The specific steps are listed in the Configuration options section.

Individual applications can set a serve_locally flag but the use of the global setting in the PLOTLY_DASH variable is recommended.

Additional components

Some components, such as dash-bootstrap-components, require external packages such as Bootstrap to be supplied. In turn this can be achieved using for example the bootstrap4 Django application. As a consequence, dependencies on external URLs are introduced.

This can be avoided by use of the dpd-static-support package, which supplies mappings to locally served versions of these assets. Installation is through the standard pip approach

pip install dpd-static-support

and then the package should be added as both an installed app and to the PLOTLY_COMPONENTS list in settings.py, along with the associated middleware

INSTALLED_APPS = [
    ...
    'dpd_static_support',
]

MIDDLEWARE = [
    ...
    'django_plotly_dash.middleware.ExternalRedirectionMiddleware',
]

PLOTLY_COMPONENTS = [
    ...
    'dpd_static_support'
]

Note that the middleware can be safely added even if the serve_locally functionality is not in use.

Known issues

Absolute paths to assets will not work correctly. For example:

app.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')),
                       html.Img(src='assets/image_two.png'),
                       html.Img(src='/assets/image_three.png'),
                       ])

Of these three images, both image_one.png and image_two.png will be served up - through the static files infrastructure - from the assets subdirectory relative to the code defining the app object. However, when rendered the application will attempt to load image_three.png using an absolute path. This is unlikely to be the desired result, but does permit the use of absolute URLs within the server.

Using Bootstrap

The django-plotly-dash package is frequently used with the dash-bootstrap-components package, and this requires a number of steps to set up correctly.

This section is a checkist of the required confiuration steps.

  • install the package as descrbed in the installation section
  • install the static support package with pip install dpd-static-support
  • add the various settings in the configuration section, particularly the STATICFILES_FINDERS, PLOTLY_COMPONENTS and MIDDLEWARE ones.
  • install django-bootstrap 4 with pip install django-bootstrap4 and add bootstrap4 to INSTALLED_APPS in the project’s settings.py file
  • make sure that the settings for serving static files are set correctly, particularly STATIC_ROOT, as described in the Django documentation
  • use the prepare_demo script or perform the equivalent steps, paricularly the migrate and collectstatic steps
  • make sure add_bootstrap_links=True is set for apps using dash-bootstrap-components
  • the Django documentation deployment section covers setting up the serving of static files for production

Demonstration application

There are a number of pages in the demo application in the source repository.

  1. Direct insertion of one or more dash applications
  2. Initial state storage within Django
  3. Enhanced callbacks
  4. Live updating
  5. Injection without using an iframe
  6. Simple html injection
  7. Bootstrap components
  8. Session state storage
  9. Local serving of assets
  10. Multiple callback values

The templates that drive each of these can be found in the github repository.

There is a more details walkthrough of the session state storage example. This example also shows the use of dash bootstrap components.

The demo application can also be viewed online.

Session state example walkthrough

The session state example has three separate components in the demo application

  • A template to render the application
  • The django-plotly-dash application itself
  • A view to render the template having initialised the session state if needed

The first of these is a standard Django template, containing instructions to render the Dash application:

{%load plotly-dash%}

...

<div class="{%plotly_class name="DjangoSessionState"%}">
  {%plotly_app name="DjangoSessionState" ratio=0.3 %}
</div>

The view sets up the initial state of the application prior to rendering. For this example we have a simple variant of rendering a template view:

def session_state_view(request, template_name, **kwargs):

    # Set up a context dict here
    context = { ... values for template go here, see below ... }

    return render(request, template_name=template_name, context=context)

and it suffices to register this view at a convenient URL as it does not use any parameters:

...
url('^demo-eight',
    session_state_view,
    {'template_name':'demo_eight.html'},
    name="demo-eight"),
...

In passing, we note that accepting parameters as part of the URL and passing them as initial parameters to the app through the template is a straightforward extension of this example.

The session state can be accessed in the app as well as the view. The app is essentially formed from a layout function and a number of callbacks. In this particular example, dash-bootstrap-components are used to form the layout:

dis = DjangoDash("DjangoSessionState",
                 add_bootstrap_links=True)

dis.layout = html.Div(
    [
        dbc.Alert("This is an alert", id="base-alert", color="primary"),
        dbc.Alert(children="Danger", id="danger-alert", color="danger"),
        dbc.Button("Update session state", id="update-button", color="warning"),
    ]
)

Within the extended callback, the session state is passed as an extra argument compared to the standard Dash callback:

@dis.callback(
    dash.dependencies.Output("danger-alert", 'children'),
    [dash.dependencies.Input('update-button', 'n_clicks'),]
    )
def session_demo_danger_callback(n_clicks, session_state=None, **kwargs):
    if session_state is None:
        raise NotImplementedError("Cannot handle a missing session state")
    csf = session_state.get('bootstrap_demo_state', None)
    if not csf:
        csf = dict(clicks=0)
        session_state['bootstrap_demo_state'] = csf
    else:
        csf['clicks'] = n_clicks
    return "Button has been clicked %s times since the page was rendered" %n_clicks

The session state is also set during the view:

def session_state_view(request, template_name, **kwargs):

    session = request.session

    demo_count = session.get('django_plotly_dash', {})

    ind_use = demo_count.get('ind_use', 0)
    ind_use += 1
    demo_count['ind_use'] = ind_use
    session['django_plotly_dash'] = demo_count

    # Use some of the information during template rendering
    context = {'ind_use' : ind_use}

    return render(request, template_name=template_name, context=context)

Reloading the demonstration page will cause the page render count to be incremented, and the button click count to be reset. Loading the page in a different session, for example by using a different browser or machine, will have an independent render count.

View decoration

The django-plotly-dash views, as served by Django, can be wrapped with an arbitrary decoration function. This allows the use of the Django login_required view decorator as well as enabling more specialised and fine-grained control.

The login_required decorator

The login_required decorator from the Django authentication system can be used as a view decorator. A wrapper function is provided in django_plotly_dash.access.

PLOTLY_DASH = {

    ...
    # Name of view wrapping function
    "view_decorator": "django_plotly_dash.access.login_required",
    ...
}

Note that the view wrapping is on all of the django-plotly-dash views.

Fine-grained control

The view decoration function is called for each variant exposed in the django_plotly_dash.urls file. As well as the underlying view function, each call to the decorator is given the name of the route, as used by django.urls.reverse, the specific url fragment for the view, and a name describing the type of view.

From this information, it is possible to implement view-specific wrapping of the view functions, and in turn the wrapper functions can then use the request content, along with other information, to control access to the underlying view function.

from django.views.decorators.csrf import csrf_exempt

def check_access_permitted(request, **kwargs):
    # See if access is allowed; if so return True
    # This function is called on each request

    ...

    return True

def user_app_control(view_function, name=None, **kwargs):
    # This function is called on the registration of each django-plotly-dash view
    # name is one of main component-suites routes layout dependencies update-component

    def wrapped_view(request, *args, **kwargs):
        is_permitted = check_access_permitted(request, **kwargs)
        if not is_permitted:
            # Access not permitted, so raise error or generate an appropriate response

            ...

        else:
            return view_function(request, *args, **kwargs)

    if getattr(view_function,"csrf_exempt",False):
        return csrf_exempt(wrapped_view)

    return wrapped_view

The above sketch highlights how access can be controlled based on each request. Note that the csrf_exempt property of any wrapped view is preserved by the decoration function and this approach needs to be extended to other properties if needed. Also, this sketch only passes kwargs to the permission function.

FAQ

  • What Dash versions are supported?

Dash v2.0 onwards is supported. The non-backwards-compatible changes of Dash make supporting earlier versions hard. Note that v1.7.2 is the last version to support (and require) Dash versions prior to v2.0

  • What environment versions are supported?

At least v3.8 of Python, and v2.2 of Django, are needed.

  • Is a virtualenv mandatory?

No, but it is strongly recommended for any Python work.

  • What about Windows?

The python package should work anywhere that Python does. Related applications, such as Redis, have their own requirements but are accessed using standard network protocols.

  • How do I report a bug or other issue?

Create a github issue. See bug reporting for details on what makes a good bug report.

  • Where should Dash layout and callback functions be placed?

In general, the only constraint on the files containing these functions is that they should be imported into the file containing the DjangoDash instantiation. This is discussed in the Installation section and also in this github issue.

  • Can per-user or other fine-grained access control be used?
Yes. See the View decoration configuration setting and View decoration section.
  • What settings are needed to run the server in debug mode?

The prepare_demo script in the root of the git repository contains the full set of commands for running the server in debug mode. In particular, the debug server is launched with the --nostatic option. This will cause the staticfiles to be served from the collected files in the STATIC_ROOT location rather than the normal runserver behaviour of serving directly from the various locations in the STATICFILES_DIRS list.

  • Is use of the get_asset_url function optional for including static assets?

No, it is needed. Consider this example (it is part of demo-nine):

localState = DjangoDash("LocalState",
                        serve_locally=True)

localState.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')),
                              html.Img(src='/assets/image_two.png'),
                              ])

The first Img will have its source file correctly served up by Django as a standard static file. However, the second image will not be rendered as the path will be incorrect.

See the Local assets section for more information on configuration with local assets.

  • Is there a live demo available?

Yes. It can be found here

Development

The application and demo are developed, built and tested in a virtualenv enviroment, supported by a number of bash shell scripts. The resultant package should work on any Python installation that meets the requirements.

Automatic builds have been set up on Travis-CI including running tests and reporting code coverage.

Current status: Travis Badge

Environment setup

To set up a development environment, first clone the repository, and then use the make_env script:

git clone https://github.com/GibbsConsulting/django-plotly-dash.git

cd django-plotly-dash

./make_env

The script creates a virtual environment and uses pip to install the package requirements from the requirements.txt file, and then also the extra packages for development listed in the dev_requirements.txt file. It also installs django-plotly-dash as a development package.

Redis is an optional dependency, and is used for live updates of Dash applications through channels endpoints. The prepare_redis script can be used to install Redis using Docker. It essentially pulls the container and launches it:

# prepare_redis content:
docker pull redis:4
docker run -p 6379:6379 -d redis

The use of Docker is not mandatory, and any method to install Redis can be used provided that the configuration of the host and port for channels is set correcty in the settings.py for the Django project.

During development, it can be convenient to serve the Dash components locally. Whilst passing serve_locally=True to a DjangoDash constructor will cause all of the css and javascript files for the components in that application from the local server, it is recommended to use the global serve_locally configuration setting.

Note that it is not good practice to serve static content in production through Django.

Coding and testing

The pylint and pytest packages are important tools in the development process. The global configuration used for pylint is in the pylintrc file in the root directory of the codebase.

Tests of the package are contained within the django_plotly_dash/tests.py file, and are invoked using the Django settings for the demo. Running the tests from the perspective of the demo also enables code coverage for both the application and the demo to be measured together, simplifying the bookkeeping.

Two helper scripts are provided for running the linter and test code:

# Run pylint on django-plotly-dash module
./check_code_dpd

# Run pylint on the demo code, and then execute the test suite
./check_code_demo

It is also possible to run all of these actions together:

# Run all of the checks
./check_code

The goal is for complete code coverage within the test suite and for maximal (‘ten out of ten’) marks from the linter. Perfection is however very hard and expensive to achieve, so the working requirement is for every release to keep the linter score above 9.5, and ideally improve it, and for the level of code coverage of the tests to increase.

Documentation

Documentation lives in the docs subdirectory as reStructuredText and is built using the sphinx toolchain.

Automatic local building of the documentation is possible with the development environment:

source env/bin/activate
cd docs && sphinx-autobuild . _build/html

In addition, the grip tool can be used to serve a rendered version of the README file:

source env/bin/activate
grip

The online documentation is automatically built by the readthedocs infrastructure when a release is formed in the main github repository.

Release builds

This section contains the recipe for building a release of the project.

First, update the version number appropriately in django_plotly_dash/version.py, and then ensure that the checks and tests have been run:

./check_code

Next, construct the pip packages and push them to pypi:

source env/bin/activate

python setup.py sdist
python setup.py bdist_wheel

twine upload dist/*

Committing a new release to the main github repository will invoke a build of the online documentation, but first a snapshot of the development environment used for the build should be generated:

pip freeze > frozen_dev.txt

git add frozen_dev.txt
git add django_plotly_dash/version.py

git commit -m" ... suitable commit message for this release ..."

# Create PR, merge into main repo, check content on PYPI and RTD

This preserves the state used for building and testing for future reference.

Bug reports and other issues

The ideal bug report is a pull request containing the addition of a failing test exhibiting the problem to the test suite. However, this rarely happens in practice!

The essential requirement of a bug report is that it contains enough information to characterise the issue, and ideally also provides some way of replicating it. Issues that cannot be replicated within a virtualenv are unlikely to get much attention, if any.

To report a bug, create a github issue.

License

The django-plotly-dash package is made available under the MIT license.

The license text can be found in the LICENSE file in the root directory of the source code, along with a CONTRIBUTIONS.md file that includes a list of the contributors to the codebase.

A copy of the license, correct at the time of writing of this documentation, follows:

MIT License

Copyright (c) 2018 Gibbs Consulting and others - see CONTRIBUTIONS.md

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