Skip to content

Commit e9cd4ef

Browse files
delsimGibbsConsulting
authored andcommitted
Add handling of multiple return values from callbacks (#194)
* 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
1 parent c9a0242 commit e9cd4ef

File tree

8 files changed

+209
-13
lines changed

8 files changed

+209
-13
lines changed

demo/demo/plotly_apps.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,61 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs):
316316
localState.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')),
317317
html.Img(src='assets/image_two.png'),
318318
])
319+
320+
multiple_callbacks = DjangoDash("MultipleCallbackValues")
321+
322+
multiple_callbacks.layout = html.Div([
323+
html.Button("Press Me",
324+
id="button"),
325+
dcc.RadioItems(id='dropdown-color',
326+
options=[{'label': c, 'value': c.lower()} for c in ['Red', 'Green', 'Blue']],
327+
value='red'
328+
),
329+
html.Div(id="output-one"),
330+
html.Div(id="output-two"),
331+
html.Div(id="output-three")
332+
])
333+
334+
@multiple_callbacks.callback(
335+
[dash.dependencies.Output('output-one', 'children'),
336+
dash.dependencies.Output('output-two', 'children'),
337+
dash.dependencies.Output('output-three', 'children')
338+
],
339+
[dash.dependencies.Input('button','n_clicks'),
340+
dash.dependencies.Input('dropdown-color','value'),
341+
])
342+
def multiple_callbacks_one(button_clicks, color_choice):
343+
return ("Output 1: %s %s" % (button_clicks, color_choice),
344+
"Output 2: %s %s" % (color_choice, button_clicks),
345+
"Output 3: %s %s" % (button_clicks, color_choice),
346+
)
347+
348+
349+
multiple_callbacks = DjangoDash("MultipleCallbackValuesExpanded")
350+
351+
multiple_callbacks.layout = html.Div([
352+
html.Button("Press Me",
353+
id="button"),
354+
dcc.RadioItems(id='dropdown-color',
355+
options=[{'label': c, 'value': c.lower()} for c in ['Red', 'Green', 'Blue']],
356+
value='red'
357+
),
358+
html.Div(id="output-one"),
359+
html.Div(id="output-two"),
360+
html.Div(id="output-three")
361+
])
362+
363+
@multiple_callbacks.expanded_callback(
364+
[dash.dependencies.Output('output-one', 'children'),
365+
dash.dependencies.Output('output-two', 'children'),
366+
dash.dependencies.Output('output-three', 'children')
367+
],
368+
[dash.dependencies.Input('button','n_clicks'),
369+
dash.dependencies.Input('dropdown-color','value'),
370+
])
371+
def multiple_callbacks_two(button_clicks, color_choice, **kwargs):
372+
return ["Output 1: %s %s" % (button_clicks, color_choice),
373+
"Output 2: %s %s" % (button_clicks, color_choice),
374+
"Output 3: %s %s [%s]" % (button_clicks, color_choice, kwargs)
375+
]
376+

demo/demo/scaffold.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django_plotly_dash import DjangoDash
44
from django.utils.module_loading import import_string
55

6+
from demo.plotly_apps import multiple_callbacks
7+
68
def stateless_app_loader(app_name):
79

810
# Load a stateless app

demo/demo/templates/demo_ten.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{%extends "base.html"%}
2+
{%load plotly_dash%}
3+
4+
{%block title%}Demo Ten - Multiple Callback Values{%endblock%}
5+
6+
{%block content%}
7+
<h1>Multiple Callback Values</h1>
8+
<p>
9+
Both standard and extended callbacks can return multiple responses. This example instantiates
10+
two example applications that use multiple return values for both types of callback.
11+
</p>
12+
<div class="card bg-light border-dark">
13+
<div class="card-body">
14+
<p><span>{</span>% load plotly_dash %}</p>
15+
<p><span>{</span>% plotly_app slug="multiple-callback-values" ratio=0.2 %}</p>
16+
<p><span>{</span>% plotly_app slug="multiple-callback-values-expanded" ratio=0.2 %}</p>
17+
</div>
18+
</div>
19+
<p>
20+
</p>
21+
<div class="card border-dark">
22+
<div class="card-body">
23+
{%plotly_app slug="multiple-callback-values-1" ratio=0.2 %}
24+
</div>
25+
</div>
26+
<p></p>
27+
<div class="card border-dark">
28+
<div class="card-body">
29+
{%plotly_app slug="multiple-callback-values-expanded" ratio=0.2 %}
30+
</div>
31+
</div>
32+
<p></p>
33+
{%endblock%}

demo/demo/templates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ <h1>Demonstration Application</h1>
1616
<li><a class="btn btn-primary btnspace" href="{%url "demo-seven"%}">Demo Seven</a> - dash-bootstrap-components example</li>
1717
<li><a class="btn btn-primary btnspace" href="{%url "demo-eight"%}">Demo Eight</a> - Django session state example</li>
1818
<li><a class="btn btn-primary btnspace" href="{%url "demo-nine"%}">Demo Nine</a> - local serving of assets</li>
19+
<li><a class="btn btn-primary btnspace" href="{%url "demo-ten"%}">Demo Ten</a> - callback multiple return values</li>
1920
</ul>
2021
{%endblock%}

demo/demo/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
url('^demo-seven', TemplateView.as_view(template_name='demo_seven.html'), name="demo-seven"),
4646
url('^demo-eight', session_state_view, {'template_name':'demo_eight.html'}, name="demo-eight"),
4747
url('^demo-nine', TemplateView.as_view(template_name='demo_nine.html'), name="demo-nine"),
48+
url('^demo-ten', TemplateView.as_view(template_name='demo_ten.html'), name="demo-ten"),
4849
url('^admin/', admin.site.urls),
4950
url('^django_plotly_dash/', include('django_plotly_dash.urls')),
5051

django_plotly_dash/dash_wrapper.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,6 @@ def callback(self, output, inputs=[], state=[], events=[]): # pylint: disable=da
468468

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

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

506+
outputs = []
508507
try:
509-
output_id = output['id']
510-
output_property = output['property']
511-
target_id = "%s.%s" %(output_id, output_property)
508+
if output[:2] == '..' and output[-2:] == '..':
509+
# Multiple outputs
510+
outputs = output[2:-2].split('...')
511+
target_id = output
512+
# Special case of a single output
513+
if len(outputs) == 1:
514+
target_id = output[2:-2]
512515
except:
513-
target_id = output
514-
output_id, output_property = output.split(".")
516+
pass
517+
518+
single_case = False
519+
if len(outputs) < 1:
520+
try:
521+
output_id = output['id']
522+
output_property = output['property']
523+
target_id = "%s.%s" %(output_id, output_property)
524+
except:
525+
target_id = output
526+
output_id, output_property = output.split(".")
527+
single_case = True
528+
outputs = [output,]
515529

516530
args = []
517531

@@ -541,10 +555,20 @@ def dispatch_with_args(self, body, argMap):
541555
return 'EDGECASEEXIT'
542556

543557
res = self.callback_map[target_id]['callback'](*args, **argMap)
544-
if da and da.have_current_state_entry(output_id, output_property):
545-
response = json.loads(res.data.decode('utf-8'))
546-
value = response.get('response', {}).get('props', {}).get(output_property, None)
547-
da.update_current_state(output_id, output_property, value)
558+
if da:
559+
if single_case and da.have_current_state_entry(output_id, output_property):
560+
response = json.loads(res.data.decode('utf-8'))
561+
value = response.get('response', {}).get('props', {}).get(output_property, None)
562+
da.update_current_state(output_id, output_property, value)
563+
564+
response = json.loads(res)
565+
root_value = response.get('response', {})
566+
for output_item in outputs:
567+
if isinstance(output_item, str):
568+
output_id, output_property = output_item.split('.')
569+
if da.have_current_state_entry(output_id, output_property):
570+
value = root_value.get(output_id,{}).get(output_property, None)
571+
da.update_current_state(output_id, output_property, value)
548572

549573
return res
550574

django_plotly_dash/migrations/0002_add_examples.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ def addExamples(apps, schema_editor):
6161

6262
da3.save()
6363

64+
sa4 = StatelessApp(app_name="MultipleCallbackValues",
65+
slug="multiple-callback-values")
66+
67+
sa4.save()
68+
69+
da4 = DashApp(stateless_app=sa4,
70+
instance_name="Multiple Callback Values Example 1",
71+
slug="multiple-callback-values-1")
72+
73+
da4.save()
74+
75+
sa5 = StatelessApp(app_name="MultipleCallbackValuesExpanded",
76+
slug="multiple-callback-values-exapnded")
77+
78+
sa5.save()
79+
80+
da5 = DashApp(stateless_app=sa5,
81+
instance_name="Multiple Callback Values Example 2",
82+
slug="multiple-callback-values-expanded")
83+
84+
da5.save()
85+
6486

6587
def remExamples(apps, schema_editor):
6688

django_plotly_dash/tests.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
'''
2929

3030
import pytest
31+
import json
3132

3233
#pylint: disable=bare-except
3334

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

112-
import json
113113
from django.urls import reverse
114114

115115
route_name = 'update-component'
@@ -160,11 +160,42 @@ def test_injection_app_access(client):
160160

161161
assert did_fail
162162

163+
@pytest.mark.django_db
164+
def test_injection_updating_multiple_callbacks(client):
165+
'Check updating of an app using demo test data for multiple callbacks'
166+
167+
from django.urls import reverse
168+
169+
route_name = 'update-component'
170+
171+
for prefix, arg_map in [('app-', {'ident':'multiple_callbacks'}),]:
172+
url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map)
173+
174+
# output is now a string of id and propery
175+
response = client.post(url, json.dumps({'output':'..output-one.children...output-two.children...output-three.children..',
176+
'inputs':[
177+
{'id':'button',
178+
'property':'n_clicks',
179+
'value':'10'},
180+
{'id':'dropdown-color',
181+
'property':'value',
182+
'value':'purple-ish yellow with a hint of greeny orange'},
183+
]}), content_type="application/json")
184+
185+
assert response.status_code == 200
186+
187+
resp = json.loads(response.content.decode('utf-8'))
188+
assert 'response' in resp
189+
190+
resp_detail = resp['response']
191+
assert 'output-two' in resp_detail
192+
assert 'children' in resp_detail['output-two']
193+
assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange"
194+
163195
@pytest.mark.django_db
164196
def test_injection_updating(client):
165197
'Check updating of an app using demo test data'
166198

167-
import json
168199
from django.urls import reverse
169200

170201
route_name = 'update-component'
@@ -183,6 +214,30 @@ def test_injection_updating(client):
183214
assert response.content[:len(rStart)] == rStart
184215
assert response.status_code == 200
185216

217+
# New variant of output has a string used to name the properties
218+
response = client.post(url, json.dumps({'output':'test-output-div.children',
219+
'inputs':[{'id':'my-dropdown1',
220+
'property':'value',
221+
'value':'TestIt'},
222+
]}), content_type="application/json")
223+
224+
rStart = b'{"response": {"props": {"children":'
225+
226+
assert response.content[:len(rStart)] == rStart
227+
assert response.status_code == 200
228+
229+
# Second variant has a single-entry mulitple property output
230+
response = client.post(url, json.dumps({'output':'..test-output-div.children..',
231+
'inputs':[{'id':'my-dropdown1',
232+
'property':'value',
233+
'value':'TestIt'},
234+
]}), content_type="application/json")
235+
236+
rStart = b'{"response": {"props": {"children":'
237+
238+
assert response.content[:len(rStart)] == rStart
239+
assert response.status_code == 200
240+
186241
have_thrown = False
187242

188243
try:

0 commit comments

Comments
 (0)