Skip to content

Commit 8951bcf

Browse files
author
Mark Gibbs
committed
Added a save-on-change flag to allow the automatic updating of objects in the database. Centralised the calls so caching can be used as well (or instead) if desired
1 parent 345b73b commit 8951bcf

File tree

6 files changed

+101
-17
lines changed

6 files changed

+101
-17
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ cd django-plotly-dash
4646
## Usage
4747

4848
To use existing dash applications, first register them using the `DelayedDash` class. This
49-
replaces the `dash.Dash` class of `plotly.py.`
49+
replaces the `Dash` class of the `dash` package.
5050

5151
Taking a very simple example inspired by the excellent [getting started](https://dash.plot.ly/) documentation:
5252

demo/demo/plotly_apps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ def callback_size(dropdown_color, dropdown_size):
4949
[dash.dependencies.Input('dropdown-one','value')]
5050
)
5151
def callback_c(*args,**kwargs):
52+
da = kwargs['dash_app']
5253
return "Args are %s and kwargs are %s" %("".join(*args),str(kwargs))
5354

django_plotly_dash/dash_wrapper.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ def as_dash_instance(self):
6262
'''
6363
return self.form_dash_instance()
6464

65+
def handle_current_state(self):
66+
'Do nothing impl - only matters if state present'
67+
pass
68+
def update_current_state(self, wid, key, value):
69+
'Do nothing impl - only matters if state present'
70+
pass
71+
def have_current_state_entry(self, wid, key):
72+
'Do nothing impl - only matters if state present'
73+
pass
74+
6575
def form_dash_instance(self, replacements=None, specific_identifier=None):
6676
if not specific_identifier:
6777
app_pathname = "%s:app-%s"% (app_name, main_view_label)
@@ -266,19 +276,27 @@ def dispatch_with_args(self, body, argMap):
266276

267277
target_id = '{}.{}'.format(output['id'], output['property'])
268278
args = []
269-
for component_registration in self.callback_map[target_id]['inputs']:
270-
args.append([
271-
c.get('value', None) for c in inputs if
272-
c['property'] == component_registration['property'] and
273-
c['id'] == component_registration['id']
274-
][0])
275279

276-
for component_registration in self.callback_map[target_id]['state']:
277-
args.append([
278-
c.get('value', None) for c in state if
279-
c['property'] == component_registration['property'] and
280-
c['id'] == component_registration['id']
281-
][0])
280+
da = argMap.get('dash_app', None)
282281

283-
return self.callback_map[target_id]['callback'](*args,**argMap)
282+
for component_registration in self.callback_map[target_id]['inputs']:
283+
for c in inputs:
284+
if c['property'] == component_registration['property'] and c['id'] == component_registration['id']:
285+
v = c.get('value',None)
286+
args.append(v)
287+
if da: da.update_current_state(c['id'],c['property'],v)
284288

289+
for component_registration in self.callback_map[target_id]['state']:
290+
for c in state:
291+
if c['property'] == component_registration['property'] and c['id'] == component_registration['id']:
292+
v = c.get('value',None)
293+
args.append(v)
294+
if da: da.update_current_state(c['id'],c['property'],v)
295+
296+
res = self.callback_map[target_id]['callback'](*args,**argMap)
297+
if da and da.have_current_state_entry(output['id'], output['property']):
298+
response = json.loads(res.data.decode('utf-8'))
299+
value = response.get('response',{}).get('props',{}).get(output['property'],None)
300+
da.update_current_state(output['id'], output['property'], value)
301+
302+
return res
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 2.0.5 on 2018-05-15 01:02
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('django_plotly_dash', '0002_simple_example_state'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='dashapp',
15+
name='save_on_change',
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AlterField(
19+
model_name='dashapp',
20+
name='base_state',
21+
field=models.TextField(default='{}'),
22+
),
23+
]

django_plotly_dash/models.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class DashApp(models.Model):
1717
base_state = models.TextField(null=False, default="{}") # If mandating postgresql then this could be a JSONField
1818
creation = models.DateTimeField(auto_now_add=True)
1919
update = models.DateTimeField(auto_now=True)
20+
save_on_change = models.BooleanField(null=False,default=False)
2021

2122
def __str__(self):
2223
return self.instance_name
@@ -36,9 +37,48 @@ def _stateless_dash_app(self):
3637
setattr(self,'_stateless_dash_app_instance',dd)
3738
return dd
3839

40+
def handle_current_state(self):
41+
'''
42+
Check to see if the current hydrated state and the saved state are different.
43+
44+
If they are, then persist the current state in the database by saving the model instance.
45+
'''
46+
if getattr(self,'_current_state_hydrated_changed',False) and self.save_on_change:
47+
new_base_state = json.dumps(getattr(self,'_current_state_hydrated',{}))
48+
if new_base_state != self.base_state:
49+
self.base_state = new_base_state
50+
self.save()
51+
52+
def have_current_state_entry(self, wid, key):
53+
cscoll = self.current_state()
54+
cs = cscoll.get(wid,{})
55+
return key in cs
56+
57+
def update_current_state(self, wid, key, value):
58+
'''
59+
Update current state with a (possibly new) value associated with key
60+
61+
If the key does not represent an existing entry, then ignore it
62+
'''
63+
cscoll = self.current_state()
64+
cs = cscoll.get(wid,{})
65+
if key in cs:
66+
current_value = cs.get(key,None)
67+
if current_value != value:
68+
cs[key] = value
69+
setattr(self,'_current_state_hydrated_changed',True)
70+
71+
def current_state(self):
72+
cs = getattr(self,'_current_state_hydrated',None)
73+
if not cs:
74+
cs = json.loads(self.base_state)
75+
setattr(self,'_current_state_hydrated',cs)
76+
setattr(self,'_current_state_hydrated_changed',False)
77+
return cs
78+
3979
def as_dash_instance(self):
4080
dd = self._stateless_dash_app()
41-
base = json.loads(self.base_state)
81+
base = self.current_state()
4282
return dd.form_dash_instance(replacements=base,
4383
specific_identifier=self.slug)
4484

@@ -76,8 +116,8 @@ def locate_item(id, stateless=False):
76116
return da, app
77117

78118
class DashAppAdmin(admin.ModelAdmin):
79-
list_display = ['instance_name','app_name','slug','creation','update',]
80-
list_filter = ['app_name','creation','update',]
119+
list_display = ['instance_name','app_name','slug','creation','update','save_on_change',]
120+
list_filter = ['creation','update','save_on_change','app_name',]
81121

82122
def _populate_values(self, request, queryset):
83123
for da in queryset:

django_plotly_dash/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ def update(request, id, stateless=False, **kwargs):
4242
# Use direct dispatch with extra arguments in the argMap
4343
app_state = request.session.get("django_plotly_dash",dict())
4444
argMap = {'dash_app_id': id,
45+
'dash_app': da,
4546
'user': request.user,
4647
'session_state': app_state}
4748
resp = app.dispatch_with_args(rb, argMap)
4849
request.session['django_plotly_dash'] = app_state
50+
da.handle_current_state()
4951

5052
return HttpResponse(resp.data,
5153
content_type=resp.mimetype)

0 commit comments

Comments
 (0)