Skip to content

Commit 54ca4a3

Browse files
authored
Merge pull request #286 from plotly/assets-index-customizations
Assets files & index customizations
2 parents ca570d7 + 495762e commit 54ca4a3

17 files changed

+414
-33
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.22.0 - 2018-07-25
2+
## Added
3+
- Assets files & index customization [#286](https://github.com/plotly/dash/pull/286)
4+
- Raise an error if there is no layout present when the server is running [#294](https://github.com/plotly/dash/pull/294)
5+
16
## 0.21.1 - 2018-04-10
27
## Added
38
- `aria-*` and `data-*` attributes are now supported in all dash html components. (#40)

dash/_utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
def interpolate_str(template, **data):
2+
s = template
3+
for k, v in data.items():
4+
key = '{%' + k + '%}'
5+
s = s.replace(key, v)
6+
return s
7+
8+
19
class AttributeDict(dict):
210
"""
311
Dictionary subclass enabling attribute lookup/assignment of keys/values.

dash/dash.py

+215-29
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import print_function
22

3+
import os
34
import sys
45
import collections
56
import importlib
67
import json
78
import pkgutil
89
import warnings
10+
import re
11+
912
from functools import wraps
1013

1114
import plotly
@@ -19,6 +22,42 @@
1922
from .development.base_component import Component
2023
from . import exceptions
2124
from ._utils import AttributeDict as _AttributeDict
25+
from ._utils import interpolate_str as _interpolate
26+
27+
_default_index = '''
28+
<!DOCTYPE html>
29+
<html>
30+
<head>
31+
{%metas%}
32+
<title>{%title%}</title>
33+
{%favicon%}
34+
{%css%}
35+
</head>
36+
<body>
37+
{%app_entry%}
38+
<footer>
39+
{%config%}
40+
{%scripts%}
41+
</footer>
42+
</body>
43+
</html>
44+
'''
45+
46+
_app_entry = '''
47+
<div id="react-entry-point">
48+
<div class="_dash-loading">
49+
Loading...
50+
</div>
51+
</div>
52+
'''
53+
54+
_re_index_entry = re.compile(r'{%app_entry%}')
55+
_re_index_config = re.compile(r'{%config%}')
56+
_re_index_scripts = re.compile(r'{%scripts%}')
57+
58+
_re_index_entry_id = re.compile(r'id="react-entry-point"')
59+
_re_index_config_id = re.compile(r'id="_dash-config"')
60+
_re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"')
2261

2362

2463
# pylint: disable=too-many-instance-attributes
@@ -29,8 +68,13 @@ def __init__(
2968
name='__main__',
3069
server=None,
3170
static_folder='static',
71+
assets_folder=None,
72+
assets_url_path='/assets',
73+
include_assets_files=True,
3274
url_base_pathname='/',
3375
compress=True,
76+
meta_tags=None,
77+
index_string=_default_index,
3478
**kwargs):
3579

3680
# pylint-disable: too-many-instance-attributes
@@ -42,20 +86,35 @@ def __init__(
4286
See https://github.com/plotly/dash/issues/141 for details.
4387
''', DeprecationWarning)
4488

45-
name = name or 'dash'
89+
self._assets_folder = assets_folder or os.path.join(
90+
flask.helpers.get_root_path(name), 'assets'
91+
)
92+
4693
# allow users to supply their own flask server
4794
self.server = server or Flask(name, static_folder=static_folder)
4895

96+
self.server.register_blueprint(
97+
flask.Blueprint('assets', 'assets',
98+
static_folder=self._assets_folder,
99+
static_url_path=assets_url_path))
100+
49101
self.url_base_pathname = url_base_pathname
50102
self.config = _AttributeDict({
51103
'suppress_callback_exceptions': False,
52104
'routes_pathname_prefix': url_base_pathname,
53-
'requests_pathname_prefix': url_base_pathname
105+
'requests_pathname_prefix': url_base_pathname,
106+
'include_assets_files': include_assets_files,
107+
'assets_external_path': '',
54108
})
55109

56110
# list of dependencies
57111
self.callback_map = {}
58112

113+
self._index_string = ''
114+
self.index_string = index_string
115+
self._meta_tags = meta_tags or []
116+
self._favicon = None
117+
59118
if compress:
60119
# gzip
61120
Compress(self.server)
@@ -149,12 +208,26 @@ def layout(self, value):
149208
# pylint: disable=protected-access
150209
self.css._update_layout(layout_value)
151210
self.scripts._update_layout(layout_value)
152-
self._collect_and_register_resources(
153-
self.scripts.get_all_scripts()
154-
)
155-
self._collect_and_register_resources(
156-
self.css.get_all_css()
211+
212+
@property
213+
def index_string(self):
214+
return self._index_string
215+
216+
@index_string.setter
217+
def index_string(self, value):
218+
checks = (
219+
(_re_index_entry.search(value), 'app_entry'),
220+
(_re_index_config.search(value), 'config',),
221+
(_re_index_scripts.search(value), 'scripts'),
157222
)
223+
missing = [missing for check, missing in checks if not check]
224+
if missing:
225+
raise Exception(
226+
'Did you forget to include {} in your index string ?'.format(
227+
', '.join('{%' + x + '%}' for x in missing)
228+
)
229+
)
230+
self._index_string = value
158231

159232
def serve_layout(self):
160233
layout = self._layout_value()
@@ -180,6 +253,7 @@ def serve_routes(self):
180253
)
181254

182255
def _collect_and_register_resources(self, resources):
256+
# now needs the app context.
183257
# template in the necessary component suite JS bundles
184258
# add the version number of the package as a query parameter
185259
# for cache busting
@@ -217,8 +291,12 @@ def _relative_url_path(relative_package_path='', namespace=''):
217291
srcs.append(url)
218292
elif 'absolute_path' in resource:
219293
raise Exception(
220-
'Serving files form absolute_path isn\'t supported yet'
294+
'Serving files from absolute_path isn\'t supported yet'
221295
)
296+
elif 'asset_path' in resource:
297+
static_url = flask.url_for('assets.static',
298+
filename=resource['asset_path'])
299+
srcs.append(static_url)
222300
return srcs
223301

224302
def _generate_css_dist_html(self):
@@ -260,6 +338,20 @@ def _generate_config_html(self):
260338
'</script>'
261339
).format(json.dumps(self._config()))
262340

341+
def _generate_meta_html(self):
342+
has_charset = any('charset' in x for x in self._meta_tags)
343+
344+
tags = []
345+
if not has_charset:
346+
tags.append('<meta charset="UTF-8"/>')
347+
for meta in self._meta_tags:
348+
attributes = []
349+
for k, v in meta.items():
350+
attributes.append('{}="{}"'.format(k, v))
351+
tags.append('<meta {} />'.format(' '.join(attributes)))
352+
353+
return '\n '.join(tags)
354+
263355
# Serve the JS bundles for each package
264356
def serve_component_suites(self, package_name, path_in_package_dist):
265357
if package_name not in self.registered_paths:
@@ -294,28 +386,83 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
294386
scripts = self._generate_scripts_html()
295387
css = self._generate_css_dist_html()
296388
config = self._generate_config_html()
389+
metas = self._generate_meta_html()
297390
title = getattr(self, 'title', 'Dash')
298-
return '''
299-
<!DOCTYPE html>
300-
<html>
301-
<head>
302-
<meta charset="UTF-8">
303-
<title>{}</title>
304-
{}
305-
</head>
306-
<body>
307-
<div id="react-entry-point">
308-
<div class="_dash-loading">
309-
Loading...
310-
</div>
311-
</div>
312-
<footer>
313-
{}
314-
{}
315-
</footer>
316-
</body>
317-
</html>
318-
'''.format(title, css, config, scripts)
391+
if self._favicon:
392+
favicon = '<link rel="icon" type="image/x-icon" href="{}">'.format(
393+
flask.url_for('assets.static', filename=self._favicon))
394+
else:
395+
favicon = ''
396+
397+
index = self.interpolate_index(
398+
metas=metas, title=title, css=css, config=config,
399+
scripts=scripts, app_entry=_app_entry, favicon=favicon)
400+
401+
checks = (
402+
(_re_index_entry_id.search(index), '#react-entry-point'),
403+
(_re_index_config_id.search(index), '#_dash-configs'),
404+
(_re_index_scripts_id.search(index), 'dash-renderer'),
405+
)
406+
missing = [missing for check, missing in checks if not check]
407+
408+
if missing:
409+
plural = 's' if len(missing) > 1 else ''
410+
raise Exception(
411+
'Missing element{pl} {ids} in index.'.format(
412+
ids=', '.join(missing),
413+
pl=plural
414+
)
415+
)
416+
417+
return index
418+
419+
def interpolate_index(self,
420+
metas='', title='', css='', config='',
421+
scripts='', app_entry='', favicon=''):
422+
"""
423+
Called to create the initial HTML string that is loaded on page.
424+
Override this method to provide you own custom HTML.
425+
426+
:Example:
427+
428+
class MyDash(dash.Dash):
429+
def interpolate_index(self, **kwargs):
430+
return '''
431+
<!DOCTYPE html>
432+
<html>
433+
<head>
434+
<title>My App</title>
435+
</head>
436+
<body>
437+
<div id="custom-header">My custom header</div>
438+
{app_entry}
439+
{config}
440+
{scripts}
441+
<div id="custom-footer">My custom footer</div>
442+
</body>
443+
</html>
444+
'''.format(
445+
app_entry=kwargs.get('app_entry'),
446+
config=kwargs.get('config'),
447+
scripts=kwargs.get('scripts'))
448+
449+
:param metas: Collected & formatted meta tags.
450+
:param title: The title of the app.
451+
:param css: Collected & formatted css dependencies as <link> tags.
452+
:param config: Configs needed by dash-renderer.
453+
:param scripts: Collected & formatted scripts tags.
454+
:param app_entry: Where the app will render.
455+
:param favicon: A favicon <link> tag if found in assets folder.
456+
:return: The interpolated HTML string for the index.
457+
"""
458+
return _interpolate(self.index_string,
459+
metas=metas,
460+
title=title,
461+
css=css,
462+
config=config,
463+
scripts=scripts,
464+
favicon=favicon,
465+
app_entry=app_entry)
319466

320467
def dependencies(self):
321468
return flask.jsonify([
@@ -558,6 +705,9 @@ def dispatch(self):
558705
return self.callback_map[target_id]['callback'](*args)
559706

560707
def _setup_server(self):
708+
if self.config.include_assets_files:
709+
self._walk_assets_directory()
710+
561711
# Make sure `layout` is set before running the server
562712
value = getattr(self, 'layout')
563713
if value is None:
@@ -567,9 +717,45 @@ def _setup_server(self):
567717
'at the time that `run_server` was called. '
568718
'Make sure to set the `layout` attribute of your application '
569719
'before running the server.')
720+
570721
self._generate_scripts_html()
571722
self._generate_css_dist_html()
572723

724+
def _walk_assets_directory(self):
725+
walk_dir = self._assets_folder
726+
slash_splitter = re.compile(r'[\\/]+')
727+
728+
def add_resource(p):
729+
res = {'asset_path': p}
730+
if self.config.assets_external_path:
731+
res['external_url'] = '{}{}'.format(
732+
self.config.assets_external_path, path)
733+
return res
734+
735+
for current, _, files in os.walk(walk_dir):
736+
if current == walk_dir:
737+
base = ''
738+
else:
739+
s = current.replace(walk_dir, '').lstrip('\\').lstrip('/')
740+
splitted = slash_splitter.split(s)
741+
if len(splitted) > 1:
742+
base = '/'.join(slash_splitter.split(s))
743+
else:
744+
base = splitted[0]
745+
746+
for f in sorted(files):
747+
if base:
748+
path = '/'.join([base, f])
749+
else:
750+
path = f
751+
752+
if f.endswith('js'):
753+
self.scripts.append_script(add_resource(path))
754+
elif f.endswith('css'):
755+
self.css.append_css(add_resource(path))
756+
elif f == 'favicon.ico':
757+
self._favicon = path
758+
573759
def run_server(self,
574760
port=8050,
575761
debug=False,

dash/resources.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ def _filter_resources(self, all_resources):
2121
filtered_resource = {}
2222
if 'namespace' in s:
2323
filtered_resource['namespace'] = s['namespace']
24-
2524
if 'external_url' in s and not self.config.serve_locally:
2625
filtered_resource['external_url'] = s['external_url']
2726
elif 'relative_package_path' in s:
@@ -30,6 +29,8 @@ def _filter_resources(self, all_resources):
3029
)
3130
elif 'absolute_path' in s:
3231
filtered_resource['absolute_path'] = s['absolute_path']
32+
elif 'asset_path' in s:
33+
filtered_resource['asset_path'] = s['asset_path']
3334
elif self.config.serve_locally:
3435
warnings.warn(
3536
'A local version of {} is not available'.format(
@@ -112,8 +113,7 @@ class config:
112113
serve_locally = False
113114

114115

115-
class Scripts:
116-
# pylint: disable=old-style-class
116+
class Scripts: # pylint: disable=old-style-class
117117
def __init__(self, layout=None):
118118
self._resources = Resources('_js_dist', layout)
119119
self._resources.config = self.config

dash/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.21.1'
1+
__version__ = '0.22.0'

tests/assets/load_first.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
window.tested = ['load_first'];

0 commit comments

Comments
 (0)