Skip to content

Commit 47bb68f

Browse files
delsimGibbsConsulting
authored andcommitted
Add staticfiles finders for locating asset and component static files (GibbsConsulting#99)
* v0.9.5 packages for future reference * Explicit list of default staticfiles finders * Added staticfiles finders for local components and assets * Component staticfiles finder now matches path * Document additional configuration for serving static files from components
1 parent 9b8c9f6 commit 47bb68f

File tree

7 files changed

+235
-27
lines changed

7 files changed

+235
-27
lines changed

demo/demo/settings.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -170,25 +170,23 @@
170170
},
171171
}
172172

173-
# In order to serve dash components locally - not recommended in general, but
174-
# can be useful for development especially if offline - we add in the root directory
175-
# of each module. This is a bit of fudge and only needed if serve_locally=True is
176-
# set on a DjangoDash instance.
177-
#
178-
# Note that this makes all of the python module (including .py and .pyc) files available
179-
# through the static route. This is not a big deal for development but at the same time
180-
# not particularly neat or tidy.
181-
182-
if DEBUG:
183-
184-
import importlib
185-
186-
for dash_module_name in ['dash_core_components',
187-
'dash_html_components',
188-
'dash_renderer',
189-
'dpd_components',
190-
'dash_bootstrap_components',
191-
]:
192-
193-
module = importlib.import_module(dash_module_name)
194-
STATICFILES_DIRS.append(("dash/%s"%dash_module_name, os.path.dirname(module.__file__)))
173+
# Staticfiles finders for locating dash app assets and related files
174+
175+
STATICFILES_FINDERS = [
176+
'django.contrib.staticfiles.finders.FileSystemFinder',
177+
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
178+
179+
'django_plotly_dash.finders.DashAssetFinder',
180+
'django_plotly_dash.finders.DashComponentFinder',
181+
]
182+
183+
# Plotly components containing static content that should
184+
# be handled by the Django staticfiles infrastructure
185+
186+
PLOTLY_COMPONENTS = [
187+
'dash_core_components',
188+
'dash_html_components',
189+
'dash_bootstrap_components',
190+
'dash_renderer',
191+
'dpd_components',
192+
]

demo/demo/templates/demo_seven.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ <h1>Dash Bootstrap Components</h1>
1212
<div class="card-body">
1313
<p><span>{</span>% load plotly_dash %}</p>
1414
<p>&lt;div class="<span>{</span>% plotly_class name="BootstrapApplication"%}">
15-
<p class="ml-3"><span>{</span>% plotly_app name="BootstrapApplication" %}</p>
15+
<p class="ml-3"><span>{</span>% plotly_app name="BootstrapApplication" ratio=0.2 %}</p>
1616
<p>&lt;\div>
1717
</div>
1818
</div>
1919
<p></p>
2020
<div class="card border-dark">
2121
<div class="card-body">
2222
<div class="{%plotly_class name="BootstrapApplication"%}">
23-
{%plotly_app name="BootstrapApplication"%}
23+
{%plotly_app name="BootstrapApplication" ratio=0.2 %}
2424
</div>
2525
</div>
2626
</div>

django_plotly_dash/dash_wrapper.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'''
2727

2828
import json
29+
import inspect
2930

3031
from dash import Dash
3132
from flask import Flask
@@ -50,6 +51,10 @@ def add_usable_app(name, app):
5051
usable_apps[name] = app
5152
return name
5253

54+
def all_apps():
55+
'Return a dictionary of all locally registered apps with the slug name as key'
56+
return usable_apps
57+
5358
def get_local_stateless_by_name(name):
5459
'''
5560
Locate a registered dash app by name, and return a DjangoDash instance encapsulating the app.
@@ -107,6 +112,11 @@ def __init__(self, name=None, serve_locally=False,
107112
bootstrap_source = css_url()['href']
108113
self.css.append_script({'external_url':[bootstrap_source,]})
109114

115+
# Remember some caller info for static files
116+
caller_frame = inspect.stack()[1]
117+
self.caller_module = inspect.getmodule(caller_frame[0])
118+
self.caller_module_location = inspect.getfile(self.caller_module)
119+
110120
def as_dash_instance(self, cache_id=None):
111121
'''
112122
Form a dash instance, for stateless use of this app

django_plotly_dash/finders.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'''
2+
Staticfiles finders for Dash assets
3+
4+
Copyright (c) 2018 Gibbs Consulting and others - see CONTRIBUTIONS.md
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
'''
24+
25+
import os
26+
import importlib
27+
28+
from collections import OrderedDict
29+
30+
from django.contrib.staticfiles.finders import BaseFinder
31+
from django.contrib.staticfiles.utils import get_files
32+
33+
from django.core.files.storage import FileSystemStorage
34+
35+
from django.conf import settings
36+
from django.apps import apps
37+
38+
from django_plotly_dash.dash_wrapper import all_apps
39+
40+
class DashComponentFinder(BaseFinder):
41+
'Find static files in components'
42+
43+
def __init__(self):
44+
45+
self.locations = []
46+
self.storages = OrderedDict()
47+
self.components = {}
48+
49+
self.ignore_patterns = ["*.py", "*.pyc",]
50+
51+
for component_name in settings.PLOTLY_COMPONENTS:
52+
53+
module = importlib.import_module(component_name)
54+
path_directory = os.path.dirname(module.__file__)
55+
56+
root = path_directory
57+
storage = FileSystemStorage(location=root)
58+
path = "dash/component/%s" % component_name
59+
60+
# Path_directory is where from
61+
# path is desired url mount point of the content of path_directory
62+
# component_name is the name of the component
63+
64+
storage.prefix = path
65+
66+
self.locations.append(component_name)
67+
68+
self.storages[component_name] = storage
69+
self.components[path] = component_name
70+
71+
super(DashComponentFinder, self).__init__()
72+
73+
def find(self, path, all=False):
74+
matches = []
75+
for component_name in self.locations:
76+
storage = self.storages[component_name]
77+
location = storage.location # dir on disc
78+
79+
component_path = "dash/component/%s" % component_name
80+
if len(path) > len(component_path) and path[:len(component_path)] == component_path:
81+
82+
matched_path = os.path.join(location, path[len(component_path)+1:])
83+
if os.path.exists(matched_path):
84+
if not all:
85+
return matched_path
86+
matches.append(matched_path)
87+
88+
return matches
89+
90+
def find_location(self, path):
91+
if os.path.exists(path):
92+
return path
93+
94+
def list(self, ignore_patterns):
95+
for component_name in self.locations:
96+
storage = self.storages[component_name]
97+
for path in get_files(storage, ignore_patterns + self.ignore_patterns):
98+
print("DashAssetFinder",path,storage)
99+
yield path, storage
100+
101+
class DashAssetFinder(BaseFinder):
102+
'Find static files in asset directories'
103+
104+
def __init__(self):
105+
106+
# Get all registered apps
107+
108+
import demo.plotly_apps
109+
import demo.dash_apps
110+
111+
self.apps = all_apps()
112+
113+
self.subdir = 'assets'
114+
115+
self.locations = []
116+
self.storages = OrderedDict()
117+
118+
self.ignore_patterns = ["*.py", "*.pyc",]
119+
120+
for app_slug, obj in self.apps.items():
121+
caller_module = obj.caller_module
122+
location = obj.caller_module_location
123+
path_directory = os.path.join(os.path.dirname(location),self.subdir)
124+
125+
if os.path.isdir(path_directory):
126+
127+
component_name = app_slug
128+
storage = FileSystemStorage(location=path_directory)
129+
path = "dash/assets/%s" % component_name
130+
storage.prefix = path
131+
132+
self.locations.append(component_name)
133+
self.storages[component_name] = storage
134+
135+
super(DashAssetFinder, self).__init__()
136+
137+
def find(self, path, all=False):
138+
return []
139+
140+
def list(self, ignore_patterns):
141+
for component_name in self.locations:
142+
storage = self.storages[component_name]
143+
for path in get_files(storage, ignore_patterns + self.ignore_patterns):
144+
yield path, storage

django_plotly_dash/views.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,25 @@ def component_suites(request, resource=None, component=None, extra_element="", *
111111
'Return part of a client-side component, served locally for some reason'
112112

113113
get_params = request.GET.urlencode()
114-
if get_params:
115-
redone_url = "/static/dash/%s/%s%s?%s" %(component, extra_element, resource, get_params)
114+
if get_params and False:
115+
redone_url = "/static/dash/component/%s/%s%s?%s" %(component, extra_element, resource, get_params)
116116
else:
117-
redone_url = "/static/dash/%s/%s%s" %(component, extra_element, resource)
117+
redone_url = "/static/dash/component/%s/%s%s" %(component, extra_element, resource)
118+
119+
print("Redirecting to :",redone_url)
118120

119121
return HttpResponseRedirect(redirect_to=redone_url)
120122

123+
def app_assets(request, **kwargs):
124+
'Return a local dash app asset, served up through the Django static framework'
125+
get_params = request.GET.urlencode()
126+
extra_part = ""
127+
if get_params:
128+
redone_url = "/static/dash/assets/%s?%s" %(extra_part, get_params)
129+
else:
130+
redone_url = "/static/dash/assets/%s" % extra_part
131+
132+
return HttpResponseRedirect(redirect_to=redone_url)
121133

122134
# pylint: disable=wrong-import-position, wrong-import-order
123135
from django.template.response import TemplateResponse

docs/configuration.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,45 @@ below.
3535
Defaults are inserted for missing values. It is also permissible to not have any ``PLOTLY_DASH`` entry in
3636
the Django settings file.
3737

38+
The Django staticfiles infrastructure is used to serve all local static files for
39+
the Dash apps. This requires adding a setting for the specification of additional static
40+
file finders
41+
42+
.. code-block:: python
43+
44+
# Staticfiles finders for locating dash app assets and related files
45+
46+
STATICFILES_FINDERS = [
47+
'django.contrib.staticfiles.finders.FileSystemFinder',
48+
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
49+
'django_plotly_dash.finders.DashAssetFinder',
50+
'django_plotly_dash.finders.DashComponentFinder',
51+
]
52+
53+
and also providing a list of components used
54+
55+
.. code-block:: python
56+
57+
# Plotly components containing static content that should
58+
# be handled by the Django staticfiles infrastructure
59+
60+
PLOTLY_COMPONENTS = [
61+
62+
# Common components
63+
'dash_core_components',
64+
'dash_html_components',
65+
'dash_renderer',
66+
67+
# django-plotly-dash components
68+
'dpd_components',
69+
70+
# Other components, as needed
71+
'dash_bootstrap_components',
72+
]
73+
74+
This list should be extended with any additional components that the applications
75+
use, where the components have files that have to be served locally.
76+
3877
.. _endpoints:
3978

4079
Endpoints

docs/installation.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,14 @@ well as adding ``channels`` to the ``INSTALLED_APPS`` list, a ``CHANNEL_LAYERS``
7979

8080
The host and port entries in ``hosts`` should be adjusted to match the network location of the Redis instance.
8181

82+
Further configuration
83+
---------------------
84+
8285
Further configuration options can be specified through the optional ``PLOTLY_DASH`` settings variable. The
8386
available options are detailed in the :ref:`configuration <configuration>` section.
8487

88+
This includes arranging for Dash assets to be served using the Django ``staticfiles`` functionality.
89+
8590
Source code and demo
8691
--------------------
8792

0 commit comments

Comments
 (0)