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
daphneor similar, and the bulk of the (synchronous) routes to be served using a WSGI server such asgunicorn.- 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
Dashapplications, 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-bootstrap4and addbootstrap4to INSTALLED_APPS in the project’ssettings.pyfile - make sure that the settings for serving static files are set correctly, particularly STATIC_ROOT, as described in the Django documentation
- use the
prepare_demoscript or perform the equivalent steps, paricularly themigrateandcollectstaticsteps - make sure
add_bootstrap_links=Trueis set for apps usingdash-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.
- Direct insertion of one or more dash applications
- Initial state storage within Django
- Enhanced callbacks
- Live updating
- Injection without using an iframe
- Simple html injection
- Bootstrap components
- Session state storage
- Local serving of assets
- 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-dashapplication 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
virtualenvmandatory?
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
Dashlayout 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_urlfunction 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:
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.