diff --git a/README.md b/README.md index edfc9794..245ac841 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # django-plotly-dash -Expose [plotly dash](https://plot.ly/products/dash/) apps as django tags. +Expose [plotly dash](https://plot.ly/products/dash/) apps as [Django](https:://www.djangoproject.com/) tags. Multiple Dash apps can +then be embedded into a single web page, persist and share internal state, and also have access to the +current user and session variables. See the source for this project here: @@ -8,6 +10,8 @@ See the source for this project here: This README file provides a short guide to installing and using the package, and also outlines how to run the demonstration application. + + More detailed information can be found in the online documentation at @@ -45,8 +49,8 @@ cd django-plotly-dash ## Usage -To use existing dash applications, first register them using the `DelayedDash` class. This -replaces the `dash.Dash` class of `plotly.py.` +To use existing dash applications, first register them using the `DjangoDash` class. This +replaces the `Dash` class of the `dash` package. Taking a very simple example inspired by the excellent [getting started](https://dash.plot.ly/) documentation: @@ -55,9 +59,9 @@ import dash import dash_core_components as dcc import dash_html_components as html -from django_plotly_dash import DelayedDash +from django_plotly_dash import DjangoDash -app = DelayedDash('SimpleExample') +app = DjangoDash('SimpleExample') app.layout = html.Div([ dcc.RadioItems( @@ -88,30 +92,15 @@ def callback_color(dropdown_value): def callback_size(dropdown_color, dropdown_size): return "The chosen T-shirt is a %s %s one." %(dropdown_size, dropdown_color) - -a2 = DelayedDash("Ex2") -a2.layout = html.Div([ - dcc.RadioItems(id="dropdown-one",options=[{'label':i,'value':j} for i,j in [ - ("O2","Oxygen"),("N2","Nitrogen"),] - ],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): - return "Args are %s and kwargs are %s" %("".join(*args),str(kwargs)) ``` -Note that the `DelayedDash` constructor requires a name to be specified. This name is then used to identify the dash app in +Note that the `DjangoDash` constructor requires a name to be specified. This name is then used to identify the dash app in templates: ```jinja2 {% load plotly_dash %} -{% plotly_item "SimpleExample" %} +{% plotly_item name="SimpleExample" %} ``` The registration code needs to be in a location diff --git a/demo/demo/plotly_apps.py b/demo/demo/plotly_apps.py index dee9c85e..a6bb63d8 100644 --- a/demo/demo/plotly_apps.py +++ b/demo/demo/plotly_apps.py @@ -2,9 +2,9 @@ import dash_core_components as dcc import dash_html_components as html -from django_plotly_dash import DelayedDash +from django_plotly_dash import DjangoDash -app = DelayedDash('SimpleExample') +app = DjangoDash('SimpleExample') app.layout = html.Div([ dcc.RadioItems( @@ -36,10 +36,10 @@ def callback_size(dropdown_color, dropdown_size): return "The chosen T-shirt is a %s %s one." %(dropdown_size, dropdown_color) -a2 = DelayedDash("Ex2") +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"),] + ("O2","Oxygen"),("N2","Nitrogen"),("CO2","Carbon Dioxide")] ],value="Oxygen"), html.Div(id="output-one") ]) @@ -49,5 +49,6 @@ def callback_size(dropdown_color, dropdown_size): [dash.dependencies.Input('dropdown-one','value')] ) def callback_c(*args,**kwargs): - return "Args are %s and kwargs are %s" %("".join(*args),str(kwargs)) + da = kwargs['dash_app'] + return "Args are [%s] and kwargs are %s" %(",".join(args),str(kwargs)) diff --git a/demo/demo/settings.py b/demo/demo/settings.py index aeae9699..d26e9373 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -107,7 +107,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'America/Vancouver' USE_I18N = True diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html index 2ad76c99..74f318dc 100644 --- a/demo/demo/templates/index.html +++ b/demo/demo/templates/index.html @@ -5,15 +5,15 @@
Content here - {%plotly_item "simpleexample-1" ratio=0.2 %} + {%plotly_app slug="simpleexample-1" ratio=0.2 %}
Content here - {%plotly_item "SimpleExample"%} + {%plotly_app name="SimpleExample"%}
Content here - {%plotly_item "Ex2"%} + {%plotly_app name="Ex2"%}
diff --git a/django_plotly_dash/__init__.py b/django_plotly_dash/__init__.py index 21100bbf..86cba23d 100644 --- a/django_plotly_dash/__init__.py +++ b/django_plotly_dash/__init__.py @@ -1,6 +1,6 @@ # -__version__ = "0.0.4" +__version__ = "0.1.0" -from .dash_wrapper import DelayedDash +from .dash_wrapper import DjangoDash diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 0f140238..e3572df9 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -3,6 +3,7 @@ from django.urls import reverse from django.http import HttpResponse +from django.utils.text import slugify import json @@ -13,43 +14,20 @@ uid_counter = 0 usable_apps = {} -nd_apps = {} def add_usable_app(name, app): + name = slugify(name) global usable_apps usable_apps[name] = app + return name -def add_instance(id, instance): - global nd_apps - nd_apps[id] = instance - -def get_app_by_name(name): - ''' - Locate a registered dash app by name, and return a DelayedDash instance encapsulating the app. - ''' - return usable_apps.get(name,None) - -def get_app_instance_by_id(id): +def get_stateless_by_name(name): ''' - Locate an instance of a dash app by identifier, or return None if one does not exist + Locate a registered dash app by name, and return a DjangoDash instance encapsulating the app. ''' - return nd_apps.get(id,None) - -def clear_app_instance(id): - try: - del nd_apps[id] - except: - pass - -def get_or_form_app(id, name, **kwargs): - ''' - Locate an instance of a dash app by identifier, loading or creating a new instance if needed - ''' - app = get_app_instance_by_id(id) - if app: - return app - dd = get_app_by_name(name) - return dd.form_dash_instance() + name = slugify(name) + # TODO wrap this in raising a 404 if not found + return usable_apps[name] class Holder: def __init__(self): @@ -59,7 +37,7 @@ def append_css(self, stylesheet): def append_script(self, script): self.items.append(script) -class DelayedDash: +class DjangoDash: def __init__(self, name=None, **kwargs): if name is None: global uid_counter @@ -78,9 +56,30 @@ def __init__(self, name=None, **kwargs): self._expanded_callbacks = False + def as_dash_instance(self): + ''' + Form a dash instance, for stateless use of this app + ''' + return self.form_dash_instance() + + def handle_current_state(self): + 'Do nothing impl - only matters if state present' + pass + def update_current_state(self, wid, key, value): + 'Do nothing impl - only matters if state present' + pass + def have_current_state_entry(self, wid, key): + 'Do nothing impl - only matters if state present' + pass + def form_dash_instance(self, replacements=None, specific_identifier=None): + if not specific_identifier: + app_pathname = "%s:app-%s"% (app_name, main_view_label) + else: + app_pathname="%s:%s" % (app_name, main_view_label) + rd = NotDash(name_root=self._uid, - app_pathname="%s:%s" % (app_name, main_view_label), + app_pathname=app_pathname, expanded_callbacks = self._expanded_callbacks, replacements = replacements, specific_identifier = specific_identifier) @@ -134,8 +133,6 @@ def __init__(self, name_root, app_pathname=None, replacements = None, specific_i else: self._uid = name_root - add_instance(self._uid, self) - self._flask_app = Flask(self._uid) self._notflask = NotFlask() self._base_pathname = reverse(app_pathname,kwargs={'id':self._uid}) @@ -279,20 +276,27 @@ def dispatch_with_args(self, body, argMap): target_id = '{}.{}'.format(output['id'], output['property']) args = [] - for component_registration in self.callback_map[target_id]['inputs']: - args.append([ - c.get('value', None) for c in inputs if - c['property'] == component_registration['property'] and - c['id'] == component_registration['id'] - ][0]) - - for component_registration in self.callback_map[target_id]['state']: - args.append([ - c.get('value', None) for c in state if - c['property'] == component_registration['property'] and - c['id'] == component_registration['id'] - ][0]) - return self.callback_map[target_id]['callback'](*args,**argMap) + da = argMap.get('dash_app', None) + for component_registration in self.callback_map[target_id]['inputs']: + for c in inputs: + if c['property'] == component_registration['property'] and c['id'] == component_registration['id']: + v = c.get('value',None) + args.append(v) + if da: da.update_current_state(c['id'],c['property'],v) + for component_registration in self.callback_map[target_id]['state']: + for c in state: + if c['property'] == component_registration['property'] and c['id'] == component_registration['id']: + v = c.get('value',None) + args.append(v) + if da: da.update_current_state(c['id'],c['property'],v) + + 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) + + return res diff --git a/django_plotly_dash/migrations/0003_auto_20180514_1802.py b/django_plotly_dash/migrations/0003_auto_20180514_1802.py new file mode 100644 index 00000000..b383c0be --- /dev/null +++ b/django_plotly_dash/migrations/0003_auto_20180514_1802.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.5 on 2018-05-15 01:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_plotly_dash', '0002_simple_example_state'), + ] + + operations = [ + migrations.AddField( + model_name='dashapp', + name='save_on_change', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='dashapp', + name='base_state', + field=models.TextField(default='{}'), + ), + ] diff --git a/django_plotly_dash/models.py b/django_plotly_dash/models.py index aee3221d..dd6403ae 100644 --- a/django_plotly_dash/models.py +++ b/django_plotly_dash/models.py @@ -1,8 +1,9 @@ from django.db import models from django.contrib import admin from django.utils.text import slugify +from django.shortcuts import get_object_or_404 -from .dash_wrapper import get_app_instance_by_id, get_app_by_name, clear_app_instance +from .dash_wrapper import get_stateless_by_name import json @@ -13,9 +14,10 @@ class DashApp(models.Model): app_name = models.CharField(max_length=100, blank=False, null=False, unique=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) # If mandating postgresql then this could be a JSONField + base_state = models.TextField(null=False, default="{}") # If mandating postgresql then this could be a JSONField creation = models.DateTimeField(auto_now_add=True) update = models.DateTimeField(auto_now=True) + save_on_change = models.BooleanField(null=False,default=False) def __str__(self): return self.instance_name @@ -27,46 +29,71 @@ def save(self, *args, **kwargs): if not self.slug or len(self.slug) < 2: self.slug = slugify(self.instance_name) super(DashApp, self).save(*args,**kwargs) - # TODO at this point should invaliate any local cache of the older values - clear_app_instance(self.slug) - def as_dash_instance(self): - ai = get_app_instance_by_id(self.slug) - if ai: - return ai - dd = get_app_by_name(self.app_name) - base = json.loads(self.base_state) - return dd.form_dash_instance(replacements=base, - specific_identifier=self.slug) + def _stateless_dash_app(self): + dd = getattr(self,'_stateless_dash_app_instance',None) + if not dd: + dd = get_stateless_by_name(self.app_name) + setattr(self,'_stateless_dash_app_instance',dd) + return dd - @staticmethod - def get_app_instance(id): + def handle_current_state(self): + ''' + Check to see if the current hydrated state and the saved state are different. + + If they are, then persist the current state in the database by saving the model instance. + ''' + if getattr(self,'_current_state_hydrated_changed',False) and self.save_on_change: + new_base_state = json.dumps(getattr(self,'_current_state_hydrated',{})) + if new_base_state != self.base_state: + self.base_state = new_base_state + self.save() + + def have_current_state_entry(self, wid, key): + cscoll = self.current_state() + cs = cscoll.get(wid,{}) + return key in cs + + def update_current_state(self, wid, key, value): + ''' + Update current state with a (possibly new) value associated with key + + If the key does not represent an existing entry, then ignore it + ''' + cscoll = self.current_state() + cs = cscoll.get(wid,{}) + if key in cs: + current_value = cs.get(key,None) + if current_value != value: + cs[key] = value + setattr(self,'_current_state_hydrated_changed',True) + + def current_state(self): ''' - Locate an application instance by id, either in local cache or in Database. - If in neither, then create a new instance assuming that the id is the app name. + Return the current internal state of the model instance. + + This is not necessarily the same as the persisted state stored in the self.base_state variable. ''' - local_instance = get_app_instance_by_id(id) - if local_instance: - return local_instance - try: - return DashApp.objects.get(slug=id).as_dash_instance() - except: - pass - - # Really no luck at all! - dd = get_app_by_name(id) - return dd.form_dash_instance() + cs = getattr(self,'_current_state_hydrated',None) + if not cs: + cs = json.loads(self.base_state) + setattr(self,'_current_state_hydrated',cs) + setattr(self,'_current_state_hydrated_changed',False) + return cs + + def as_dash_instance(self): + dd = self._stateless_dash_app() + base = self.current_state() + return dd.form_dash_instance(replacements=base, + specific_identifier=self.slug) def _get_base_state(self): ''' Get the base state of the object, as defined by the app.layout code, as a python dict ''' - # Get base layout response, from a base object - base_app_inst = get_app_instance_by_id(self.app_name) - if not base_app_inst: - base_app = get_app_by_name(self.app_name) - base_app_inst = base_app.form_dash_instance() + base_app_inst = self._stateless_dash_app().as_dash_instance() + # Get base layout response, from a base object base_resp = base_app_inst.locate_endpoint_function('dash-layout')() base_obj = json.loads(base_resp.data.decode('utf-8')) @@ -83,9 +110,19 @@ def populate_values(self): obj = self._get_base_state() self.base_state = json.dumps(obj) + @staticmethod + def locate_item(id, stateless=False): + if stateless: + da = get_stateless_by_name(id) + else: + da = get_object_or_404(DashApp,slug=id) + + app = da.as_dash_instance() + return da, app + class DashAppAdmin(admin.ModelAdmin): - list_display = ['instance_name','app_name','slug','creation','update',] - list_filter = ['app_name','creation','update',] + list_display = ['instance_name','app_name','slug','creation','update','save_on_change',] + list_filter = ['creation','update','save_on_change','app_name',] def _populate_values(self, request, queryset): for da in queryset: diff --git a/django_plotly_dash/templatetags/plotly_dash.py b/django_plotly_dash/templatetags/plotly_dash.py index 567f6e56..10ba444e 100644 --- a/django_plotly_dash/templatetags/plotly_dash.py +++ b/django_plotly_dash/templatetags/plotly_dash.py @@ -1,11 +1,13 @@ from django import template +from django.shortcuts import get_object_or_404 register = template.Library() from django_plotly_dash.models import DashApp +from django_plotly_dash.dash_wrapper import get_stateless_by_name @register.inclusion_tag("django_plotly_dash/plotly_item.html", takes_context=True) -def plotly_item(context, app_name, ratio=0.1, use_frameborder=False): +def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborder=False): fbs = use_frameborder and '1' or '0' @@ -24,6 +26,12 @@ def plotly_item(context, app_name, ratio=0.1, use_frameborder=False): height: 100%; """ - app = DashApp.get_app_instance(app_name) + if name is not None: + da = get_stateless_by_name(name) + if slug is not None: + da = get_object_or_404(DashApp,slug=slug) + + app = da.as_dash_instance() return locals() + diff --git a/django_plotly_dash/urls.py b/django_plotly_dash/urls.py index 1d6696b6..dc81954f 100644 --- a/django_plotly_dash/urls.py +++ b/django_plotly_dash/urls.py @@ -6,11 +6,16 @@ from .app_name import app_name, main_view_label urlpatterns = [ - path('_dash-routes', routes, name="routes"), - path('_dash-layout', layout, name="layout"), - path('_dash-dependencies', dependencies, name="dependencies"), - path('_dash-update-component', csrf_exempt(update), name="update-component"), + path('instance/_dash-routes', routes, name="routes"), + path('instance/_dash-layout', layout, name="layout"), + path('instance/_dash-dependencies', dependencies, name="dependencies"), + path('instance/_dash-update-component', csrf_exempt(update), name="update-component"), + path('instance/', main_view, name=main_view_label), - path('', main_view, name=main_view_label), + path('app/_dash-routes', routes, {'stateless':True}, name="app-routes"), + path('app/_dash-layout', layout, {'stateless':True}, name="app-layout"), + path('app/_dash-dependencies', dependencies, {'stateless':True}, name="app-dependencies"), + path('app/_dash-update-component', csrf_exempt(update), {'stateless':True}, name="app-update-component"), + path('app/', main_view, {'stateless':True}, name='app-%s'%main_view_label), ] diff --git a/django_plotly_dash/views.py b/django_plotly_dash/views.py index 7247992c..abd73ebb 100644 --- a/django_plotly_dash/views.py +++ b/django_plotly_dash/views.py @@ -1,4 +1,4 @@ -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse import json @@ -6,24 +6,27 @@ from .models import DashApp def routes(*args,**kwargs): - pass + raise NotImplementedError + +def dependencies(request, id, stateless=False, **kwargs): + da, app = DashApp.locate_item(id, stateless) -def dependencies(request, id, **kwargs): - app = DashApp.get_app_instance(id) with app.app_context(): mFunc = app.locate_endpoint_function('dash-dependencies') resp = mFunc() return HttpResponse(resp.data, content_type=resp.mimetype) -def layout(request, id, **kwargs): - app = DashApp.get_app_instance(id) +def layout(request, id, stateless=False, **kwargs): + da, app = DashApp.locate_item(id, stateless) + mFunc = app.locate_endpoint_function('dash-layout') resp = mFunc() return app.augment_initial_layout(resp) -def update(request, id, **kwargs): - app = DashApp.get_app_instance(id) +def update(request, id, stateless=False, **kwargs): + da, app = DashApp.locate_item(id, stateless) + rb = json.loads(request.body.decode('utf-8')) if app.use_dash_dispatch(): @@ -37,15 +40,21 @@ def update(request, id, **kwargs): resp = mFunc() else: # Use direct dispatch with extra arguments in the argMap - argMap = {'id':id, - 'session':request.session} + app_state = request.session.get("django_plotly_dash",dict()) + argMap = {'dash_app_id': id, + 'dash_app': da, + 'user': request.user, + 'session_state': app_state} resp = app.dispatch_with_args(rb, argMap) + request.session['django_plotly_dash'] = app_state + da.handle_current_state() return HttpResponse(resp.data, content_type=resp.mimetype) -def main_view(request, id, **kwargs): - app = DashApp.get_app_instance(id) +def main_view(request, id, stateless=False, **kwargs): + da, app = DashApp.locate_item(id, stateless) + mFunc = app.locate_endpoint_function() resp = mFunc() return HttpResponse(resp) diff --git a/docs/extended_callbacks.rst b/docs/extended_callbacks.rst new file mode 100644 index 00000000..cff71624 --- /dev/null +++ b/docs/extended_callbacks.rst @@ -0,0 +1,58 @@ +.. _extended_callbacks: + +Extended callback syntax +======================== + +The ``DjangoDash`` class allows callbacks to request extra arguments when registered. + +To do this, simply replace ``callback`` with ``expanded_callback`` when registering any callback. This will cause all of the callbacks +registered with this application +to receive extra ``kwargs`` in addition to the callback parameters. + +For example, the ``plotly_apps.py`` example contains this dash application: + +.. code-block:: python + + import dash + import dash_core_components as dcc + import dash_html_components as 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.expanded_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),str(kwargs)) + +The additional arguments, which are reported as the ``kwargs`` content in this example, include + +: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. +: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. + +The ``DashApp`` model instance can also be configured to persist itself on any change. This is discussed +in the :ref:`models_and_state` section. + + +.. _using_session_state: +Using session state +------------------ + +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 callaback; 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. + diff --git a/docs/index.rst b/docs/index.rst index 06d66d94..ce6f4727 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,9 @@ Contents introduction installation simple_use + extended_callbacks + template_tags + models_and_state Indices and tables diff --git a/docs/introduction.rst b/docs/introduction.rst index 9ade7119..d9354f46 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -3,7 +3,8 @@ 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 +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 @@ -13,3 +14,18 @@ these features: 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. + +.. _overview: +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. diff --git a/docs/models_and_state.rst b/docs/models_and_state.rst new file mode 100644 index 00000000..78474ef1 --- /dev/null +++ b/docs/models_and_state.rst @@ -0,0 +1,53 @@ +.. _models_and_state: + +Django models and application state +================ + +The ``django_plotly_dash`` application defines a ``DashApp`` model. This represents an instance of application state. + +.. code-block:: python + + class DashApp(models.Model): + ''' + An instance of this model represents a Dash application and its internal state + ''' + app_name = models.CharField(max_length=100, blank=False, null=False, unique=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 ``app_name`` corresponds to an application registered through the instantiation of a ``DjangoDash`` object. 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 ``_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``. diff --git a/docs/simple_use.rst b/docs/simple_use.rst index a7fae581..c0175c7e 100644 --- a/docs/simple_use.rst +++ b/docs/simple_use.rst @@ -1,20 +1,22 @@ .. _simple_use: -Simple Usage +Simple usage ============ -To use existing dash applications, first register them using the ``DelayedDash`` class. This +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:: +Taking a simple example inspired by the excellent `getting started `_ guide: + +.. code-block:: python import dash import dash_core_components as dcc import dash_html_components as html - from django_plotly_dash import DelayedDash + from django_plotly_dash import DjangoDash - app = DelayedDash('SimpleExample') # replaces dash.Dash + app = DjangoDash('SimpleExample') # replaces dash.Dash app.layout = html.Div([ dcc.RadioItems( @@ -47,12 +49,12 @@ Taking a simple example inspired by the excellent `getting started