11from __future__ import print_function
22
3+ import os
34import sys
45import collections
56import importlib
67import json
78import pkgutil
89import warnings
10+ import re
11+
912from functools import wraps
1013
1114import plotly
1922from .development .base_component import Component
2023from . import exceptions
2124from ._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 ,
0 commit comments