Skip to content

Commit

Permalink
Add handling of multiple return values from callbacks (#194)
Browse files Browse the repository at this point in the history
* Outline of demo-ten for multiple callback example

* Set up both types of example app prior to enabling multiple return values

* Add handling of multiple output values

* Extend test suite to cover multiple callbacks

* Handle special case of single member of outputs array
  • Loading branch information
delsim authored and GibbsConsulting committed Nov 16, 2019
1 parent c9a0242 commit e9cd4ef
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 13 deletions.
58 changes: 58 additions & 0 deletions demo/demo/plotly_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,61 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs):
localState.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')),
html.Img(src='assets/image_two.png'),
])

multiple_callbacks = DjangoDash("MultipleCallbackValues")

multiple_callbacks.layout = html.Div([
html.Button("Press Me",
id="button"),
dcc.RadioItems(id='dropdown-color',
options=[{'label': c, 'value': c.lower()} for c in ['Red', 'Green', 'Blue']],
value='red'
),
html.Div(id="output-one"),
html.Div(id="output-two"),
html.Div(id="output-three")
])

@multiple_callbacks.callback(
[dash.dependencies.Output('output-one', 'children'),
dash.dependencies.Output('output-two', 'children'),
dash.dependencies.Output('output-three', 'children')
],
[dash.dependencies.Input('button','n_clicks'),
dash.dependencies.Input('dropdown-color','value'),
])
def multiple_callbacks_one(button_clicks, color_choice):
return ("Output 1: %s %s" % (button_clicks, color_choice),
"Output 2: %s %s" % (color_choice, button_clicks),
"Output 3: %s %s" % (button_clicks, color_choice),
)


multiple_callbacks = DjangoDash("MultipleCallbackValuesExpanded")

multiple_callbacks.layout = html.Div([
html.Button("Press Me",
id="button"),
dcc.RadioItems(id='dropdown-color',
options=[{'label': c, 'value': c.lower()} for c in ['Red', 'Green', 'Blue']],
value='red'
),
html.Div(id="output-one"),
html.Div(id="output-two"),
html.Div(id="output-three")
])

@multiple_callbacks.expanded_callback(
[dash.dependencies.Output('output-one', 'children'),
dash.dependencies.Output('output-two', 'children'),
dash.dependencies.Output('output-three', 'children')
],
[dash.dependencies.Input('button','n_clicks'),
dash.dependencies.Input('dropdown-color','value'),
])
def multiple_callbacks_two(button_clicks, color_choice, **kwargs):
return ["Output 1: %s %s" % (button_clicks, color_choice),
"Output 2: %s %s" % (button_clicks, color_choice),
"Output 3: %s %s [%s]" % (button_clicks, color_choice, kwargs)
]

2 changes: 2 additions & 0 deletions demo/demo/scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django_plotly_dash import DjangoDash
from django.utils.module_loading import import_string

from demo.plotly_apps import multiple_callbacks

def stateless_app_loader(app_name):

# Load a stateless app
Expand Down
33 changes: 33 additions & 0 deletions demo/demo/templates/demo_ten.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{%extends "base.html"%}
{%load plotly_dash%}

{%block title%}Demo Ten - Multiple Callback Values{%endblock%}

{%block content%}
<h1>Multiple Callback Values</h1>
<p>
Both standard and extended callbacks can return multiple responses. This example instantiates
two example applications that use multiple return values for both types of callback.
</p>
<div class="card bg-light border-dark">
<div class="card-body">
<p><span>{</span>% load plotly_dash %}</p>
<p><span>{</span>% plotly_app slug="multiple-callback-values" ratio=0.2 %}</p>
<p><span>{</span>% plotly_app slug="multiple-callback-values-expanded" ratio=0.2 %}</p>
</div>
</div>
<p>
</p>
<div class="card border-dark">
<div class="card-body">
{%plotly_app slug="multiple-callback-values-1" ratio=0.2 %}
</div>
</div>
<p></p>
<div class="card border-dark">
<div class="card-body">
{%plotly_app slug="multiple-callback-values-expanded" ratio=0.2 %}
</div>
</div>
<p></p>
{%endblock%}
1 change: 1 addition & 0 deletions demo/demo/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ <h1>Demonstration Application</h1>
<li><a class="btn btn-primary btnspace" href="{%url "demo-seven"%}">Demo Seven</a> - dash-bootstrap-components example</li>
<li><a class="btn btn-primary btnspace" href="{%url "demo-eight"%}">Demo Eight</a> - Django session state example</li>
<li><a class="btn btn-primary btnspace" href="{%url "demo-nine"%}">Demo Nine</a> - local serving of assets</li>
<li><a class="btn btn-primary btnspace" href="{%url "demo-ten"%}">Demo Ten</a> - callback multiple return values</li>
</ul>
{%endblock%}
1 change: 1 addition & 0 deletions demo/demo/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
url('^demo-seven', TemplateView.as_view(template_name='demo_seven.html'), name="demo-seven"),
url('^demo-eight', session_state_view, {'template_name':'demo_eight.html'}, name="demo-eight"),
url('^demo-nine', TemplateView.as_view(template_name='demo_nine.html'), name="demo-nine"),
url('^demo-ten', TemplateView.as_view(template_name='demo_ten.html'), name="demo-ten"),
url('^admin/', admin.site.urls),
url('^django_plotly_dash/', include('django_plotly_dash.urls')),

Expand Down
46 changes: 35 additions & 11 deletions django_plotly_dash/dash_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,6 @@ def callback(self, output, inputs=[], state=[], events=[]): # pylint: disable=da

if isinstance(output, (list, tuple)):
fixed_outputs = [self._fix_callback_item(x) for x in output]
# Temporary check; can be removed once the library has been extended
raise NotImplementedError("django-plotly-dash cannot handle multiple callback outputs at present")
else:
fixed_outputs = self._fix_callback_item(output)

Expand Down Expand Up @@ -505,13 +503,29 @@ def dispatch_with_args(self, body, argMap):
state = body.get('state', [])
output = body['output']

outputs = []
try:
output_id = output['id']
output_property = output['property']
target_id = "%s.%s" %(output_id, output_property)
if output[:2] == '..' and output[-2:] == '..':
# Multiple outputs
outputs = output[2:-2].split('...')
target_id = output
# Special case of a single output
if len(outputs) == 1:
target_id = output[2:-2]
except:
target_id = output
output_id, output_property = output.split(".")
pass

single_case = False
if len(outputs) < 1:
try:
output_id = output['id']
output_property = output['property']
target_id = "%s.%s" %(output_id, output_property)
except:
target_id = output
output_id, output_property = output.split(".")
single_case = True
outputs = [output,]

args = []

Expand Down Expand Up @@ -541,10 +555,20 @@ def dispatch_with_args(self, body, argMap):
return 'EDGECASEEXIT'

res = self.callback_map[target_id]['callback'](*args, **argMap)
if da and da.have_current_state_entry(output_id, output_property):
response = json.loads(res.data.decode('utf-8'))
value = response.get('response', {}).get('props', {}).get(output_property, None)
da.update_current_state(output_id, output_property, value)
if da:
if single_case and da.have_current_state_entry(output_id, output_property):
response = json.loads(res.data.decode('utf-8'))
value = response.get('response', {}).get('props', {}).get(output_property, None)
da.update_current_state(output_id, output_property, value)

response = json.loads(res)
root_value = response.get('response', {})
for output_item in outputs:
if isinstance(output_item, str):
output_id, output_property = output_item.split('.')
if da.have_current_state_entry(output_id, output_property):
value = root_value.get(output_id,{}).get(output_property, None)
da.update_current_state(output_id, output_property, value)

return res

Expand Down
22 changes: 22 additions & 0 deletions django_plotly_dash/migrations/0002_add_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ def addExamples(apps, schema_editor):

da3.save()

sa4 = StatelessApp(app_name="MultipleCallbackValues",
slug="multiple-callback-values")

sa4.save()

da4 = DashApp(stateless_app=sa4,
instance_name="Multiple Callback Values Example 1",
slug="multiple-callback-values-1")

da4.save()

sa5 = StatelessApp(app_name="MultipleCallbackValuesExpanded",
slug="multiple-callback-values-exapnded")

sa5.save()

da5 = DashApp(stateless_app=sa5,
instance_name="Multiple Callback Values Example 2",
slug="multiple-callback-values-expanded")

da5.save()


def remExamples(apps, schema_editor):

Expand Down
59 changes: 57 additions & 2 deletions django_plotly_dash/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'''

import pytest
import json

#pylint: disable=bare-except

Expand Down Expand Up @@ -109,7 +110,6 @@ def test_direct_access(client):
def test_updating(client):
'Check updating of an app using demo test data'

import json
from django.urls import reverse

route_name = 'update-component'
Expand Down Expand Up @@ -160,11 +160,42 @@ def test_injection_app_access(client):

assert did_fail

@pytest.mark.django_db
def test_injection_updating_multiple_callbacks(client):
'Check updating of an app using demo test data for multiple callbacks'

from django.urls import reverse

route_name = 'update-component'

for prefix, arg_map in [('app-', {'ident':'multiple_callbacks'}),]:
url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map)

# output is now a string of id and propery
response = client.post(url, json.dumps({'output':'..output-one.children...output-two.children...output-three.children..',
'inputs':[
{'id':'button',
'property':'n_clicks',
'value':'10'},
{'id':'dropdown-color',
'property':'value',
'value':'purple-ish yellow with a hint of greeny orange'},
]}), content_type="application/json")

assert response.status_code == 200

resp = json.loads(response.content.decode('utf-8'))
assert 'response' in resp

resp_detail = resp['response']
assert 'output-two' in resp_detail
assert 'children' in resp_detail['output-two']
assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange"

@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'
Expand All @@ -183,6 +214,30 @@ def test_injection_updating(client):
assert response.content[:len(rStart)] == rStart
assert response.status_code == 200

# New variant of output has a string used to name the properties
response = client.post(url, json.dumps({'output':'test-output-div.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

# Second variant has a single-entry mulitple property output
response = client.post(url, json.dumps({'output':'..test-output-div.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:
Expand Down

0 comments on commit e9cd4ef

Please sign in to comment.