Skip to content

Commit c50decc

Browse files
delsimGibbsConsulting
authored andcommitted
Serve locally (#133)
* Local serving of assets * Demo nine to show and test use of local assets. * Move to serving dash components assets through staticfiles, and steps towards serving other files locally through url rewriting * Constrain dash component versions * Add static support to dev environment for demo * Increased test coverage
1 parent 9013698 commit c50decc

23 files changed

+412
-29
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ coverage.xml
5454
*.log
5555
local_settings.py
5656
*/db.sqlite3
57-
demo/static/*
57+
demo/staticfiles/*
5858

5959
# Flask stuff:
6060
instance/

demo/demo/assets/image_one.png

989 Bytes
Loading

demo/demo/plotly_apps.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def callback_size(dropdown_color, dropdown_size):
8484

8585
a2 = DjangoDash("Ex2",
8686
serve_locally=True)
87+
8788
a2.layout = html.Div([
8889
dcc.RadioItems(id="dropdown-one",
8990
options=[{'label':i, 'value':j} for i, j in [("O2", "Oxygen"),
@@ -186,8 +187,7 @@ def callback_liveIn_button_press(red_clicks, blue_clicks, green_clicks,
186187
change_col,
187188
datetime.fromtimestamp(0.001*timestamp))
188189

189-
liveOut = DjangoDash("LiveOutput",
190-
)#serve_locally=True)
190+
liveOut = DjangoDash("LiveOutput")
191191

192192
def _get_cache_key(state_uid):
193193
return "demo-liveout-s6-%s" % state_uid
@@ -309,3 +309,10 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs):
309309
return {'data':traces,
310310
#'layout': go.Layout
311311
}
312+
313+
localState = DjangoDash("LocalState",
314+
serve_locally=True)
315+
316+
localState.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')),
317+
html.Img(src='assets/image_two.png'),
318+
])

demo/demo/settings.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,22 @@
4242
'bootstrap4',
4343

4444
'django_plotly_dash.apps.DjangoPlotlyDashConfig',
45+
'dpd_static_support',
4546
]
4647

4748
MIDDLEWARE = [
4849
'django.middleware.security.SecurityMiddleware',
50+
51+
'whitenoise.middleware.WhiteNoiseMiddleware',
52+
4953
'django.contrib.sessions.middleware.SessionMiddleware',
5054
'django.middleware.common.CommonMiddleware',
5155
'django.middleware.csrf.CsrfViewMiddleware',
5256
'django.contrib.auth.middleware.AuthenticationMiddleware',
5357
'django.contrib.messages.middleware.MessageMiddleware',
5458

5559
'django_plotly_dash.middleware.BaseMiddleware',
60+
'django_plotly_dash.middleware.ExternalRedirectionMiddleware',
5661

5762
'django.middleware.clickjacking.XFrameOptionsMiddleware',
5863
]
@@ -134,13 +139,15 @@
134139
"view_decorator" : None, # Specify a function to be used to wrap each of the dpd view functions
135140

136141
"cache_arguments" : True, # True for cache, False for session-based argument propagation
142+
143+
#"serve_locally" : True, # True to serve assets locally, False to use their unadulterated urls (eg a CDN)
137144
}
138145

139146
# Static files (CSS, JavaScript, Images)
140147
# https://docs.djangoproject.com/en/2.0/howto/static-files/
141148

142149
STATIC_URL = '/static/'
143-
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
150+
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
144151

145152
STATICFILES_DIRS = [
146153
os.path.join(BASE_DIR, 'demo', 'static'),
@@ -178,6 +185,7 @@
178185

179186
'django_plotly_dash.finders.DashAssetFinder',
180187
'django_plotly_dash.finders.DashComponentFinder',
188+
'django_plotly_dash.finders.DashAppDirectoryFinder',
181189
]
182190

183191
# Plotly components containing static content that should
@@ -189,4 +197,5 @@
189197
'dash_bootstrap_components',
190198
'dash_renderer',
191199
'dpd_components',
200+
'dpd_static_support',
192201
]

demo/demo/templates/demo_nine.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{%extends "base.html"%}
2+
{%load plotly_dash%}
3+
4+
{%block title%}Demo Nine - Serving local assets{%endblock%}
5+
6+
{%block content%}
7+
<h1>Serving Local Assets</h1>
8+
<p>
9+
This example demonstrates serving local assets as part of a Dash app.
10+
</p>
11+
<p>
12+
The extra files are specified using the standard plotly dash approach, and are
13+
made available through the standard Django staticfiles infrastructure. This means that
14+
they will be served the same way as other static files through a reverse proxy.
15+
</p>
16+
<p></p>
17+
<div class="card bg-light border-dark">
18+
<div class="card-body">
19+
<p><span>{</span>% load plotly_dash %}</p>
20+
<p>&lt;div class="<span>{</span>% plotly_class name="LocalState"%}">
21+
<p class="ml-3"><span>{</span>% plotly_app name="LocalState" ratio=0.3 %}</p>
22+
<p>&lt;\div>
23+
</div>
24+
</div>
25+
<p></p>
26+
<div class="card border-dark">
27+
<div class="card-body">
28+
<div class="{%plotly_class name="LocalState"%}">
29+
{%plotly_app name="LocalState" ratio=0.3 %}
30+
</div>
31+
</div>
32+
</div>
33+
{%endblock%}

demo/demo/templates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ <h1>Demonstration Application</h1>
1515
<li><a class="btn btn-primary btnspace" href="{%url "demo-six"%}">Demo Six</a> - simple html injection example</li>
1616
<li><a class="btn btn-primary btnspace" href="{%url "demo-seven"%}">Demo Seven</a> - dash-bootstrap-components example</li>
1717
<li><a class="btn btn-primary btnspace" href="{%url "demo-eight"%}">Demo Eight</a> - Django session state example</li>
18+
<li><a class="btn btn-primary btnspace" href="{%url "demo-nine"%}">Demo Nine</a> - local serving of assets</li>
1819
</ul>
1920
{%endblock%}

demo/demo/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
url('^demo-six', dash_example_1_view, name="demo-six"),
4545
url('^demo-seven', TemplateView.as_view(template_name='demo_seven.html'), name="demo-seven"),
4646
url('^demo-eight', session_state_view, {'template_name':'demo_eight.html'}, name="demo-eight"),
47+
url('^demo-nine', TemplateView.as_view(template_name='demo_nine.html'), name="demo-nine"),
4748
url('^admin/', admin.site.urls),
4849
url('^django_plotly_dash/', include('django_plotly_dash.urls')),
4950

dev_requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
channels>=2.0
22
channels-redis
33
daphne
4-
Django>=2.0
4+
Django>=2.0,<2.2
55
django-bootstrap4
66
django-redis
7+
dpd-static-support
78
grip
89
pandas
910
pylint
@@ -16,4 +17,5 @@ redis
1617
sphinx
1718
sphinx-autobuild
1819
twine
20+
whitenoise
1921

django_plotly_dash/assets/some_asset

Whitespace-only changes.

django_plotly_dash/dash_wrapper.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
from .app_name import app_name, main_view_label
4040
from .middleware import EmbeddedHolder
4141

42+
from .util import static_asset_path
43+
from .util import serve_locally as serve_locally_setting
4244

4345
uid_counter = 0
4446

@@ -84,11 +86,12 @@ class DjangoDash:
8486
To use, construct an instance of DjangoDash() in place of a Dash() one.
8587
'''
8688
#pylint: disable=too-many-instance-attributes
87-
def __init__(self, name=None, serve_locally=False,
89+
def __init__(self, name=None, serve_locally=None,
8890
expanded_callbacks=False,
8991
add_bootstrap_links=False,
9092
suppress_callback_exceptions=False,
9193
**kwargs): # pylint: disable=unused-argument, too-many-arguments
94+
9295
if name is None:
9396
global uid_counter # pylint: disable=global-statement
9497
uid_counter += 1
@@ -104,19 +107,40 @@ def __init__(self, name=None, serve_locally=False,
104107
add_usable_app(self._uid,
105108
self)
106109

110+
if serve_locally is None:
111+
self._serve_locally = serve_locally_setting()
112+
else:
113+
self._serve_locally = serve_locally
114+
107115
self._expanded_callbacks = expanded_callbacks
108-
self._serve_locally = serve_locally
109116
self._suppress_callback_exceptions = suppress_callback_exceptions
110117

111118
if add_bootstrap_links:
112119
from bootstrap4.bootstrap import css_url
113120
bootstrap_source = css_url()['href']
114-
self.css.append_script({'external_url':[bootstrap_source,]})
121+
122+
if self._serve_locally:
123+
# Ensure package is loaded; if not present then pip install dpd-static-support
124+
import dpd_static_support
125+
hard_coded_package_name = "dpd_static_support"
126+
base_file_name = bootstrap_source.split('/')[-1]
127+
128+
self.css.append_script({'external_url': [bootstrap_source,],
129+
'relative_package_path' : base_file_name,
130+
'namespace': hard_coded_package_name,
131+
})
132+
else:
133+
self.css.append_script({'external_url':[bootstrap_source,],})
115134

116135
# Remember some caller info for static files
117136
caller_frame = inspect.stack()[1]
118137
self.caller_module = inspect.getmodule(caller_frame[0])
119138
self.caller_module_location = inspect.getfile(self.caller_module)
139+
self.assets_folder = "assets"
140+
141+
def get_asset_static_url(self, asset_path):
142+
module_name = self.caller_module.__name__
143+
return static_asset_path(module_name, asset_path)
120144

121145
def as_dash_instance(self, cache_id=None):
122146
'''
@@ -205,6 +229,16 @@ def expanded_callback(self, output, inputs=[], state=[], events=[]): # pylint: d
205229
self._expanded_callbacks = True
206230
return self.callback(output, inputs, state, events)
207231

232+
def get_asset_url(self, asset_name):
233+
'''URL of an asset associated with this component
234+
235+
Use a placeholder and insert later
236+
'''
237+
238+
return "assets/" + str(asset_name)
239+
240+
#return self.as_dash_instance().get_asset_url(asset_name)
241+
208242
class PseudoFlask:
209243
'Dummy implementation of a Flask instance, providing stub functionality'
210244
def __init__(self):
@@ -234,7 +268,8 @@ class WrappedDash(Dash):
234268
# pylint: disable=too-many-arguments, too-many-instance-attributes
235269
def __init__(self,
236270
base_pathname=None, replacements=None, ndid=None,
237-
expanded_callbacks=False, serve_locally=False, **kwargs):
271+
expanded_callbacks=False, serve_locally=False,
272+
**kwargs):
238273

239274
self._uid = ndid
240275

@@ -245,7 +280,8 @@ def __init__(self,
245280
kwargs['url_base_pathname'] = self._base_pathname
246281
kwargs['server'] = self._notflask
247282

248-
super(WrappedDash, self).__init__(**kwargs)
283+
super(WrappedDash, self).__init__(__name__,
284+
**kwargs)
249285

250286
self.css.config.serve_locally = serve_locally
251287
self.scripts.config.serve_locally = serve_locally
@@ -507,6 +543,8 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
507543

508544
def interpolate_index(self, **kwargs): #pylint: disable=arguments-differ
509545

546+
print("IN INTERPOLATE INDEX")
547+
510548
if not self._return_embedded:
511549
resp = super(WrappedDash, self).interpolate_index(**kwargs)
512550
return resp

django_plotly_dash/finders.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
from django.core.files.storage import FileSystemStorage
3434

3535
from django.conf import settings
36-
from django.apps import apps #pylint: disable=unused-import
36+
from django.apps import apps
3737

3838
from django_plotly_dash.dash_wrapper import all_apps
39+
from django_plotly_dash.util import full_asset_path
3940

4041
class DashComponentFinder(BaseFinder):
4142
'Find static files in components'
@@ -104,37 +105,80 @@ def list(self, ignore_patterns):
104105
for component_name in self.locations:
105106
storage = self.storages[component_name]
106107
for path in get_files(storage, ignore_patterns + self.ignore_patterns):
107-
print("DashAssetFinder", path, storage)
108108
yield path, storage
109109

110+
class DashAppDirectoryFinder(BaseFinder):
111+
'Find static fies in application subdirectories'
112+
113+
def __init__(self):
114+
# get all registered apps
115+
116+
self.locations = []
117+
self.storages = OrderedDict()
118+
119+
self.ignore_patterns = ["*.py", "*.pyc",]
120+
121+
for app_config in apps.get_app_configs():
122+
123+
path_directory = os.path.join(app_config.path, 'assets')
124+
125+
if os.path.isdir(path_directory):
126+
127+
storage = FileSystemStorage(location=path_directory)
128+
129+
storage.prefix = full_asset_path(app_config.name, "")
130+
131+
self.locations.append(app_config.name)
132+
self.storages[app_config.name] = storage
133+
134+
super(DashAppDirectoryFinder, self).__init__()
135+
136+
#pylint: disable=redefined-builtin
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
145+
146+
110147
class DashAssetFinder(BaseFinder):
111148
'Find static files in asset directories'
112149

113150
#pylint: disable=unused-import, unused-variable, no-name-in-module, import-error, abstract-method
114151

115152
def __init__(self):
116153

117-
# Get all registered apps
154+
# Ensure urls are loaded
155+
root_urls = settings.ROOT_URLCONF
156+
importlib.import_module(root_urls)
118157

119-
self.apps = all_apps()
158+
# Get all registered django dash apps
120159

121-
self.subdir = 'assets'
160+
self.apps = all_apps()
122161

123162
self.locations = []
124163
self.storages = OrderedDict()
125164

126165
self.ignore_patterns = ["*.py", "*.pyc",]
127166

167+
added_locations = {}
168+
128169
for app_slug, obj in self.apps.items():
170+
129171
caller_module = obj.caller_module
130172
location = obj.caller_module_location
131-
path_directory = os.path.join(os.path.dirname(location), self.subdir)
173+
subdir = obj.assets_folder
174+
175+
path_directory = os.path.join(os.path.dirname(location), subdir)
132176

133177
if os.path.isdir(path_directory):
134178

135179
component_name = app_slug
136180
storage = FileSystemStorage(location=path_directory)
137-
path = "dash/assets/%s" % component_name
181+
path = full_asset_path(obj.caller_module.__name__,"")
138182
storage.prefix = path
139183

140184
self.locations.append(component_name)
@@ -151,3 +195,4 @@ def list(self, ignore_patterns):
151195
storage = self.storages[component_name]
152196
for path in get_files(storage, ignore_patterns + self.ignore_patterns):
153197
yield path, storage
198+

0 commit comments

Comments
 (0)