From ef92858b286dfe4cb2e004ddce531ead6dac31e2 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Mon, 6 Aug 2018 20:12:47 -0700 Subject: [PATCH 01/15] Stubbed out interpolate_index function --- django_plotly_dash/dash_wrapper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 0be40a20..837733ac 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -456,3 +456,9 @@ def extra_html_properties(self, prefix=None, postfix=None, template_type=None): 'template_type':template_type, 'prefix':prefix, } + + def interpolate_index(self, **kwargs): + resp = super(WrappedDash, self).interpolate_index(**kwargs) + + #print(resp) + return resp From 814c4a1fd9fed3867ac9b7dfda9b43055b06e803 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 7 Aug 2018 00:17:43 -0700 Subject: [PATCH 02/15] First cut of direct html insertion of a dash app using a django template --- demo/demo/settings.py | 3 + demo/demo/templates/base.html | 6 +- django_plotly_dash/dash_wrapper.py | 44 +++++++++++- django_plotly_dash/middleware.py | 61 +++++++++++++++++ .../django_plotly_dash/plotly_direct.html | 3 + .../templatetags/plotly_dash.py | 67 ++++++++++++------- 6 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 django_plotly_dash/middleware.py create mode 100644 django_plotly_dash/templates/django_plotly_dash/plotly_direct.html diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 6aeb2005..9871f2b6 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -51,6 +51,9 @@ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + + 'django_plotly_dash.middleware.BaseMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] diff --git a/demo/demo/templates/base.html b/demo/demo/templates/base.html index a3bb833b..784ffe64 100644 --- a/demo/demo/templates/base.html +++ b/demo/demo/templates/base.html @@ -10,6 +10,7 @@ {%block app_header_css%} {%endblock%} + {%plotly_header%} Django Plotly Dash Examples - {%block title%}{%endblock%} @@ -39,6 +40,7 @@ {%block footer%} {%endblock%} - -{%block post_body%}{%endblock%} + + {%block post_body%}{%endblock%} + {%plotly_footer%} diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 837733ac..992844e2 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -237,6 +237,8 @@ def __init__(self, self._replacements = dict() self._use_dash_layout = len(self._replacements) < 1 + self._return_embedded = False + def use_dash_dispatch(self): 'Indicate if dispatch is using underlying dash code or the wrapped code' return self._dash_dispatch @@ -457,8 +459,44 @@ def extra_html_properties(self, prefix=None, postfix=None, template_type=None): 'prefix':prefix, } + def index(self, *args, **kwargs): # pylint: disable=unused-argument + scripts = self._generate_scripts_html() + css = self._generate_css_dist_html() + config = self._generate_config_html() + metas = self._generate_meta_html() + title = getattr(self, 'title', 'Dash') + if self._favicon: + favicon = ''.format( + flask.url_for('assets.static', filename=self._favicon)) + else: + favicon = '' + + _app_entry = ''' +
+
+ Loading Django-Plotly-Dash app +
+
+''' + index = self.interpolate_index( + metas=metas, title=title, css=css, config=config, + scripts=scripts, app_entry=_app_entry, favicon=favicon) + + return index + def interpolate_index(self, **kwargs): - resp = super(WrappedDash, self).interpolate_index(**kwargs) - #print(resp) - return resp + if not self._return_embedded: + resp = super(WrappedDash, self).interpolate_index(**kwargs) + return resp + + self._return_embedded.add_css(kwargs['css']) + self._return_embedded.add_config(kwargs['config']) + self._return_embedded.add_scripts(kwargs['scripts']) + + return kwargs['app_entry'] + + def set_embedded(self, embedded_holder=None): + self._return_embedded = embedded_holder if embedded_holder else EmbeddedHolder() + def exit_embedded(self): + self._return_embedded = False diff --git a/django_plotly_dash/middleware.py b/django_plotly_dash/middleware.py new file mode 100644 index 00000000..27d09fbe --- /dev/null +++ b/django_plotly_dash/middleware.py @@ -0,0 +1,61 @@ +''' +Django-plotly-dash middleware + +This middleware enables the collection of items from templates for inclusion in the header and footer +''' + +class EmbeddedHolder(object): + def __init__(self): + self.css = "" + self.config = "" + self.scripts = "" + def add_css(self, css): + if css: + self.css = css + def add_config(self, config): + if config: + self.config = config + def add_scripts(self, scripts): + if scripts: + self.scripts = scripts + +class ContentCollector: + def __init__(self): + self.header_placeholder = "DJANGO_PLOTLY_DASH_HEADER_PLACEHOLDER" + self.footer_placeholder = "DJANGO_PLOTLY_DASH_FOOTER_PLACEHOLDER" + + self.embedded_holder = EmbeddedHolder() + self._encoding = "utf-8" + + def adjust_response(self, response): + + c1 = self._replace(response.content, + self.header_placeholder, + self.embedded_holder.css) + + response.content = self._replace(c1, + self.footer_placeholder, + "\n".join([self.embedded_holder.config, + self.embedded_holder.scripts])) + + return response + + def _replace(self, content, placeholder, substitution): + return content.replace(self._encode(placeholder), + self._encode(substitution if substitution else "")) + + def _encode(self, string): + return string.encode(self._encoding) + +class BaseMiddleware: + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + + request.dpd_content_handler = ContentCollector() + response = self.get_response(request) + response = request.dpd_content_handler.adjust_response(response) + + return response diff --git a/django_plotly_dash/templates/django_plotly_dash/plotly_direct.html b/django_plotly_dash/templates/django_plotly_dash/plotly_direct.html new file mode 100644 index 00000000..284ea601 --- /dev/null +++ b/django_plotly_dash/templates/django_plotly_dash/plotly_direct.html @@ -0,0 +1,3 @@ +
+ {%autoescape off%}{{resp}}{%endautoescape%} +
diff --git a/django_plotly_dash/templatetags/plotly_dash.py b/django_plotly_dash/templatetags/plotly_dash.py index e92482ab..d9cb6beb 100644 --- a/django_plotly_dash/templatetags/plotly_dash.py +++ b/django_plotly_dash/templatetags/plotly_dash.py @@ -36,6 +36,21 @@ ws_default_url = "/%s" % pipe_ws_endpoint_name() +def _locate_daapp(name, slug, da, cache_id=None): + + app = None + + if name is not None: + da, app = DashApp.locate_item(name, stateless=True, cache_id=cache_id) + + if slug is not None: + da, app = DashApp.locate_item(slug, stateless=False, cache_id=cache_id) + + if not app: + app = da.as_dash_instance() + + return da, app + @register.inclusion_tag("django_plotly_dash/plotly_app.html", takes_context=True) def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborder=False, initial_arguments=None): 'Insert a dash application using a html iframe' @@ -57,8 +72,6 @@ def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborde height: 100%; """ - app = None - if initial_arguments: # Generate a cache id cache_id = "dpd-initial-args-%s" % str(uuid.uuid4()).replace('-', '') @@ -67,14 +80,35 @@ def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborde else: cache_id = None - if name is not None: - da, app = DashApp.locate_item(name, stateless=True, cache_id=cache_id) + da, app = _locate_daapp(name, slug, da, cache_id=cache_id) - if slug is not None: - da, app = DashApp.locate_item(slug, stateless=False, cache_id=cache_id) + return locals() - if not app: - app = da.as_dash_instance(cache_id=cache_id) +@register.simple_tag(takes_context=True) +def plotly_header(context): + 'Insert placeholder for django-plotly-dash header content' + return context.request.dpd_content_handler.header_placeholder + +@register.simple_tag(takes_context=True) +def plotly_footer(context): + 'Insert placeholder for django-plotly-dash footer content' + return context.request.dpd_content_handler.footer_placeholder + +@register.inclusion_tag("django_plotly_dash/plotly_direct.html", takes_context=True) +def plotly_direct(context, name=None, slug=None, da=None): + 'Direct insertion of a Dash app' + + da, app = _locate_daapp(name, slug, da) + + view_func = app.locate_endpoint_function() + + # Load embedded holder inserted by middleware + eh = context.request.dpd_content_handler.embedded_holder + app.set_embedded(eh) + try: + resp = view_func() + finally: + app.exit_embedded() return locals() @@ -87,14 +121,8 @@ def plotly_message_pipe(context, url=None): @register.simple_tag() def plotly_app_identifier(name=None, slug=None, da=None, postfix=None): 'Return a slug-friendly identifier' - if name is not None: - da, app = DashApp.locate_item(name, stateless=True) - - if slug is not None: - da, app = DashApp.locate_item(slug, stateless=False) - if not app: - app = da.as_dash_instance() + da, app = _locate_daapp(name, slug, da) slugified_id = app.slugified_id() @@ -106,14 +134,7 @@ def plotly_app_identifier(name=None, slug=None, da=None, postfix=None): def plotly_class(name=None, slug=None, da=None, prefix=None, postfix=None, template_type=None): 'Return a string of space-separated class names' - if name is not None: - da, app = DashApp.locate_item(name, stateless=True) - - if slug is not None: - da, app = DashApp.locate_item(slug, stateless=False) - - if not app: - app = da.as_dash_instance() + da, app = _locate_daapp(name, slug, da) return app.extra_html_properties(prefix=prefix, postfix=postfix, From a29fbd082cb03311f712464affb551ba595c3e75 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 21 Aug 2018 21:43:23 -0700 Subject: [PATCH 03/15] Remove serve locally for a bootstrap-using app --- demo/demo/plotly_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/demo/plotly_apps.py b/demo/demo/plotly_apps.py index 68ed4538..9ca73464 100644 --- a/demo/demo/plotly_apps.py +++ b/demo/demo/plotly_apps.py @@ -117,7 +117,7 @@ def callback_c(*args, **kwargs): return "Args are [%s] and kwargs are %s" %(",".join(args), str(kwargs)) liveIn = DjangoDash("LiveInput", - serve_locally=True, + serve_locally=False, add_bootstrap_links=True) liveIn.layout = html.Div([ From f0d04a1c290d9a395b3a9f522362fd63932abaa8 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 21 Aug 2018 22:33:17 -0700 Subject: [PATCH 04/15] Added a demo of direct html injection --- demo/demo/templates/demo_five.html | 35 ++++++++++++++++++++++++++++++ demo/demo/urls.py | 1 + 2 files changed, 36 insertions(+) create mode 100644 demo/demo/templates/demo_five.html diff --git a/demo/demo/templates/demo_five.html b/demo/demo/templates/demo_five.html new file mode 100644 index 00000000..4611ee96 --- /dev/null +++ b/demo/demo/templates/demo_five.html @@ -0,0 +1,35 @@ +{%extends "base.html"%} +{%load plotly_dash%} + +{%block title%}Demo One - Simple Embedding{%endblock%} + +{%block content%} +

Simple App Embedding

+

+ This is a simple example of use of a dash application within a Django template. Use of + the plotly_app template tag with the name of a dash application represents the simplest use of + the django_plotly_dash framework. +

+

+ The plotly_class tag is also used to wrap the application in css class names based on the + application (django-plotly-dash), the + type of the embedding (iframe), and the slugified version of the app name (simpleexample). +

+
+
+

{% load plotly_dash %}

+

<div class="{% plotly_class name="SimpleExample"%}"> +

{% plotly_direct name="SimpleExample" %}

+

<\div> +

+
+

+
+
+
+ {%plotly_direct name="SimpleExample"%} +
+
+
+

+{%endblock%} diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 1dff0780..64910928 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -36,6 +36,7 @@ url('^demo-two$', TemplateView.as_view(template_name='demo_two.html'), name="demo-two"), url('^demo-three$', TemplateView.as_view(template_name='demo_three.html'), name="demo-three"), url('^demo-four$', TemplateView.as_view(template_name='demo_four.html'), name="demo-four"), + url('^demo-five$', TemplateView.as_view(template_name='demo_five.html'), name="demo-five"), url('^admin/', admin.site.urls), url('^django_plotly_dash/', include('django_plotly_dash.urls')), From 28ce1c456eed486f9d6656d3d2b8dc2b123e2c23 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 21 Aug 2018 22:36:00 -0700 Subject: [PATCH 05/15] Add fifth demo to list --- demo/demo/templates/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html index 8c17c5cc..4924e8f5 100644 --- a/demo/demo/templates/index.html +++ b/demo/demo/templates/index.html @@ -11,5 +11,6 @@

Demonstration Application

  • Demo Two - storage of application initial state within Django
  • Demo Three - adding Django features with enhanced callbacks
  • Demo Four - live updating of apps by pushing from the Django server
  • +
  • Demo Five - injection of a Dash application without embedding in an html iframe
  • {%endblock%} From 1715385c87377298a296628317596e4f743e0937 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 22 Aug 2018 13:49:23 -0700 Subject: [PATCH 06/15] Added demo five for explicit embedding --- demo/demo/templates/base.html | 9 +++++---- demo/demo/templates/demo_five.html | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/demo/demo/templates/base.html b/demo/demo/templates/base.html index 784ffe64..93015127 100644 --- a/demo/demo/templates/base.html +++ b/demo/demo/templates/base.html @@ -22,10 +22,11 @@ {%block demo_items%} Contents - Demo One - Simple Use - Demo Two - Initial State - Demo Three - Enhanced Callbacks - Demo Four - Live Updating + One - Simple Use + Two - Initial State + Three - Enhanced Callbacks + Four - Live Updating + Five - Direct Embedding Online Documentation diff --git a/demo/demo/templates/demo_five.html b/demo/demo/templates/demo_five.html index 4611ee96..36209ce5 100644 --- a/demo/demo/templates/demo_five.html +++ b/demo/demo/templates/demo_five.html @@ -4,11 +4,11 @@ {%block title%}Demo One - Simple Embedding{%endblock%} {%block content%} -

    Simple App Embedding

    +

    Direct App Embedding

    This is a simple example of use of a dash application within a Django template. Use of - the plotly_app template tag with the name of a dash application represents the simplest use of - the django_plotly_dash framework. + the plotly_direct template tag with the name of a dash application causes + the Dash application to be directly embedded within the page.

    The plotly_class tag is also used to wrap the application in css class names based on the @@ -18,7 +18,7 @@

    Simple App Embedding

    {% load plotly_dash %}

    -

    <div class="{% plotly_class name="SimpleExample"%}"> +

    <div class="{% plotly_class name="SimpleExample" template_type="div-direct"%}">

    {% plotly_direct name="SimpleExample" %}

    <\div>

    @@ -26,7 +26,7 @@

    Simple App Embedding

    -
    +
    {%plotly_direct name="SimpleExample"%}
    From 4798532cc76481a60dc94820a910757bf4abd696 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 22 Aug 2018 13:50:12 -0700 Subject: [PATCH 07/15] Improve descriptive text --- demo/demo/templates/demo_five.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/demo/templates/demo_five.html b/demo/demo/templates/demo_five.html index 36209ce5..077944db 100644 --- a/demo/demo/templates/demo_five.html +++ b/demo/demo/templates/demo_five.html @@ -13,7 +13,7 @@

    Direct App Embedding

    The plotly_class tag is also used to wrap the application in css class names based on the application (django-plotly-dash), the - type of the embedding (iframe), and the slugified version of the app name (simpleexample). + type of the embedding (here labelled "div-direct"), and the slugified version of the app name (simpleexample).

    From 3d38a728371d8b995aa69ea12a88fc42cc381ba3 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 28 Aug 2018 13:39:21 -0700 Subject: [PATCH 08/15] Adding demo of injected html from eddy-ojb --- demo/demo/dash_apps.py | 104 ++++++++++++++++++++++++++++++ demo/demo/templates/base.html | 41 ++++++------ demo/demo/templates/demo_six.html | 36 +++++++++++ demo/demo/templates/index.html | 1 + demo/demo/urls.py | 4 ++ demo/demo/views.py | 16 +++++ 6 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 demo/demo/dash_apps.py create mode 100644 demo/demo/templates/demo_six.html create mode 100644 demo/demo/views.py diff --git a/demo/demo/dash_apps.py b/demo/demo/dash_apps.py new file mode 100644 index 00000000..71ba1893 --- /dev/null +++ b/demo/demo/dash_apps.py @@ -0,0 +1,104 @@ +'''Dash demonstration application + +TODO attribution here +''' + +import dash +import dash_core_components as dcc +import dash_html_components as html +import plotly.graph_objs as go +#import dpd_components as dpd +import numpy as np +from django_plotly_dash import DjangoDash + +#from .urls import app_name +app_name ="DPD demo application" + +dashboard_name1 = 'dash_example_1' +dash_example1 = DjangoDash(name=dashboard_name1, + serve_locally=True, + app_name=app_name + ) + +# Below is a random Dash app. +# I encountered no major problems in using Dash this way. I did encounter problems but it was because +# I was using e.g. Bootstrap inconsistenyly across the dash layout. Staying consistent worked fine for me. +dash_example1.layout = html.Div(id='main', + children=[ + + html.Div([ + dcc.Dropdown( + id='my-dropdown1', + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': 'Montreal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} + ], + value='NYC', + className='col-md-12', + + ), + html.Div(id='test-output-div') + ]), + + dcc.Dropdown( + id='my-dropdown2', + options=[ + {'label': 'Oranges', 'value': 'Oranges'}, + {'label': 'Plums', 'value': 'Plums'}, + {'label': 'Peaches', 'value': 'Peaches'} + ], + value='Oranges', + className='col-md-12', + ), + + html.Div(id='test-output-div2') + + ]) # end of 'main' + +@dash_example1.expanded_callback( + dash.dependencies.Output('test-output-div', 'children'), + [dash.dependencies.Input('my-dropdown1', 'value')]) +def callback_test(*args, **kwargs): + + # Creating a random Graph from a Plotly example: + N = 500 + random_x = np.linspace(0, 1, N) + random_y = np.random.randn(N) + + # Create a trace + trace = go.Scatter( + x = random_x, + y = random_y + ) + + data = [trace] + + layout = dict(title='', + yaxis = dict(zeroline = False, title='Total Expense (£)',), + xaxis = dict(zeroline = False, title='Date', tickangle=0), + margin=dict(t=20, b=50, l=50, r=40), + height=350, + ) + + + fig = dict(data=data, layout=layout) + line_graph = dcc.Graph(id='line-area-graph2', figure=fig, style={'display':'inline-block', 'width':'100%', + 'height': '100%;'} ) + children = [line_graph] + + return children + + +@dash_example1.expanded_callback( + dash.dependencies.Output('test-output-div2', 'children'), + [dash.dependencies.Input('my-dropdown2', 'value')]) +def callback_test2(*args, **kwargs): + + print(args) + print(kwargs) + + children = [html.Div(["You have selected %s." %(args[0])]), + html.Div(["The session context message is '%s'" %(kwargs['session_state']['django_to_dash_context'])])] + + return children diff --git a/demo/demo/templates/base.html b/demo/demo/templates/base.html index 93015127..cba3e48e 100644 --- a/demo/demo/templates/base.html +++ b/demo/demo/templates/base.html @@ -1,17 +1,17 @@ - {%load plotly_dash%} - {%load staticfiles%} - {%load bootstrap4%} - {%bootstrap_css%} - {%bootstrap_javascript jquery="full"%} - {%block extra_header%}{%endblock%} - {%block app_header_css%} + {% load plotly_dash%} + {% load staticfiles%} + {% load bootstrap4%} + {% bootstrap_css%} + {% bootstrap_javascript jquery="full"%} + {% block extra_header%}{% endblock%} + {% block app_header_css%} - {%endblock%} - {%plotly_header%} - Django Plotly Dash Examples - {%block title%}{%endblock%} + {% endblock%} + {% plotly_header%} + Django Plotly Dash Examples - {% block title%}{% endblock%}
    @@ -20,28 +20,33 @@ Logo - {%block demo_items%} + {% block demo_items %} Contents + {% block demo_menu_items %} + {% if 0 %} One - Simple Use Two - Initial State Three - Enhanced Callbacks Four - Live Updating - Five - Direct Embedding + Five - Direct Injection + Six - Simple Injection + {% endif %} + {% endblock %} Online Documentation - {%endblock%} + {% endblock %}
    - {%block content%}{%endblock%} + {% block content%}{% endblock%}
    - {%block footer%} - {%endblock%} + {% block footer%} + {% endblock%} - {%block post_body%}{%endblock%} - {%plotly_footer%} + {% block post_body%}{% endblock%} + {% plotly_footer%} diff --git a/demo/demo/templates/demo_six.html b/demo/demo/templates/demo_six.html new file mode 100644 index 00000000..8563cad9 --- /dev/null +++ b/demo/demo/templates/demo_six.html @@ -0,0 +1,36 @@ +{%extends "base.html"%} +{%load plotly_dash%} + +{%block title%}Demo Six - Simple Injection{%endblock%} + +{%block content%} + +

    Simple embedding

    + +

    +Direct insertion of html into a web page. +

    + +

    +This demo is based on a contribution by, and +with thanks to, @eddy-ojb +

    + +
    +
    +

    {% load plotly_dash %}

    +

    <div class="{% plotly_class name="dash_example_1" template_type="div-direct"%}"> +

    {% plotly_direct name="dash_example_1" %}

    +

    <\div> +

    +
    +

    +
    +
    +
    + {%plotly_direct name="dash_example_1"%} +
    +
    +
    +

    +{%endblock%} diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html index 4924e8f5..921a6b06 100644 --- a/demo/demo/templates/index.html +++ b/demo/demo/templates/index.html @@ -12,5 +12,6 @@

    Demonstration Application

  • Demo Three - adding Django features with enhanced callbacks
  • Demo Four - live updating of apps by pushing from the Django server
  • Demo Five - injection of a Dash application without embedding in an html iframe
  • +
  • Demo Six - simple html injection example
  • {%endblock%} diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 64910928..c6f9d4dd 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -27,9 +27,12 @@ # Load demo plotly apps - this triggers their registration import demo.plotly_apps # pylint: disable=unused-import +import demo.dash_apps # pylint: disable=unused-import from django_plotly_dash.views import add_to_session +from .views import dash_example_1_view + urlpatterns = [ url('^$', TemplateView.as_view(template_name='index.html'), name="home"), url('^demo-one$', TemplateView.as_view(template_name='demo_one.html'), name="demo-one"), @@ -37,6 +40,7 @@ url('^demo-three$', TemplateView.as_view(template_name='demo_three.html'), name="demo-three"), url('^demo-four$', TemplateView.as_view(template_name='demo_four.html'), name="demo-four"), url('^demo-five$', TemplateView.as_view(template_name='demo_five.html'), name="demo-five"), + url('^demo-six', dash_example_1_view, name="demo-six"), url('^admin/', admin.site.urls), url('^django_plotly_dash/', include('django_plotly_dash.urls')), diff --git a/demo/demo/views.py b/demo/demo/views.py new file mode 100644 index 00000000..c389e459 --- /dev/null +++ b/demo/demo/views.py @@ -0,0 +1,16 @@ +''' +Example view generating non-trivial content +''' + +from django.shortcuts import render + +def dash_example_1_view(request, template_name="demo_six.html",**kwargs): + + context = {} + + # create some context to send over to Dash: + dash_context = request.session.get("django_plotly_dash", dict()) + dash_context['django_to_dash_context'] = "I am Dash receiving context from Django" + request.session['django_plotly_dash'] = dash_context + + return render(request, template_name=template_name, context=context) From e97434e87db49cbc3af88e98de0512ea99973124 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 28 Aug 2018 13:53:34 -0700 Subject: [PATCH 09/15] Tidy up some of the linter objections --- demo/demo/dash_apps.py | 56 +++++++++++++++++++----------------------- demo/demo/views.py | 9 ++++--- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/demo/demo/dash_apps.py b/demo/demo/dash_apps.py index 71ba1893..95c3b89a 100644 --- a/demo/demo/dash_apps.py +++ b/demo/demo/dash_apps.py @@ -12,34 +12,29 @@ from django_plotly_dash import DjangoDash #from .urls import app_name -app_name ="DPD demo application" +app_name = "DPD demo application" dashboard_name1 = 'dash_example_1' dash_example1 = DjangoDash(name=dashboard_name1, - serve_locally=True, - app_name=app_name - ) + serve_locally=True, + app_name=app_name + ) # Below is a random Dash app. # I encountered no major problems in using Dash this way. I did encounter problems but it was because # I was using e.g. Bootstrap inconsistenyly across the dash layout. Staying consistent worked fine for me. dash_example1.layout = html.Div(id='main', - children=[ - - html.Div([ - dcc.Dropdown( - id='my-dropdown1', - options=[ - {'label': 'New York City', 'value': 'NYC'}, - {'label': 'Montreal', 'value': 'MTL'}, - {'label': 'San Francisco', 'value': 'SF'} - ], - value='NYC', - className='col-md-12', - - ), - html.Div(id='test-output-div') - ]), + children=[ + html.Div([dcc.Dropdown(id='my-dropdown1', + options=[{'label': 'New York City', 'value': 'NYC'}, + {'label': 'Montreal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} + ], + value='NYC', + className='col-md-12', + ), + html.Div(id='test-output-div') + ]), dcc.Dropdown( id='my-dropdown2', @@ -59,7 +54,8 @@ @dash_example1.expanded_callback( dash.dependencies.Output('test-output-div', 'children'), [dash.dependencies.Input('my-dropdown1', 'value')]) -def callback_test(*args, **kwargs): +def callback_test(*args, **kwargs): #pylint: disable=unused-argument + 'Callback to generate test data on each change of the dropdown' # Creating a random Graph from a Plotly example: N = 500 @@ -67,24 +63,22 @@ def callback_test(*args, **kwargs): random_y = np.random.randn(N) # Create a trace - trace = go.Scatter( - x = random_x, - y = random_y - ) + trace = go.Scatter(x=random_x, + y=random_y) data = [trace] layout = dict(title='', - yaxis = dict(zeroline = False, title='Total Expense (£)',), - xaxis = dict(zeroline = False, title='Date', tickangle=0), - margin=dict(t=20, b=50, l=50, r=40), - height=350, - ) + yaxis=dict(zeroline=False, title='Total Expense (£)',), + xaxis=dict(zeroline=False, title='Date', tickangle=0), + margin=dict(t=20, b=50, l=50, r=40), + height=350, + ) fig = dict(data=data, layout=layout) line_graph = dcc.Graph(id='line-area-graph2', figure=fig, style={'display':'inline-block', 'width':'100%', - 'height': '100%;'} ) + 'height':'100%;'} ) children = [line_graph] return children diff --git a/demo/demo/views.py b/demo/demo/views.py index c389e459..b73a2f1f 100644 --- a/demo/demo/views.py +++ b/demo/demo/views.py @@ -4,10 +4,13 @@ from django.shortcuts import render -def dash_example_1_view(request, template_name="demo_six.html",**kwargs): - +#pylint: disable=unused-argument + +def dash_example_1_view(request, template_name="demo_six.html", **kwargs): + 'Example view that inserts content into the dash context passed to the dash application' + context = {} - + # create some context to send over to Dash: dash_context = request.session.get("django_plotly_dash", dict()) dash_context['django_to_dash_context'] = "I am Dash receiving context from Django" From 82ae4e3f82be1d6bdafca33bc351acfadc859bbf Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 28 Aug 2018 20:14:33 -0700 Subject: [PATCH 10/15] Added test for app in demo six --- demo/demo/tests/test_dpd_demo.py | 2 +- django_plotly_dash/tests.py | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/demo/demo/tests/test_dpd_demo.py b/demo/demo/tests/test_dpd_demo.py index 23b89239..d1d7227d 100644 --- a/demo/demo/tests/test_dpd_demo.py +++ b/demo/demo/tests/test_dpd_demo.py @@ -10,7 +10,7 @@ def test_template_tag_use(client): 'Check use of template tag' - for name in ['demo-one', 'demo-two', 'demo-three', 'demo-four',]: + for name in ['demo-one', 'demo-two', 'demo-three', 'demo-four', 'demo-five', 'demo-six',]: url = reverse(name, kwargs={}) response = client.get(url) diff --git a/django_plotly_dash/tests.py b/django_plotly_dash/tests.py index 737598fe..70c13d34 100644 --- a/django_plotly_dash/tests.py +++ b/django_plotly_dash/tests.py @@ -119,3 +119,85 @@ def test_updating(client): assert response.content == b'{"response": {"props": {"children": "The chosen T-shirt is a medium blue one."}}}' assert response.status_code == 200 + +@pytest.mark.django_db +def test_injection_app_access(client): + 'Check direct use of a stateless application using demo test data' + + from django.urls import reverse + from .app_name import main_view_label + + for route_name in ['layout', 'dependencies', main_view_label]: + for prefix, arg_map in [('app-', {'ident':'dash_example_1'}), + #('', {'ident':'simpleexample-1'}), + ]: + url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map) + + response = client.get(url) + + assert response.content + assert response.status_code == 200 + + for route_name in ['routes',]: + for prefix, arg_map in [('app-', {'ident':'dash_example_1'}),]: + url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map) + + did_fail = False + try: + response = client.get(url) + except: + did_fail = True + + assert did_fail + +@pytest.mark.django_db +def test_injection_updating(client): + 'Check updating of an app using demo test data' + + import json + from django.urls import reverse + + route_name = 'update-component' + + for prefix, arg_map in [('app-', {'ident':'dash_example_1'}),]: + url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map) + + response = client.post(url, json.dumps({'output':{'id':'test-output-div', 'property':'children'}, + 'inputs':[{'id':'my-dropdown1', + 'property':'value', + 'value':'TestIt'}, + ]}), content_type="application/json") + + rStart = b'{"response": {"props": {"children":' + + assert response.content[:len(rStart)] == rStart + assert response.status_code == 200 + + have_thrown = False + + try: + response2 = client.post(url, json.dumps({'output':{'id':'test-output-div2', 'property':'children'}, + 'inputs':[{'id':'my-dropdown2', + 'property':'value', + 'value':'TestIt'}, + ]}), content_type="application/json") + except: + have_thrown = True + + assert have_thrown + + session = client.session + session['django_plotly_dash'] = {'django_to_dash_context': 'Test 789 content'} + session.save() + + response3 = client.post(url, json.dumps({'output':{'id':'test-output-div2', 'property':'children'}, + 'inputs':[{'id':'my-dropdown2', + 'property':'value', + 'value':'TestIt'}, + ]}), content_type="application/json") + rStart3 = b'{"response": {"props": {"children":' + + assert response3.content[:len(rStart3)] == rStart3 + assert response3.status_code == 200 + + assert response3.content.find(b'Test 789 content') > 0 From 1d973dd1e8c53eccfbb9c0af2c65cce85c6fcce7 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 29 Aug 2018 08:23:48 -0700 Subject: [PATCH 11/15] Improve code based on linter report --- demo/demo/dash_apps.py | 28 ++++++++++++++++------------ django_plotly_dash/dash_wrapper.py | 7 ++++++- django_plotly_dash/middleware.py | 14 ++++++++++++++ django_plotly_dash/tests.py | 12 +++++++----- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/demo/demo/dash_apps.py b/demo/demo/dash_apps.py index 95c3b89a..e8a5a55a 100644 --- a/demo/demo/dash_apps.py +++ b/demo/demo/dash_apps.py @@ -3,6 +3,9 @@ TODO attribution here ''' +# The linter doesn't like the members of the html and dcc imports (as they are dynamic?) +#pylint: disable=no-member + import dash import dash_core_components as dcc import dash_html_components as html @@ -25,16 +28,16 @@ # I was using e.g. Bootstrap inconsistenyly across the dash layout. Staying consistent worked fine for me. dash_example1.layout = html.Div(id='main', children=[ - html.Div([dcc.Dropdown(id='my-dropdown1', - options=[{'label': 'New York City', 'value': 'NYC'}, - {'label': 'Montreal', 'value': 'MTL'}, - {'label': 'San Francisco', 'value': 'SF'} - ], - value='NYC', - className='col-md-12', - ), - html.Div(id='test-output-div') - ]), + html.Div([dcc.Dropdown(id='my-dropdown1', + options=[{'label': 'New York City', 'value': 'NYC'}, + {'label': 'Montreal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} + ], + value='NYC', + className='col-md-12', + ), + html.Div(id='test-output-div') + ]), dcc.Dropdown( id='my-dropdown2', @@ -49,7 +52,7 @@ html.Div(id='test-output-div2') - ]) # end of 'main' + ]) # end of 'main' @dash_example1.expanded_callback( dash.dependencies.Output('test-output-div', 'children'), @@ -78,7 +81,7 @@ def callback_test(*args, **kwargs): #pylint: disable=unused-argument fig = dict(data=data, layout=layout) line_graph = dcc.Graph(id='line-area-graph2', figure=fig, style={'display':'inline-block', 'width':'100%', - 'height':'100%;'} ) + 'height':'100%;'}) children = [line_graph] return children @@ -88,6 +91,7 @@ def callback_test(*args, **kwargs): #pylint: disable=unused-argument dash.dependencies.Output('test-output-div2', 'children'), [dash.dependencies.Input('my-dropdown2', 'value')]) def callback_test2(*args, **kwargs): + 'Callback to exercise session functionality' print(args) print(kwargs) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 992844e2..630d54ca 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -36,6 +36,7 @@ from plotly.utils import PlotlyJSONEncoder from .app_name import app_name, main_view_label +from .middleware import EmbeddedHolder uid_counter = 0 @@ -466,6 +467,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument metas = self._generate_meta_html() title = getattr(self, 'title', 'Dash') if self._favicon: + import flask favicon = ''.format( flask.url_for('assets.static', filename=self._favicon)) else: @@ -484,7 +486,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument return index - def interpolate_index(self, **kwargs): + def interpolate_index(self, **kwargs): #pylint: disable=arguments-differ if not self._return_embedded: resp = super(WrappedDash, self).interpolate_index(**kwargs) @@ -497,6 +499,9 @@ def interpolate_index(self, **kwargs): return kwargs['app_entry'] def set_embedded(self, embedded_holder=None): + 'Set a handler for embedded references prior to evaluating a view function' self._return_embedded = embedded_holder if embedded_holder else EmbeddedHolder() + def exit_embedded(self): + 'Exit the embedded section after processing a view' self._return_embedded = False diff --git a/django_plotly_dash/middleware.py b/django_plotly_dash/middleware.py index 27d09fbe..75fede53 100644 --- a/django_plotly_dash/middleware.py +++ b/django_plotly_dash/middleware.py @@ -4,22 +4,34 @@ This middleware enables the collection of items from templates for inclusion in the header and footer ''' +#pylint: disable=too-few-public-methods + class EmbeddedHolder(object): + 'Hold details of embedded content from processing a view' def __init__(self): self.css = "" self.config = "" self.scripts = "" def add_css(self, css): + 'Add css content' if css: self.css = css def add_config(self, config): + 'Add config content' if config: self.config = config def add_scripts(self, scripts): + 'Add js content' if scripts: self.scripts = scripts class ContentCollector: + ''' + Collect content during view processing, and substitute in response by finding magic strings. + + This enables view functionality, such as template tags, to introduce content such as css and js + inclusion into the header and footer. + ''' def __init__(self): self.header_placeholder = "DJANGO_PLOTLY_DASH_HEADER_PLACEHOLDER" self.footer_placeholder = "DJANGO_PLOTLY_DASH_FOOTER_PLACEHOLDER" @@ -28,6 +40,7 @@ def __init__(self): self._encoding = "utf-8" def adjust_response(self, response): + 'Locate placeholder magic strings and replace with content' c1 = self._replace(response.content, self.header_placeholder, @@ -48,6 +61,7 @@ def _encode(self, string): return string.encode(self._encoding) class BaseMiddleware: + 'Django-plotly-dash middleware' def __init__(self, get_response): self.get_response = get_response diff --git a/django_plotly_dash/tests.py b/django_plotly_dash/tests.py index 70c13d34..c77a2589 100644 --- a/django_plotly_dash/tests.py +++ b/django_plotly_dash/tests.py @@ -29,6 +29,8 @@ import pytest +#pylint: disable=bare-except + def test_dash_app(): 'Test the import and formation of the dash app orm wrappers' @@ -176,11 +178,11 @@ def test_injection_updating(client): have_thrown = False try: - response2 = client.post(url, json.dumps({'output':{'id':'test-output-div2', 'property':'children'}, - 'inputs':[{'id':'my-dropdown2', - 'property':'value', - 'value':'TestIt'}, - ]}), content_type="application/json") + client.post(url, json.dumps({'output':{'id':'test-output-div2', 'property':'children'}, + 'inputs':[{'id':'my-dropdown2', + 'property':'value', + 'value':'TestIt'}, + ]}), content_type="application/json") except: have_thrown = True From 4eb6f656e5dfbcd2f11c33f6cfe0612453a390da Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 29 Aug 2018 20:53:01 -0700 Subject: [PATCH 12/15] Improve live updating demo commentary --- demo/demo/templates/demo_four.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/demo/demo/templates/demo_four.html b/demo/demo/templates/demo_four.html index c6e1ace4..801943bb 100644 --- a/demo/demo/templates/demo_four.html +++ b/demo/demo/templates/demo_four.html @@ -11,6 +11,12 @@

    Live Updating

    Live updating uses a websocket connection. The server pushes messages to the UI, and this is then translated into a callback through a dash component.

    +

    +Each press of a button causes a new random value to be added to that colour's time series in each +chart. Separate values are generated for each chart. The top chart has values +local to this page, and the bottom chart - including its values - is shared across all views of this +page. +

    {% load plotly_dash %}

    @@ -45,7 +51,7 @@

    Live Updating

    Any http command - can be used to send a message to the apps. This is equiavent to a press of + can be used to send a message to the apps. This is equivalent to a press of the red button. Other colours can be specified, including yellow, cyan and black in addition to the three named in the LiveInput app.

    From 24c29e0a2276f4d671bbd0f4baa47c3f09cbbdc5 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 29 Aug 2018 21:03:40 -0700 Subject: [PATCH 13/15] More prose for demo four --- demo/demo/templates/demo_four.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/demo/demo/templates/demo_four.html b/demo/demo/templates/demo_four.html index 801943bb..a118e5b2 100644 --- a/demo/demo/templates/demo_four.html +++ b/demo/demo/templates/demo_four.html @@ -17,6 +17,12 @@

    Live Updating

    local to this page, and the bottom chart - including its values - is shared across all views of this page.

    +

    +Reloading this page, or viewing in a second browser window, will show a new and initially empty +top chart and a copy of the bottom chart. Pressing any button in any window will cause all instances +of the bottom chart to update with the same values. Note that button presses are throttled so that +only one press per colour per second is processed. +

    {% load plotly_dash %}

    From ed83aa705f015d8fbee9f88a7b1e4ec9746022af Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 29 Aug 2018 21:11:09 -0700 Subject: [PATCH 14/15] Added configuration parameter for initial arguments caching --- django_plotly_dash/dash_wrapper.py | 1 + django_plotly_dash/templatetags/plotly_dash.py | 4 ++-- django_plotly_dash/util.py | 4 ++++ docs/configuration.rst | 3 +++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 630d54ca..13355ed1 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -38,6 +38,7 @@ from .app_name import app_name, main_view_label from .middleware import EmbeddedHolder + uid_counter = 0 usable_apps = {} diff --git a/django_plotly_dash/templatetags/plotly_dash.py b/django_plotly_dash/templatetags/plotly_dash.py index d9cb6beb..d74df8a6 100644 --- a/django_plotly_dash/templatetags/plotly_dash.py +++ b/django_plotly_dash/templatetags/plotly_dash.py @@ -30,7 +30,7 @@ from django.core.cache import cache from django_plotly_dash.models import DashApp -from django_plotly_dash.util import pipe_ws_endpoint_name +from django_plotly_dash.util import pipe_ws_endpoint_name, cache_timeout_initial_arguments register = template.Library() @@ -76,7 +76,7 @@ def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborde # Generate a cache id cache_id = "dpd-initial-args-%s" % str(uuid.uuid4()).replace('-', '') # Store args in json form in cache - cache.set(cache_id, initial_arguments, 60) + cache.set(cache_id, initial_arguments, cache_timeout_initial_arguments()) else: cache_id = None diff --git a/django_plotly_dash/util.py b/django_plotly_dash/util.py index a71c6682..bf16df7e 100644 --- a/django_plotly_dash/util.py +++ b/django_plotly_dash/util.py @@ -53,3 +53,7 @@ def insert_demo_migrations(): def http_poke_endpoint_enabled(): 'Return true if the http endpoint is enabled through the settings' return _get_settings().get('http_poke_enabled', True) + +def cache_timeout_initial_arguments(): + 'Return cache timeout, in seconds, for initial arguments' + return _get_settings().get('cache_timeout_initial_arguments', 60) diff --git a/docs/configuration.rst b/docs/configuration.rst index 96da2987..f684ece8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -21,6 +21,9 @@ below. # Insert data for the demo when migrating "insert_demo_migrations" : False, + + # Timeout for caching of initial arguments in seconds + "cache_timeout_initial_arguments": 60, } Defaults are inserted for missing values. It is also permissible to not have any ``PLOTLY_DASH`` entry in From f751a6c5a6b237342b9560b1c1a3caeb96d0d160 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 29 Aug 2018 22:09:24 -0700 Subject: [PATCH 15/15] Documentation of plotly_direct template tag --- docs/template_tags.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/template_tags.rst b/docs/template_tags.rst index 25ad0808..c15f584a 100644 --- a/docs/template_tags.rst +++ b/docs/template_tags.rst @@ -42,6 +42,32 @@ a JSON-encoded string representation. Each entry in the dictionary has the ``id` value is a dictionary mapping property name keys to initial values. +.. _plotly_direct: + +The ``plotly_direct`` template tag +---------------------------------- + +This template tag allows the direct insertion of html into a template, instead +of embedding it in an iframe. + +.. code-block:: jinja + + {%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. + .. _plotly_message_pipe: The ``plotly_message_pipe`` template tag