From 06183b79cc73b684f61ebc87cd51147c4a573ebc Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 29 May 2018 20:31:28 -0700 Subject: [PATCH 01/13] Extended wrapper to allow for proxy stub --- django_plotly_dash/dash_wrapper.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index b1c4af39..6e6f792a 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -72,7 +72,7 @@ def have_current_state_entry(self, wid, key): 'Do nothing impl - only matters if state present' pass - def get_base_pathname(self, specific_identifier): + def get_base_pathname(self, specific_identifier, stub=None): if not specific_identifier: app_pathname = "%s:app-%s"% (app_name, main_view_label) ndid = self._uid @@ -83,13 +83,16 @@ def get_base_pathname(self, specific_identifier): try: full_url = reverse(app_pathname,kwargs={'id':ndid}) except: - full_url = "/%s/" %ndid + if stub: + full_url = "%s/%s/" %(stub,ndid) + else: + full_url = ndid return ndid, full_url - def form_dash_instance(self, replacements=None, specific_identifier=None): + def form_dash_instance(self, replacements=None, specific_identifier=None, stub=None): - ndid, base_pathname = self.get_base_pathname(specific_identifier) + ndid, base_pathname = self.get_base_pathname(specific_identifier, stub) rd = NotDash(base_pathname=base_pathname, expanded_callbacks = self._expanded_callbacks, @@ -153,7 +156,7 @@ def __init__(self, base_pathname=None, replacements = None, ndid=None, expanded_ super(NotDash, self).__init__(**kwargs) self.css.config.serve_locally = True - #self.css.config.serve_locally = False + self.css.config.serve_locally = False self.scripts.config.serve_locally = self.css.config.serve_locally From 9dafb2c53f8d2d71adf7144a5c5710975490cbd0 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 29 May 2018 20:32:14 -0700 Subject: [PATCH 02/13] Push version to 0.2.1 --- django_plotly_dash/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_plotly_dash/__init__.py b/django_plotly_dash/__init__.py index 36a143b7..0274e21d 100644 --- a/django_plotly_dash/__init__.py +++ b/django_plotly_dash/__init__.py @@ -1,6 +1,6 @@ # -__version__ = "0.2.0" +__version__ = "0.2.1" from .dash_wrapper import DjangoDash From c95ab012e265ba3325360b5dd7e28ba7868ec06b Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Tue, 29 May 2018 21:47:14 -0700 Subject: [PATCH 03/13] Unwind attempt to use proxy in jupyterhub, at least for now --- django_plotly_dash/dash_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 6e6f792a..4f05ce9f 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -83,10 +83,10 @@ def get_base_pathname(self, specific_identifier, stub=None): try: full_url = reverse(app_pathname,kwargs={'id':ndid}) except: - if stub: + if stub is not None: full_url = "%s/%s/" %(stub,ndid) else: - full_url = ndid + full_url = "/%s/"%ndid return ndid, full_url From 64fe81d1ba864c5fe14d85cbc0d403ac429707d2 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 30 May 2018 10:13:12 -0700 Subject: [PATCH 04/13] Rename wrappers for Flask and Dash --- django_plotly_dash/dash_wrapper.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 4f05ce9f..8af4682d 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -94,10 +94,10 @@ def form_dash_instance(self, replacements=None, specific_identifier=None, stub=N ndid, base_pathname = self.get_base_pathname(specific_identifier, stub) - rd = NotDash(base_pathname=base_pathname, - expanded_callbacks = self._expanded_callbacks, - replacements = replacements, - ndid = ndid) + rd = WrappedDash(base_pathname=base_pathname, + expanded_callbacks = self._expanded_callbacks, + replacements = replacements, + ndid = ndid) rd.layout = self.layout @@ -124,7 +124,7 @@ def expanded_callback(self, output, inputs=[], state=[], events=[]): self._expanded_callbacks = True return self.callback(output, inputs, state, events) -class NotFlask: +class PseudoFlask: def __init__(self): self.config = {} self.endpoints = {} @@ -141,19 +141,19 @@ def before_first_request(self,*args,**kwargs): def run(self,*args,**kwargs): pass -class NotDash(Dash): +class WrappedDash(Dash): def __init__(self, base_pathname=None, replacements = None, ndid=None, expanded_callbacks=False, **kwargs): self._uid = ndid self._flask_app = Flask(self._uid) - self._notflask = NotFlask() + self._notflask = PseudoFlask() self._base_pathname = base_pathname kwargs['url_base_pathname'] = self._base_pathname kwargs['server'] = self._notflask - super(NotDash, self).__init__(**kwargs) + super(WrappedDash, self).__init__(**kwargs) self.css.config.serve_locally = True self.css.config.serve_locally = False @@ -276,10 +276,10 @@ def _fix_callback_item(self, item): return item def callback(self, output, inputs=[], state=[], events=[]): - return super(NotDash, self).callback(self._fix_callback_item(output), - [self._fix_callback_item(x) for x in inputs], - [self._fix_callback_item(x) for x in state], - [self._fix_callback_item(x) for x in events]) + return super(WrappedDash, self).callback(self._fix_callback_item(output), + [self._fix_callback_item(x) for x in inputs], + [self._fix_callback_item(x) for x in state], + [self._fix_callback_item(x) for x in events]) def dispatch(self): import flask From 49c6b054c65dd79c561648a5f62ada9ab45cbe88 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 30 May 2018 16:25:26 -0700 Subject: [PATCH 05/13] Refactor formation of dash instance to clarify name generation --- django_plotly_dash/dash_wrapper.py | 23 +++++++++++------------ django_plotly_dash/models.py | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 8af4682d..9017ea30 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -60,7 +60,7 @@ def as_dash_instance(self): ''' Form a dash instance, for stateless use of this app ''' - return self.form_dash_instance() + return self.do_form_dash_instance() def handle_current_state(self): 'Do nothing impl - only matters if state present' @@ -72,7 +72,7 @@ def have_current_state_entry(self, wid, key): 'Do nothing impl - only matters if state present' pass - def get_base_pathname(self, specific_identifier, stub=None): + def get_base_pathname(self, specific_identifier): if not specific_identifier: app_pathname = "%s:app-%s"% (app_name, main_view_label) ndid = self._uid @@ -80,19 +80,18 @@ def get_base_pathname(self, specific_identifier, stub=None): app_pathname="%s:%s" % (app_name, main_view_label) ndid = specific_identifier - try: - full_url = reverse(app_pathname,kwargs={'id':ndid}) - except: - if stub is not None: - full_url = "%s/%s/" %(stub,ndid) - else: - full_url = "/%s/"%ndid - + full_url = reverse(app_pathname,kwargs={'id':ndid}) return ndid, full_url - def form_dash_instance(self, replacements=None, specific_identifier=None, stub=None): + def do_form_dash_instance(self, replacements=None, specific_identifier=None): - ndid, base_pathname = self.get_base_pathname(specific_identifier, stub) + ndid, base_pathname = self.get_base_pathname(specific_identifier) + return self.form_dash_instance(replacements, ndid, base_pathname) + + def form_dash_instance(self, replacements=None, ndid=None, base_pathname=None): + + if ndid is None: + ndid = self._uid rd = WrappedDash(base_pathname=base_pathname, expanded_callbacks = self._expanded_callbacks, diff --git a/django_plotly_dash/models.py b/django_plotly_dash/models.py index dd6403ae..472f64d7 100644 --- a/django_plotly_dash/models.py +++ b/django_plotly_dash/models.py @@ -84,8 +84,8 @@ def current_state(self): 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) + return dd.do_form_dash_instance(replacements=base, + specific_identifier=self.slug) def _get_base_state(self): ''' From 9c36c2d1176b441c12be24acf78960306b4c087e Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 30 May 2018 18:33:13 -0700 Subject: [PATCH 06/13] First steps towards an app model --- django_plotly_dash/admin.py | 5 ++++- django_plotly_dash/models.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/django_plotly_dash/admin.py b/django_plotly_dash/admin.py index 4580ac5b..491f1515 100644 --- a/django_plotly_dash/admin.py +++ b/django_plotly_dash/admin.py @@ -1,6 +1,9 @@ from django.contrib import admin -from .models import DashApp, DashAppAdmin +from .models import (DashApp, DashAppAdmin, + StatelessApp, StatelessAppAdmin, + ) admin.site.register(DashApp, DashAppAdmin) +admin.site.register(StatelessApp, StatelessAppAdmin) diff --git a/django_plotly_dash/models.py b/django_plotly_dash/models.py index dd6403ae..3db5f8b0 100644 --- a/django_plotly_dash/models.py +++ b/django_plotly_dash/models.py @@ -7,6 +7,25 @@ import json +class StatelessApp(models.Model): + ''' + A stateless Dash app. An instance of this model represents a dash app witout any specific state + ''' + app_name = models.CharField(max_length=100, blank=False, null=False, unique=True) + slug = models.SlugField(max_length=110, unique=True, blank=True) + + def __str__(self): + return self.app_name + + def save(self, *args, **kwargs): + if not self.slug or len(self.slug) < 2: + self.slug = slugify(self.app_name) + return super(StatelessApp, self).save(*args,**kwargs) + +class StatelessAppAdmin(admin.ModelAdmin): + list_display = ['app_name','slug',] + list_filter = ['app_name','slug',] + class DashApp(models.Model): ''' An instance of this model represents a dash application and its internal state From ad8f48975e67dd5a434bb8920aaf1abce79e6de6 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Wed, 30 May 2018 18:33:42 -0700 Subject: [PATCH 07/13] Migration for app model --- .../migrations/0004_add_stateless_app.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 django_plotly_dash/migrations/0004_add_stateless_app.py diff --git a/django_plotly_dash/migrations/0004_add_stateless_app.py b/django_plotly_dash/migrations/0004_add_stateless_app.py new file mode 100644 index 00000000..dbe6f9e4 --- /dev/null +++ b/django_plotly_dash/migrations/0004_add_stateless_app.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.5 on 2018-05-27 00:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_plotly_dash', '0003_auto_20180514_1802'), + ] + + operations = [ + migrations.CreateModel( + name='StatelessApp', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('app_name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(blank=True, max_length=110, unique=True)), + ], + ), + ] From 39fb8483a6070bc181ff9dc7230e16094d928849 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Thu, 31 May 2018 12:48:07 -0700 Subject: [PATCH 08/13] Move version to 0.2.2 before pushing a patch --- django_plotly_dash/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_plotly_dash/__init__.py b/django_plotly_dash/__init__.py index 0274e21d..08a33e0b 100644 --- a/django_plotly_dash/__init__.py +++ b/django_plotly_dash/__init__.py @@ -1,6 +1,6 @@ # -__version__ = "0.2.1" +__version__ = "0.2.2" from .dash_wrapper import DjangoDash From ae896fb5c752d3b61ca4bbdcbdebef2ab6069af0 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Fri, 8 Jun 2018 11:51:31 -0700 Subject: [PATCH 09/13] Refactor access to stateless app locator --- django_plotly_dash/templatetags/plotly_dash.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django_plotly_dash/templatetags/plotly_dash.py b/django_plotly_dash/templatetags/plotly_dash.py index 10ba444e..69ad13d9 100644 --- a/django_plotly_dash/templatetags/plotly_dash.py +++ b/django_plotly_dash/templatetags/plotly_dash.py @@ -3,8 +3,7 @@ register = template.Library() -from django_plotly_dash.models import DashApp -from django_plotly_dash.dash_wrapper import get_stateless_by_name +from django_plotly_dash.models import DashApp, get_stateless_by_name @register.inclusion_tag("django_plotly_dash/plotly_item.html", takes_context=True) def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborder=False): From def91a6e63656beda198bee75e383e3f3e7c1d2d Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Fri, 8 Jun 2018 12:04:34 -0700 Subject: [PATCH 10/13] More rationalisation for the process to locate stateless apps --- django_plotly_dash/dash_wrapper.py | 2 +- django_plotly_dash/models.py | 5 ++++- django_plotly_dash/templatetags/plotly_dash.py | 12 ++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 9017ea30..0a0d132e 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -21,7 +21,7 @@ def add_usable_app(name, app): usable_apps[name] = app return name -def get_stateless_by_name(name): +def get_local_stateless_by_name(name): ''' Locate a registered dash app by name, and return a DjangoDash instance encapsulating the app. ''' diff --git a/django_plotly_dash/models.py b/django_plotly_dash/models.py index 66c4e278..0283fcb9 100644 --- a/django_plotly_dash/models.py +++ b/django_plotly_dash/models.py @@ -3,7 +3,7 @@ from django.utils.text import slugify from django.shortcuts import get_object_or_404 -from .dash_wrapper import get_stateless_by_name +from .dash_wrapper import get_local_stateless_by_name import json @@ -26,6 +26,9 @@ class StatelessAppAdmin(admin.ModelAdmin): list_display = ['app_name','slug',] list_filter = ['app_name','slug',] +def get_stateless_by_name(name): + return get_local_stateless_by_name(name) + class DashApp(models.Model): ''' An instance of this model represents a dash application and its internal state diff --git a/django_plotly_dash/templatetags/plotly_dash.py b/django_plotly_dash/templatetags/plotly_dash.py index 69ad13d9..6b0773f8 100644 --- a/django_plotly_dash/templatetags/plotly_dash.py +++ b/django_plotly_dash/templatetags/plotly_dash.py @@ -3,7 +3,7 @@ register = template.Library() -from django_plotly_dash.models import DashApp, get_stateless_by_name +from django_plotly_dash.models import DashApp @register.inclusion_tag("django_plotly_dash/plotly_item.html", takes_context=True) def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborder=False): @@ -25,12 +25,16 @@ def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborde height: 100%; """ + app = None + if name is not None: - da = get_stateless_by_name(name) + da, app = DashApp.locate_item(name, stateless=True) if slug is not None: - da = get_object_or_404(DashApp,slug=slug) + da, app = DashApp.locate_item(slug, stateless=False) + + if not app: + app = da.as_dash_instance() - app = da.as_dash_instance() return locals() From 921df5be063de21f05526ae40ff88c0dc97fb307 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Fri, 8 Jun 2018 13:22:45 -0700 Subject: [PATCH 11/13] All stateless apps are now saved into the database when first seen --- django_plotly_dash/migrations/0001_initial.py | 20 +++++- .../migrations/0002_add_examples.py | 36 +++++++++++ .../migrations/0002_simple_example_state.py | 28 --------- .../migrations/0003_auto_20180514_1802.py | 23 ------- .../migrations/0004_add_stateless_app.py | 21 ------- django_plotly_dash/models.py | 63 ++++++++++++------- 6 files changed, 95 insertions(+), 96 deletions(-) create mode 100644 django_plotly_dash/migrations/0002_add_examples.py delete mode 100644 django_plotly_dash/migrations/0002_simple_example_state.py delete mode 100644 django_plotly_dash/migrations/0003_auto_20180514_1802.py delete mode 100644 django_plotly_dash/migrations/0004_add_stateless_app.py diff --git a/django_plotly_dash/migrations/0001_initial.py b/django_plotly_dash/migrations/0001_initial.py index 8cb034e5..f4016aea 100644 --- a/django_plotly_dash/migrations/0001_initial.py +++ b/django_plotly_dash/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 2.0.5 on 2018-05-10 21:48 +# Generated by Django 2.0.5 on 2018-06-08 20:02 from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -15,12 +16,25 @@ class Migration(migrations.Migration): name='DashApp', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('app_name', models.CharField(max_length=100)), ('instance_name', models.CharField(blank=True, max_length=100, unique=True)), ('slug', models.SlugField(blank=True, max_length=110, unique=True)), - ('base_state', models.TextField()), + ('base_state', models.TextField(default='{}')), ('creation', models.DateTimeField(auto_now_add=True)), ('update', models.DateTimeField(auto_now=True)), + ('save_on_change', models.BooleanField(default=False)), ], ), + migrations.CreateModel( + name='StatelessApp', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('app_name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(blank=True, max_length=110, unique=True)), + ], + ), + migrations.AddField( + model_name='dashapp', + name='stateless_app', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='django_plotly_dash.StatelessApp'), + ), ] diff --git a/django_plotly_dash/migrations/0002_add_examples.py b/django_plotly_dash/migrations/0002_add_examples.py new file mode 100644 index 00000000..2a97dcc4 --- /dev/null +++ b/django_plotly_dash/migrations/0002_add_examples.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.5 on 2018-06-08 20:03 + +from django.db import migrations + +def addExamples(apps, schema_editor): + + DashApp = apps.get_model("django_plotly_dash","DashApp") + StatelessApp = apps.get_model("django_plotly_dash","StatelessApp") + + sa1 = StatelessApp(app_name="SimpleExample", + slug="simple-example") + + sa1.save() + + da1 = DashApp(stateless_app=sa1, + instance_name="SimpleExample-1", + slug="simpleexample-1", + base_state='{"dropdown-color":{"value":"blue"},"dropdown-size":{"value":"small"}}') + + da1.save() + + +def remExamples(apps, schema_editor): + + DashApp.objects.all().delete() + StatelessApp.objects.all().delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('django_plotly_dash', '0001_initial'), + ] + + operations = [ + migrations.RunPython(addExamples, remExamples), + ] diff --git a/django_plotly_dash/migrations/0002_simple_example_state.py b/django_plotly_dash/migrations/0002_simple_example_state.py deleted file mode 100644 index c6cef1e3..00000000 --- a/django_plotly_dash/migrations/0002_simple_example_state.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.0.5 on 2018-05-10 23:26 - -from django.db import migrations - -def getDA(apps): - return apps.get_model("django_plotly_dash","DashApp") - -def forward(apps, schema_editor): - DashApp = getDA(apps) - da = DashApp(app_name="SimpleExample", - instance_name="SimpleExample-1", - slug="simpleexample-1", - base_state='{"dropdown-color":{"value":"blue"},"dropdown-size":{"value":"small"}}') - da.save() - -def backward(apps, schema_editor): - DashApp = getDA(apps) - DashApp.objects.all().delete() - -class Migration(migrations.Migration): - - dependencies = [ - ('django_plotly_dash', '0001_initial'), - ] - - operations = [ - migrations.RunPython(forward, backward), - ] diff --git a/django_plotly_dash/migrations/0003_auto_20180514_1802.py b/django_plotly_dash/migrations/0003_auto_20180514_1802.py deleted file mode 100644 index b383c0be..00000000 --- a/django_plotly_dash/migrations/0003_auto_20180514_1802.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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/migrations/0004_add_stateless_app.py b/django_plotly_dash/migrations/0004_add_stateless_app.py deleted file mode 100644 index dbe6f9e4..00000000 --- a/django_plotly_dash/migrations/0004_add_stateless_app.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.0.5 on 2018-05-27 00:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_plotly_dash', '0003_auto_20180514_1802'), - ] - - operations = [ - migrations.CreateModel( - name='StatelessApp', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('app_name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField(blank=True, max_length=110, unique=True)), - ], - ), - ] diff --git a/django_plotly_dash/models.py b/django_plotly_dash/models.py index 0283fcb9..2b17d6db 100644 --- a/django_plotly_dash/models.py +++ b/django_plotly_dash/models.py @@ -7,9 +7,12 @@ import json +def get_stateless_by_name(name): + return get_local_stateless_by_name(name) + class StatelessApp(models.Model): ''' - A stateless Dash app. An instance of this model represents a dash app witout any specific state + A stateless Dash app. An instance of this model represents a dash app without any specific state ''' app_name = models.CharField(max_length=100, blank=False, null=False, unique=True) slug = models.SlugField(max_length=110, unique=True, blank=True) @@ -22,18 +25,34 @@ def save(self, *args, **kwargs): self.slug = slugify(self.app_name) return super(StatelessApp, self).save(*args,**kwargs) + def as_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 + +def find_stateless_by_name(name): + try: + dsa = StatelessApp.objects.get(app_name=name) + return dsa.as_dash_app() + except: + pass + + da = get_stateless_by_name(name) + dsa = StatelessApp(app_name=name) + dsa.save() + return da + class StatelessAppAdmin(admin.ModelAdmin): list_display = ['app_name','slug',] list_filter = ['app_name','slug',] -def get_stateless_by_name(name): - return get_local_stateless_by_name(name) - 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) + stateless_app = models.ForeignKey(StatelessApp, on_delete=models.PROTECT, unique=False, null=False, blank=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="{}") # If mandating postgresql then this could be a JSONField @@ -46,19 +65,12 @@ def __str__(self): def save(self, *args, **kwargs): if not self.instance_name: - existing_count = DashApp.objects.filter(app_name=self.app_name).count() - self.instance_name = "%s-%i" %(self.app_name, existing_count+1) + existing_count = DashApp.objects.all().count() + self.instance_name = "%s-%i" %(self.stateless_app.app_name, existing_count+1) if not self.slug or len(self.slug) < 2: self.slug = slugify(self.instance_name) super(DashApp, self).save(*args,**kwargs) - 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 - def handle_current_state(self): ''' Check to see if the current hydrated state and the saved state are different. @@ -104,7 +116,7 @@ def current_state(self): return cs def as_dash_instance(self): - dd = self._stateless_dash_app() + dd = self.stateless_app.as_dash_app() base = self.current_state() return dd.do_form_dash_instance(replacements=base, specific_identifier=self.slug) @@ -113,7 +125,7 @@ def _get_base_state(self): ''' Get the base state of the object, as defined by the app.layout code, as a python dict ''' - base_app_inst = self._stateless_dash_app().as_dash_instance() + base_app_inst = self.stateless_app.as_dash_app().as_dash_instance() # Get base layout response, from a base object base_resp = base_app_inst.locate_endpoint_function('dash-layout')() @@ -135,7 +147,7 @@ def populate_values(self): @staticmethod def locate_item(id, stateless=False): if stateless: - da = get_stateless_by_name(id) + da = find_stateless_by_name(id) else: da = get_object_or_404(DashApp,slug=id) @@ -143,13 +155,22 @@ def locate_item(id, stateless=False): return da, app class DashAppAdmin(admin.ModelAdmin): - list_display = ['instance_name','app_name','slug','creation','update','save_on_change',] - list_filter = ['creation','update','save_on_change','app_name',] + list_display = ['instance_name','stateless_app','slug','creation','update','save_on_change',] + list_filter = ['creation','update','save_on_change','stateless_app',] def _populate_values(self, request, queryset): for da in queryset: da.populate_values() da.save() - _populate_values.short_description = "Populate app" + _populate_values.short_description = "Populate app instance" + + def _clone(self, request, queryset): + for da in queryset: + nda = DashApp(stateless_app=da.stateless_app, + base_state=da.base_state, + save_on_change=da.save_on_change) + nda.save() + + _clone.short_description = "Clone app instance" - actions = ['_populate_values',] + actions = ['_populate_values','_clone',] From 928e4b8c5591c2513afc823518965b74cf1f7c6b Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Fri, 8 Jun 2018 13:52:06 -0700 Subject: [PATCH 12/13] Add documentation and bump version number to 0.3.0 --- django_plotly_dash/__init__.py | 2 +- django_plotly_dash/models.py | 3 +++ docs/models_and_state.rst | 43 ++++++++++++++++++++++++++++++---- docs/template_tags.rst | 3 +-- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/django_plotly_dash/__init__.py b/django_plotly_dash/__init__.py index 08a33e0b..ba2ec4d3 100644 --- a/django_plotly_dash/__init__.py +++ b/django_plotly_dash/__init__.py @@ -1,6 +1,6 @@ # -__version__ = "0.2.2" +__version__ = "0.3.0" from .dash_wrapper import DjangoDash diff --git a/django_plotly_dash/models.py b/django_plotly_dash/models.py index 2b17d6db..0a183496 100644 --- a/django_plotly_dash/models.py +++ b/django_plotly_dash/models.py @@ -26,6 +26,9 @@ def save(self, *args, **kwargs): return super(StatelessApp, self).save(*args,**kwargs) def as_dash_app(self): + ''' + Return a DjangoDash instance of the dash application + ''' dd = getattr(self,'_stateless_dash_app_instance',None) if not dd: dd = get_stateless_by_name(self.app_name) diff --git a/docs/models_and_state.rst b/docs/models_and_state.rst index 78474ef1..7c5b782a 100644 --- a/docs/models_and_state.rst +++ b/docs/models_and_state.rst @@ -1,9 +1,43 @@ .. _models_and_state: Django models and application state -================ +=================================== -The ``django_plotly_dash`` application defines a ``DashApp`` model. This represents an instance of application state. +The ``django_plotly_dash`` application defines ``DashApp`` and ``StatelessApp`` models. + +The ``StatelessApp`` model +---------------------- + +An instance of the ``StatelessApp`` model represents a single dash application. Every instantiation of +a ``DjangoDash`` object is registered, and any object that is referenced through the ``DashApp`` model - this +includes all template access as well as model instances themselves - causes a ``StatelessApp`` model instance to +be created if one does not already exist. + +.. code-block:: python + + class StatelessApp(models.Model): + ''' + A stateless Dash app. + + An instance of this model represents a dash app without any specific state + ''' + + app_name = models.CharField(max_length=100, blank=False, null=False, unique=True) + slug = models.SlugField(max_length=110, unique=True, blank=True) + + def as_dash_app(self): + ''' + Return a DjangoDash instance of the dash application + ''' + +The main role of a ``StatelessApp`` instance is to manage access to the associated ``DjangoDash`` object, as +expsosed through the ``as_dash_app`` member +function. + +The ``DashApp`` model +--------------------- + +An instance of the ``DashApp`` model represents an instance of application state. .. code-block:: python @@ -11,7 +45,8 @@ The ``django_plotly_dash`` application defines a ``DashApp`` model. This represe ''' 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) + stateless_app = models.ForeignKey(StatelessApp, on_delete=models.PROTECT, + unique=False, null=False, blank=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="{}") @@ -36,7 +71,7 @@ The ``django_plotly_dash`` application defines a ``DashApp`` model. This represe 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 +The ``stateless_app`` references an instance of the ``StatelessApp`` model described above. 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 diff --git a/docs/template_tags.rst b/docs/template_tags.rst index 6edaf789..3f1e5aac 100644 --- a/docs/template_tags.rst +++ b/docs/template_tags.rst @@ -15,7 +15,7 @@ a ``DjangoDash`` app within a page as a responsive ``iframe`` element. The tag arguments are: :name = None: The name of the application, as passed to a ``DjangoDash`` constructor. -:slug = None: The slug of the application instance. +:slug = None: The slug of an existing ``DashApp`` instance. :da = None: An existing ``django_plotly_dash.models.DashApp`` model instance. :ratio = 0.1: The ratio of height to width. The container will inherit its width as 100% of its parent, and then rely on this ratio to set its height. @@ -24,4 +24,3 @@ The tag arguments are: At least one of ``da``, ``slug`` or ``name`` must be provided. An object identified by ``slug`` will always be used, otherwise any identified by ``name`` will be. If either of these arguments are provided, they must resolve to valid objects even if not used. If neither are provided, then the model instance in ``da`` will be used. - From 53903b7377d73d990fbc2cdb35783098c552df47 Mon Sep 17 00:00:00 2001 From: Mark Gibbs Date: Fri, 8 Jun 2018 13:52:58 -0700 Subject: [PATCH 13/13] Rudimentary build script, missing tests, coverage and linter --- do_the_build | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 do_the_build diff --git a/do_the_build b/do_the_build new file mode 100644 index 00000000..21a42a93 --- /dev/null +++ b/do_the_build @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# +source env/bin/activate +python setup.py sdist +python setup.py bdist_wheel + +#twine upload dist/* +#cd docs && sphinx-autobuild . _build/html +#grip +