diff --git a/demo/demo/consumers.py b/demo/demo/consumers.py new file mode 100644 index 00000000..f5a21297 --- /dev/null +++ b/demo/demo/consumers.py @@ -0,0 +1,60 @@ +from channels.generic.websocket import WebsocketConsumer + +import json + +ALL_CONSUMERS = [] + +class MessageConsumer(WebsocketConsumer): + def __init__(self, *args, **kwargs): + super(MessageConsumer, self).__init__(*args, **kwargs) + global ALL_CONSUMERS + ALL_CONSUMERS.append(self) + + def connect(self): + self.accept() + + def disconnect(self, close_code): + ac = [] + global ALL_CONSUMERS + for c in ALL_CONSUMERS: + if c != self: + ac.append(c) + ALL_CONSUMERS = ac + + def send_to_widgets(self, channel_name, label, value): + message = json.dumps({'channel_name':channel_name, + 'label':label, + 'value':value}) + global ALL_CONSUMERS + + for c in ALL_CONSUMERS: + c.send(message) + + def receive(self, text_data): + message = json.loads(text_data) + + message_type = message.get('type','unknown_type') + + if message_type == 'connection_triplet': + + channel_name = message.get('channel_name',"UNNAMED_CHANNEL") + uid = message.get('uid',"0000-0000") + label = message.get('label','DEFAULT$LABEL') + + # For now, send the uid as value. This essentially 'resets' the value + # each time the periodic connection announcement is made + self.send_to_widgets(channel_name=channel_name, + label=label, + value=uid) + else: + # Not a periodic control message, so do something useful + # For now, this is just pushing to all other consumers indiscrimnately + + channel_name = message.get('channel_name',"UNNAMED_CHANNEL") + uid = message.get('uid',"0000-0000") + value = message.get('value',{'source_uid':uid}) + label = message.get('label','DEFAULT$LABEL') + + self.send_to_widgets(channel_name=channel_name, + label=label, + value=value) diff --git a/demo/demo/plotly_apps.py b/demo/demo/plotly_apps.py index a6bb63d8..78294c78 100644 --- a/demo/demo/plotly_apps.py +++ b/demo/demo/plotly_apps.py @@ -2,6 +2,8 @@ import dash_core_components as dcc import dash_html_components as html +import dpd_components as dpd + from django_plotly_dash import DjangoDash app = DjangoDash('SimpleExample') @@ -52,3 +54,33 @@ def callback_c(*args,**kwargs): da = kwargs['dash_app'] return "Args are [%s] and kwargs are %s" %(",".join(args),str(kwargs)) +a3 = DjangoDash("Connected") + +a3.layout = html.Div([ + dpd.Pipe(id="dynamic", + value="Dynamo 123", + label="rotational energy", + channel_name="test_widget_channel", + uid="need_to_generate_this"), + dpd.Pipe(id="also_dynamic", + value="Alternator 456", + label="momentum", + channel_name="test_widget_channel", + uid="and_this_one"), + dpd.DPDirectComponent(id="direct"), + 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-three") + ]) + +@a3.expanded_callback( + dash.dependencies.Output('output-three','children'), + [dash.dependencies.Input('dynamic','value'), + dash.dependencies.Input('dynamic','label'), + dash.dependencies.Input('also_dynamic','value'), + dash.dependencies.Input('dropdown-one','value'), + ]) +def callback_a3(*args, **kwargs): + da = kwargs['dash_app'] + return "Args are [%s] and kwargs are %s" %(",".join(args),str(kwargs)) diff --git a/demo/demo/routing.py b/demo/demo/routing.py new file mode 100644 index 00000000..437bf966 --- /dev/null +++ b/demo/demo/routing.py @@ -0,0 +1,10 @@ +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack + +from django.conf.urls import url + +from .consumers import MessageConsumer + +application = ProtocolTypeRouter({ + 'websocket': AuthMiddlewareStack(URLRouter([url('ws/channel', MessageConsumer),])), + }) diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 2371cc06..3fc9af3a 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -38,6 +38,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', + 'channels', + 'django_plotly_dash.apps.DjangoPlotlyDashConfig', ] @@ -71,6 +73,7 @@ WSGI_APPLICATION = 'demo.wsgi.application' +ASGI_APPLICATION = 'demo.routing.application' # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases @@ -127,6 +130,10 @@ import dash_core_components as dcc _rname = os.path.join(os.path.dirname(dcc.__file__),'..') -for dash_module_name in ['dash_core_components','dash_html_components','dash_renderer',]: +for dash_module_name in ['dash_core_components', + 'dash_html_components', + 'dash_renderer',]: STATICFILES_DIRS.append( ("dash/%s"%dash_module_name, os.path.join(_rname,dash_module_name)) ) +# Fudge to work with channels in debug mode +STATICFILES_DIRS.append(("dash/dpd_components","/home/mark/local/dpd-components/lib")) diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html index 74f318dc..fae11fa9 100644 --- a/demo/demo/templates/index.html +++ b/demo/demo/templates/index.html @@ -1,8 +1,16 @@ -{%load plotly_dash%} - Simple stuff + + {%load plotly_dash%} + Simple stuff + +
+

Navigational links : + Main Page + Second Page +

+
Content here {%plotly_app slug="simpleexample-1" ratio=0.2 %} @@ -15,5 +23,14 @@ Content here {%plotly_app name="Ex2"%}
- +
+ WS Content here + {%plotly_app name="Connected"%} +
+
+ WS Content here + {%plotly_app slug="connected-2"%} +
+ +{%plotly_message_pipe%} diff --git a/demo/demo/templates/second_page.html b/demo/demo/templates/second_page.html new file mode 100644 index 00000000..38dc6ffd --- /dev/null +++ b/demo/demo/templates/second_page.html @@ -0,0 +1,23 @@ + + + + {%load plotly_dash%} + More Examples + + +
+

Navigational links : + Main Page + Second Page +

+
+
+ WS Content here + {%plotly_app name="Connected"%} +
+
+ WS Content here + {%plotly_app slug="connected-2"%} +
+ + diff --git a/demo/demo/urls.py b/demo/demo/urls.py index d5aa60e9..2ee12daf 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -22,7 +22,8 @@ import demo.plotly_apps urlpatterns = [ - path('', TemplateView.as_view(template_name='index.html')), + path('', TemplateView.as_view(template_name='index.html'), name="home"), + path('second_page', TemplateView.as_view(template_name='second_page.html'), name="second"), path('admin/', admin.site.urls), path('django_plotly_dash/', include('django_plotly_dash.urls')), ] diff --git a/dev_requirements.txt b/dev_requirements.txt index 5b220038..2833ba69 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,9 +1,17 @@ alabaster==0.7.10 argh==0.26.2 +asgiref==2.3.2 +async-timeout==2.0.1 +attrs==18.1.0 +autobahn==18.6.1 +Automat==0.6.0 Babel==2.5.3 certifi==2018.4.16 +channels==2.1.2 chardet==3.0.4 click==6.7 +constantly==15.1.0 +daphne==2.2.0 dash==0.21.1 dash-core-components==0.22.1 dash-html-components==0.10.1 @@ -15,8 +23,10 @@ docutils==0.14 Flask==1.0.2 Flask-Compress==1.4.0 grip==4.5.2 +hyperlink==18.0.0 idna==2.6 imagesize==1.0.0 +incremental==17.5.0 ipython-genutils==0.2.0 itsdangerous==0.24 Jinja2==2.10 @@ -48,6 +58,9 @@ tornado==5.0.2 tqdm==4.23.3 traitlets==4.3.2 twine==1.11.0 +Twisted==18.4.0 +txaio==2.10.0 urllib3==1.22 watchdog==0.8.3 Werkzeug==0.14.1 +zope.interface==4.5.0 diff --git a/django_plotly_dash/__init__.py b/django_plotly_dash/__init__.py index ba2ec4d3..428dae82 100644 --- a/django_plotly_dash/__init__.py +++ b/django_plotly_dash/__init__.py @@ -1,6 +1,6 @@ # -__version__ = "0.3.0" +__version__ = "0.4.0" from .dash_wrapper import DjangoDash diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 0a0d132e..9ef9e766 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -155,7 +155,7 @@ def __init__(self, base_pathname=None, replacements = None, ndid=None, expanded_ 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 diff --git a/django_plotly_dash/migrations/0002_add_examples.py b/django_plotly_dash/migrations/0002_add_examples.py index 2a97dcc4..7aaff27d 100644 --- a/django_plotly_dash/migrations/0002_add_examples.py +++ b/django_plotly_dash/migrations/0002_add_examples.py @@ -19,9 +19,33 @@ def addExamples(apps, schema_editor): da1.save() + sa2 = StatelessApp(app_name="Connected", + slug="connected") + + sa2.save() + + da2 = DashApp(stateless_app=sa2, + instance_name="Connected-2", + slug="connected-2", + base_state='''{"dynamic": {"value": "Dynamo 123", + "uid": "need_to_generate_this", + "channel_name": "test_widget_channel", + "label": "rotational energy"}, + "dropdown-one": {"value": "Oxygen"}, + "also_dynamic": {"value": "Alternator 456", + "uid": "and_this_one", + "channel_name": "test_widget_channel", + "label": "momentum"}}''', + save_on_change=True) + + da2.save() + def remExamples(apps, schema_editor): + DashApp = apps.get_model("django_plotly_dash","DashApp") + StatelessApp = apps.get_model("django_plotly_dash","StatelessApp") + DashApp.objects.all().delete() StatelessApp.objects.all().delete() diff --git a/django_plotly_dash/templates/django_plotly_dash/plotly_messaging.html b/django_plotly_dash/templates/django_plotly_dash/plotly_messaging.html new file mode 100644 index 00000000..f6049982 --- /dev/null +++ b/django_plotly_dash/templates/django_plotly_dash/plotly_messaging.html @@ -0,0 +1,26 @@ +{%load staticfiles%} + + diff --git a/django_plotly_dash/templatetags/plotly_dash.py b/django_plotly_dash/templatetags/plotly_dash.py index 6b0773f8..8a3cd614 100644 --- a/django_plotly_dash/templatetags/plotly_dash.py +++ b/django_plotly_dash/templatetags/plotly_dash.py @@ -38,3 +38,7 @@ def plotly_app(context, name=None, slug=None, da=None, ratio=0.1, use_frameborde return locals() +@register.inclusion_tag("django_plotly_dash/plotly_messaging.html", takes_context=True) +def plotly_message_pipe(context, url=None): + url = url and url or '/ws/channel' + return locals() diff --git a/docs/extended_callbacks.rst b/docs/extended_callbacks.rst index cff71624..b6b986e7 100644 --- a/docs/extended_callbacks.rst +++ b/docs/extended_callbacks.rst @@ -54,5 +54,8 @@ in the :ref:`models_and_state` section. 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. +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 callback; 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/installation.rst b/docs/installation.rst index b745f6b0..1b34ccfe 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -7,7 +7,7 @@ Use pip to install the package, preferably to a local virtualenv.:: pip install django_plotly_dash -Then, add ``django_plotly_dash`` to ``INSTALLED_APPS`` in the Django settings.py file:: +Then, add ``django_plotly_dash`` to ``INSTALLED_APPS`` in the Django ``settings.py`` file:: INSTALLED_APPS = [ ... @@ -15,10 +15,27 @@ Then, add ``django_plotly_dash`` to ``INSTALLED_APPS`` in the Django settings.py ... ] +The project directory name ``django_plotly_dash`` can also be used on its own if preferred, but this will stop the use of readable application names in +the Django admin interface. + +The application's routes need to be registered within the routing structure by an appropriate ``include`` statement in +a ``urls.py`` file:: + + urlpatterns = [ + ... + path('django_plotly_dash/', include('django_plotly_dash.urls')), + ] + +The name within the URL is not important and can be changed. + +For the final installation step, a migration is needed to update the +database:: + + ./manage.py migrate + The ``plotly_item`` tag in the ``plotly_dash`` tag library can then be used to render any registered dash component. See :ref:`simple_use` for a simple example. -The project directory name ``django_plotly_dash`` can also be used on its own if preferred, but this will then skip the use of readable application names in -the Django admin interface. +It is important to ensure that any applications are registered using the ``DjangoDash`` class. This means that any python module containing the registration code has to be known to Django and loaded at the appropriate time. An easy way to ensure this is to import these modules into a standard Django file loaded at registration time. Source code and demo -------------------- @@ -40,3 +57,7 @@ To install and run it:: # at http://localhost:8000 This will launch a simple Django application. A superuser account is also configured, with both username and password set to ``admin``. + +Note that the current demo, along with the codebase, is in a prerelease and very raw form. + + diff --git a/docs/models_and_state.rst b/docs/models_and_state.rst index 7c5b782a..5062923b 100644 --- a/docs/models_and_state.rst +++ b/docs/models_and_state.rst @@ -31,7 +31,7 @@ be created if one does not already exist. ''' The main role of a ``StatelessApp`` instance is to manage access to the associated ``DjangoDash`` object, as -expsosed through the ``as_dash_app`` member +exposed through the ``as_dash_app`` member function. The ``DashApp`` model diff --git a/make_env b/make_env index 85b5112a..6e48fc41 100755 --- a/make_env +++ b/make_env @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -virtualenv -p python3 env +virtualenv -p python3.6 env source env/bin/activate pip install -r requirements.txt pip install -r dev_requirements.txt