Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion django_plotly_dash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#

__version__ = "0.2.0"
__version__ = "0.3.0"

from .dash_wrapper import DjangoDash

5 changes: 4 additions & 1 deletion django_plotly_dash/admin.py
Original file line number Diff line number Diff line change
@@ -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)

44 changes: 23 additions & 21 deletions django_plotly_dash/dash_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
'''
Expand Down Expand Up @@ -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'
Expand All @@ -80,21 +80,23 @@ def get_base_pathname(self, specific_identifier):
app_pathname="%s:%s" % (app_name, main_view_label)
ndid = specific_identifier

try:
full_url = reverse(app_pathname,kwargs={'id':ndid})
except:
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):
def do_form_dash_instance(self, replacements=None, specific_identifier=None):

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 = 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

Expand All @@ -121,7 +123,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 = {}
Expand All @@ -138,22 +140,22 @@ 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
self.css.config.serve_locally = False

self.scripts.config.serve_locally = self.css.config.serve_locally

Expand Down Expand Up @@ -273,10 +275,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
Expand Down
20 changes: 17 additions & 3 deletions django_plotly_dash/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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'),
),
]
36 changes: 36 additions & 0 deletions django_plotly_dash/migrations/0002_add_examples.py
Original file line number Diff line number Diff line change
@@ -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),
]
28 changes: 0 additions & 28 deletions django_plotly_dash/migrations/0002_simple_example_state.py

This file was deleted.

23 changes: 0 additions & 23 deletions django_plotly_dash/migrations/0003_auto_20180514_1802.py

This file was deleted.

86 changes: 66 additions & 20 deletions django_plotly_dash/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,59 @@
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

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 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 __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)

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)
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',]

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
Expand All @@ -24,19 +68,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.
Expand Down Expand Up @@ -82,16 +119,16 @@ 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.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):
'''
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')()
Expand All @@ -113,21 +150,30 @@ 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)

app = da.as_dash_instance()
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',]
Loading