From 53ec2eff77b54c5dfe2f00a76cce5e1bffbc0e9f Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 4 Jul 2018 14:57:36 -0400 Subject: [PATCH 01/27] Add support for static_path resources. --- dash/resources.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dash/resources.py b/dash/resources.py index c08c0bad3f..e6cb2dc5ab 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -30,6 +30,8 @@ def _filter_resources(self, all_resources): ) elif 'absolute_path' in s: filtered_resource['absolute_path'] = s['absolute_path'] + elif 'static_path' in s: + filtered_resource = s elif self.config.serve_locally: warnings.warn( 'A local version of {} is not available'.format( From a14709a4bb24432adcebe65d0b4175d5a36e9051 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 4 Jul 2018 15:29:13 -0400 Subject: [PATCH 02/27] Add static directory walk for files to include. --- dash/dash.py | 68 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 143d1e802b..fc470ebb33 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1,5 +1,6 @@ from __future__ import print_function +import os import sys import collections import importlib @@ -28,7 +29,9 @@ def __init__( self, name='__main__', server=None, - static_folder='static', + static_folder=None, + static_url_path='/static', + include_static_files=True, url_base_pathname='/', compress=True, **kwargs): @@ -43,14 +46,36 @@ def __init__( ''', DeprecationWarning) name = name or 'dash' + + lib_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..')) + template_folder = os.path.join(lib_dir, 'templates') + lib_static = os.path.join(lib_dir, 'static') + + # Set the static folder (same as flask does, but we can use it) + static_folder = static_folder or os.path.join( + os.getcwd(), 'static' + ) + # allow users to supply their own flask server - self.server = server or Flask(name, static_folder=static_folder) + self.server = server or Flask(name, + static_folder=static_folder, + static_url_path=static_url_path) + + _dash_files = flask.Blueprint('_dash_files', + '_dash_files', + url_prefix='/dash-files', + static_folder=lib_static, + template_folder=template_folder) + + self.server.register_blueprint(_dash_files) self.url_base_pathname = url_base_pathname self.config = _AttributeDict({ 'suppress_callback_exceptions': False, 'routes_pathname_prefix': url_base_pathname, - 'requests_pathname_prefix': url_base_pathname + 'requests_pathname_prefix': url_base_pathname, + 'include_static_files': True, }) # list of dependencies @@ -149,12 +174,6 @@ def layout(self, value): # pylint: disable=protected-access self.css._update_layout(layout_value) self.scripts._update_layout(layout_value) - self._collect_and_register_resources( - self.scripts.get_all_scripts() - ) - self._collect_and_register_resources( - self.css.get_all_css() - ) def serve_layout(self): layout = self._layout_value() @@ -180,6 +199,7 @@ def serve_routes(self): ) def _collect_and_register_resources(self, resources): + # now needs the app context. # template in the necessary component suite JS bundles # add the version number of the package as a query parameter # for cache busting @@ -217,8 +237,12 @@ def _relative_url_path(relative_package_path='', namespace=''): srcs.append(url) elif 'absolute_path' in resource: raise Exception( - 'Serving files form absolute_path isn\'t supported yet' + 'Serving files from absolute_path isn\'t supported yet' ) + elif 'static_path' in resource: + static_url = flask.url_for('static', + filename=resource['static_path']) + srcs.append(static_url) return srcs def _generate_css_dist_html(self): @@ -558,9 +582,33 @@ def dispatch(self): return self.callback_map[target_id]['callback'](*args) def _setup_server(self): + if self.config.include_static_files: + self._walk_static_directory() + self._generate_scripts_html() self._generate_css_dist_html() + def _walk_static_directory(self): + walk_dir = self.config.static_folder + subs = [] + for current, _, files in os.walk(walk_dir): + if current != walk_dir: + subs.append(os.path.basename(current)) + + for f in files: + + # path.join is bad here cause we call url_for + path = f if not subs else '/'.join(subs + [f]) + + if f.endswith('js'): + self.scripts.append_script({ + 'static_path': path + }) + elif f.endswith('css'): + self.css.append_css({ + 'static_path': path + }) + def run_server(self, port=8050, debug=False, From 69b8adc9718529372805e287767e93ba7545adca Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 4 Jul 2018 15:52:53 -0400 Subject: [PATCH 03/27] pylint fixes. --- dash/dash.py | 2 +- dash/resources.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index fc470ebb33..43716a98e3 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -23,7 +23,7 @@ # pylint: disable=too-many-instance-attributes -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, too-many-locals class Dash(object): def __init__( self, diff --git a/dash/resources.py b/dash/resources.py index e6cb2dc5ab..b4f61bc075 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -21,7 +21,6 @@ def _filter_resources(self, all_resources): filtered_resource = {} if 'namespace' in s: filtered_resource['namespace'] = s['namespace'] - if 'external_url' in s and not self.config.serve_locally: filtered_resource['external_url'] = s['external_url'] elif 'relative_package_path' in s: @@ -114,8 +113,7 @@ class config: serve_locally = False -class Scripts: - # pylint: disable=old-style-class +class Scripts: # pylint: disable=old-style-class def __init__(self, layout=None): self._resources = Resources('_js_dist', layout) self._resources.config = self.config From 7cafb47b0f5e38128530a96463d4719041cf4622 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 4 Jul 2018 16:16:06 -0400 Subject: [PATCH 04/27] Add support for meta tags. --- dash/dash.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 43716a98e3..96f6435cf5 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -75,12 +75,14 @@ def __init__( 'suppress_callback_exceptions': False, 'routes_pathname_prefix': url_base_pathname, 'requests_pathname_prefix': url_base_pathname, - 'include_static_files': True, + 'include_static_files': include_static_files, }) # list of dependencies self.callback_map = {} + self._meta_tags = collections.OrderedDict() + if compress: # gzip Compress(self.server) @@ -318,6 +320,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument scripts = self._generate_scripts_html() css = self._generate_css_dist_html() config = self._generate_config_html() + metas = self._meta_tags.values() title = getattr(self, 'title', 'Dash') return ''' @@ -609,6 +612,9 @@ def _walk_static_directory(self): 'static_path': path }) + def add_meta_tag(self, name, content): + self._meta_tags[name] = {'name': name, 'content': content} + def run_server(self, port=8050, debug=False, From 7ebab390b2ada25209b35757d4fbd936ec927c84 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 4 Jul 2018 17:48:43 -0400 Subject: [PATCH 05/27] Add support favicon located in static dir. --- dash/dash.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dash/dash.py b/dash/dash.py index 96f6435cf5..21eb119feb 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -76,12 +76,14 @@ def __init__( 'routes_pathname_prefix': url_base_pathname, 'requests_pathname_prefix': url_base_pathname, 'include_static_files': include_static_files, + 'static_folder': static_folder }) # list of dependencies self.callback_map = {} self._meta_tags = collections.OrderedDict() + self._favicon = None if compress: # gzip @@ -611,6 +613,8 @@ def _walk_static_directory(self): self.css.append_css({ 'static_path': path }) + elif f == 'favicon.ico': + self._favicon = path def add_meta_tag(self, name, content): self._meta_tags[name] = {'name': name, 'content': content} From fc265442e067057cd84202032e5d0bd5f586703e Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 5 Jul 2018 17:49:40 -0400 Subject: [PATCH 06/27] Fix static walking nested directories. --- dash/dash.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 21eb119feb..c4712c8be5 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -7,6 +7,9 @@ import json import pkgutil import warnings +import shutil +import re + from functools import wraps import plotly @@ -595,15 +598,24 @@ def _setup_server(self): def _walk_static_directory(self): walk_dir = self.config.static_folder - subs = [] + slash_splitter = re.compile(r'[\\/]+') + for current, _, files in os.walk(walk_dir): - if current != walk_dir: - subs.append(os.path.basename(current)) + if current == walk_dir: + base = '' + else: + s = current.replace(walk_dir, '').lstrip('\\').lstrip('/') + splitted = slash_splitter.split(s) + if len(splitted) > 1: + base = '/'.join(slash_splitter.split(s)) + else: + base = splitted[0] for f in files: - - # path.join is bad here cause we call url_for - path = f if not subs else '/'.join(subs + [f]) + if base: + path = '/'.join([base, f]) + else: + path = f if f.endswith('js'): self.scripts.append_script({ From d2cce95a12badafb497785c0663495a329f66f13 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Fri, 6 Jul 2018 15:16:57 -0400 Subject: [PATCH 07/27] Changed the meta tags dict to a list, added meta_tags to dash.__init__. --- dash/dash.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index c4712c8be5..d6e5f41472 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -37,6 +37,7 @@ def __init__( include_static_files=True, url_base_pathname='/', compress=True, + meta_tags=None, **kwargs): # pylint-disable: too-many-instance-attributes @@ -85,7 +86,7 @@ def __init__( # list of dependencies self.callback_map = {} - self._meta_tags = collections.OrderedDict() + self._meta_tags = meta_tags or [] self._favicon = None if compress: @@ -629,7 +630,7 @@ def _walk_static_directory(self): self._favicon = path def add_meta_tag(self, name, content): - self._meta_tags[name] = {'name': name, 'content': content} + self._meta_tags.append({'name': name, 'content': content}) def run_server(self, port=8050, From 1212ee53b50b6e9e839b728ff953fcb563436b0a Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Fri, 6 Jul 2018 16:41:53 -0400 Subject: [PATCH 08/27] Add test for meta tags. --- tests/test_integration.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1bdc298c41..25ec3c19da 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -266,3 +266,30 @@ def display_output(react_value, flow_value): self.startServer(app) self.wait_for_element_by_id('waitfor') self.percy_snapshot(name='flowtype') + + def test_meta_tags(self): + app = dash.Dash() + + app.layout = html.Div(id='content') + + metas = ( + ('description', 'my dash app'), + ('custom', 'customized') + ) + + for m in metas: + app.add_meta_tag(*m) + + self.startServer(app) + + meta = self.driver.find_elements_by_tag_name('meta') + + self.assertEqual(len(metas), len(meta) - 1, 'Not enough meta tags') + + for i in range(1, len(meta)): + meta_tag = meta[i] + meta_info = metas[i - 1] + name = meta_tag.get_attribute('name') + content = meta_tag.get_attribute('content') + self.assertEqual(name, meta_info[0]) + self.assertEqual(content, meta_info[1]) From 846c8fcf3250649809eadc065857071f97ef268e Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 12:14:01 -0400 Subject: [PATCH 09/27] Fix bad line that was included in rebase. --- dash/dash.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index d6e5f41472..df748d7a62 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -7,7 +7,6 @@ import json import pkgutil import warnings -import shutil import re from functools import wraps @@ -326,7 +325,6 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument scripts = self._generate_scripts_html() css = self._generate_css_dist_html() config = self._generate_config_html() - metas = self._meta_tags.values() title = getattr(self, 'title', 'Dash') return ''' From 4777731bdae3a26de0a3ecea54194b0982ba1a91 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 12:21:33 -0400 Subject: [PATCH 10/27] Add support for static external resources. --- dash/dash.py | 18 +++++++++++------- dash/resources.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index df748d7a62..daec47a51e 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -79,7 +79,8 @@ def __init__( 'routes_pathname_prefix': url_base_pathname, 'requests_pathname_prefix': url_base_pathname, 'include_static_files': include_static_files, - 'static_folder': static_folder + 'static_folder': static_folder, + 'static_external_path': '' }) # list of dependencies @@ -599,6 +600,13 @@ def _walk_static_directory(self): walk_dir = self.config.static_folder slash_splitter = re.compile(r'[\\/]+') + def add_resource(p): + res = {'static_path': p} + if self.config.static_external_path: + res['external_url'] = '{}{}'.format( + self.config.static_external_path, path) + return res + for current, _, files in os.walk(walk_dir): if current == walk_dir: base = '' @@ -617,13 +625,9 @@ def _walk_static_directory(self): path = f if f.endswith('js'): - self.scripts.append_script({ - 'static_path': path - }) + self.scripts.append_script(add_resource(path)) elif f.endswith('css'): - self.css.append_css({ - 'static_path': path - }) + self.css.append_css(add_resource(path)) elif f == 'favicon.ico': self._favicon = path diff --git a/dash/resources.py b/dash/resources.py index b4f61bc075..9ee82e4f7d 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -30,7 +30,7 @@ def _filter_resources(self, all_resources): elif 'absolute_path' in s: filtered_resource['absolute_path'] = s['absolute_path'] elif 'static_path' in s: - filtered_resource = s + filtered_resource['static_path'] = s['static_path'] elif self.config.serve_locally: warnings.warn( 'A local version of {} is not available'.format( From 23195f0e5e0bd157a8a3a88fa6eb5bfd16ec2c63 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 12:31:10 -0400 Subject: [PATCH 11/27] Rename `static` to `assets` for user static file includes. --- dash/dash.py | 60 ++++++++++++++++++++--------------------------- dash/resources.py | 4 ++-- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index daec47a51e..dc5daba7c0 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -31,9 +31,9 @@ def __init__( self, name='__main__', server=None, - static_folder=None, - static_url_path='/static', - include_static_files=True, + assets_folder=None, + assets_url_path='/assets', + include_assets_files=True, url_base_pathname='/', compress=True, meta_tags=None, @@ -50,37 +50,23 @@ def __init__( name = name or 'dash' - lib_dir = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..')) - template_folder = os.path.join(lib_dir, 'templates') - lib_static = os.path.join(lib_dir, 'static') - - # Set the static folder (same as flask does, but we can use it) - static_folder = static_folder or os.path.join( - os.getcwd(), 'static' + self.assets_folder = assets_folder or os.path.join( + os.getcwd(), 'assets' ) # allow users to supply their own flask server - self.server = server or Flask(name, - static_folder=static_folder, - static_url_path=static_url_path) - - _dash_files = flask.Blueprint('_dash_files', - '_dash_files', - url_prefix='/dash-files', - static_folder=lib_static, - template_folder=template_folder) - - self.server.register_blueprint(_dash_files) + self.server = server or Flask(name) self.url_base_pathname = url_base_pathname self.config = _AttributeDict({ 'suppress_callback_exceptions': False, 'routes_pathname_prefix': url_base_pathname, 'requests_pathname_prefix': url_base_pathname, - 'include_static_files': include_static_files, - 'static_folder': static_folder, - 'static_external_path': '' + 'include_assets_files': include_assets_files, + 'assets_folder': assets_folder or os.path.join( + os.getcwd(), 'assets'), + 'assets_external_path': '', + 'assets_url_path': assets_url_path, }) # list of dependencies @@ -247,9 +233,9 @@ def _relative_url_path(relative_package_path='', namespace=''): raise Exception( 'Serving files from absolute_path isn\'t supported yet' ) - elif 'static_path' in resource: - static_url = flask.url_for('static', - filename=resource['static_path']) + elif 'asset_path' in resource: + static_url = flask.url_for('assets.static', + filename=resource['asset_path']) srcs.append(static_url) return srcs @@ -590,21 +576,21 @@ def dispatch(self): return self.callback_map[target_id]['callback'](*args) def _setup_server(self): - if self.config.include_static_files: - self._walk_static_directory() + if self.config.include_assets_files: + self._walk_assets_directory() self._generate_scripts_html() self._generate_css_dist_html() - def _walk_static_directory(self): - walk_dir = self.config.static_folder + def _walk_assets_directory(self): + walk_dir = self.config.assets_folder slash_splitter = re.compile(r'[\\/]+') def add_resource(p): - res = {'static_path': p} - if self.config.static_external_path: + res = {'asset_path': p} + if self.config.assets_external_path: res['external_url'] = '{}{}'.format( - self.config.static_external_path, path) + self.config.assets_external_path, path) return res for current, _, files in os.walk(walk_dir): @@ -638,4 +624,8 @@ def run_server(self, port=8050, debug=False, **flask_run_options): + bp = flask.Blueprint('assets', 'assets', + static_folder=self.config.assets_folder, + static_url_path=self.config.assets_url_path) + self.server.register_blueprint(bp) self.server.run(port=port, debug=debug, **flask_run_options) diff --git a/dash/resources.py b/dash/resources.py index 9ee82e4f7d..70f37a5389 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -29,8 +29,8 @@ def _filter_resources(self, all_resources): ) elif 'absolute_path' in s: filtered_resource['absolute_path'] = s['absolute_path'] - elif 'static_path' in s: - filtered_resource['static_path'] = s['static_path'] + elif 'asset_path' in s: + filtered_resource['asset_path'] = s['asset_path'] elif self.config.serve_locally: warnings.warn( 'A local version of {} is not available'.format( From 0c96dc71af985de7b0d05b0e345509115b16a357 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 14:40:55 -0400 Subject: [PATCH 12/27] Add _generate_meta_html to build html meta tags. --- dash/dash.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index dc5daba7c0..f930def97e 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -278,6 +278,20 @@ def _generate_config_html(self): '' ).format(json.dumps(self._config())) + def _generate_meta_html(self): + has_charset = any(filter(lambda x: 'charset' in x, self._meta_tags)) + + tags = [] + if not has_charset: + tags.append('') + for meta in self._meta_tags: + attributes = [] + for k, v in meta.items(): + attributes.append('{}="{}"'.format(k, v)) + tags.append(''.format(' '.join(attributes))) + + return '\n '.join(tags) + # Serve the JS bundles for each package def serve_component_suites(self, package_name, path_in_package_dist): if package_name not in self.registered_paths: @@ -312,12 +326,13 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument scripts = self._generate_scripts_html() css = self._generate_css_dist_html() config = self._generate_config_html() + metas = self._generate_meta_html() title = getattr(self, 'title', 'Dash') return ''' - + {} {} {} @@ -333,7 +348,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument - '''.format(title, css, config, scripts) + '''.format(metas, title, css, config, scripts) def dependencies(self): return flask.jsonify([ From f9434716bf093d6267b233450bfa3756bcb6bd66 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 15:08:49 -0400 Subject: [PATCH 13/27] Add index customization by string interpolations. --- dash/_utils.py | 8 +++++++ dash/dash.py | 61 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index edc5c20f52..11d1cfd6f6 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -1,3 +1,11 @@ +def interpolate_str(template, **data): + s = template + for k, v in data.items(): + key = '%({})'.format(k) + s = s.replace(key, v) + return s + + class AttributeDict(dict): """ Dictionary subclass enabling attribute lookup/assignment of keys/values. diff --git a/dash/dash.py b/dash/dash.py index f930def97e..d5d947661a 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -22,6 +22,33 @@ from .development.base_component import Component from . import exceptions from ._utils import AttributeDict as _AttributeDict +from ._utils import interpolate_str + +_default_index = ''' + + + + %(metas) + %(title) + %(css) + + + %(app_entry) +
+ %(config) + %(scripts) +
+ + +''' + +_app_entry = ''' +
+
+ Loading... +
+
+''' # pylint: disable=too-many-instance-attributes @@ -37,6 +64,7 @@ def __init__( url_base_pathname='/', compress=True, meta_tags=None, + index_string=_default_index, **kwargs): # pylint-disable: too-many-instance-attributes @@ -72,6 +100,7 @@ def __init__( # list of dependencies self.callback_map = {} + self.index_string = index_string self._meta_tags = meta_tags or [] self._favicon = None @@ -328,27 +357,17 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument config = self._generate_config_html() metas = self._generate_meta_html() title = getattr(self, 'title', 'Dash') - return ''' - - - - {} - {} - {} - - -
-
- Loading... -
-
-
- {} - {} -
- - - '''.format(metas, title, css, config, scripts) + return self.interpolate_index( + metas, title, css, config, scripts, _app_entry) + + def interpolate_index(self, metas, title, css, config, scripts, app_entry): + return interpolate_str(self.index_string, + metas=metas, + title=title, + css=css, + config=config, + scripts=scripts, + app_entry=app_entry) def dependencies(self): return flask.jsonify([ From cd0c15c04eddefc49663ea6d82edd29a02f0bbb9 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 15:24:20 -0400 Subject: [PATCH 14/27] Re-add support for favicon in interpolate_index. --- dash/dash.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index d5d947661a..8ea5575cee 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -30,6 +30,7 @@ %(metas) %(title) + %(favicon) %(css) @@ -357,16 +358,22 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument config = self._generate_config_html() metas = self._generate_meta_html() title = getattr(self, 'title', 'Dash') + favicon = ''.format( + flask.url_for('assets.static', filename=self._favicon))\ + if self._favicon else '' return self.interpolate_index( - metas, title, css, config, scripts, _app_entry) + metas, title, css, config, scripts, _app_entry, favicon) - def interpolate_index(self, metas, title, css, config, scripts, app_entry): + def interpolate_index(self, + metas, title, css, config, + scripts, app_entry, favicon): return interpolate_str(self.index_string, metas=metas, title=title, css=css, config=config, scripts=scripts, + favicon=favicon, app_entry=app_entry) def dependencies(self): From 0d9151c636a180bdefe119cd481689ffc52193bb Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 15:46:32 -0400 Subject: [PATCH 15/27] Change add_meta_tag args to a dict to support every meta attributes. --- dash/dash.py | 4 ++-- tests/test_integration.py | 26 +++++++++----------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 8ea5575cee..401e156718 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -658,8 +658,8 @@ def add_resource(p): elif f == 'favicon.ico': self._favicon = path - def add_meta_tag(self, name, content): - self._meta_tags.append({'name': name, 'content': content}) + def add_meta_tag(self, meta): + self._meta_tags.append(meta) def run_server(self, port=8050, diff --git a/tests/test_integration.py b/tests/test_integration.py index 25ec3c19da..0dbb131e60 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,6 +6,7 @@ import dash_core_components as dcc import dash_flow_example import dash + from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate from .IntegrationTests import IntegrationTests @@ -13,13 +14,6 @@ class Tests(IntegrationTests): - def setUp(self): - def wait_for_element_by_id(id): - wait_for(lambda: None is not invincible( - lambda: self.driver.find_element_by_id(id) - )) - return self.driver.find_element_by_id(id) - self.wait_for_element_by_id = wait_for_element_by_id def test_simple_callback(self): app = dash.Dash(__name__) @@ -268,22 +262,20 @@ def display_output(react_value, flow_value): self.percy_snapshot(name='flowtype') def test_meta_tags(self): - app = dash.Dash() - - app.layout = html.Div(id='content') - metas = ( - ('description', 'my dash app'), - ('custom', 'customized') + {'name': 'description', 'content': 'my dash app'}, + {'name': 'custom', 'content': 'customized'} ) - for m in metas: - app.add_meta_tag(*m) + app = dash.Dash(meta_tags=metas) + + app.layout = html.Div(id='content') self.startServer(app) meta = self.driver.find_elements_by_tag_name('meta') + # -1 for the meta charset. self.assertEqual(len(metas), len(meta) - 1, 'Not enough meta tags') for i in range(1, len(meta)): @@ -291,5 +283,5 @@ def test_meta_tags(self): meta_info = metas[i - 1] name = meta_tag.get_attribute('name') content = meta_tag.get_attribute('content') - self.assertEqual(name, meta_info[0]) - self.assertEqual(content, meta_info[1]) + self.assertEqual(name, meta_info['name']) + self.assertEqual(content, meta_info['content']) From ff1cff3e80337e6afcd0d50abba6c947ff98d4bf Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 10 Jul 2018 16:31:27 -0400 Subject: [PATCH 16/27] Add test for index customization. --- dash/_utils.py | 2 +- dash/dash.py | 45 +++++++++++++++-------------- tests/test_integration.py | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 11d1cfd6f6..55ea16f72d 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -1,7 +1,7 @@ def interpolate_str(template, **data): s = template for k, v in data.items(): - key = '%({})'.format(k) + key = '{' + k + '}' s = s.replace(key, v) return s diff --git a/dash/dash.py b/dash/dash.py index 401e156718..89d2cc69e3 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -22,22 +22,22 @@ from .development.base_component import Component from . import exceptions from ._utils import AttributeDict as _AttributeDict -from ._utils import interpolate_str +from ._utils import interpolate_str as _interpolate _default_index = ''' - %(metas) - %(title) - %(favicon) - %(css) + {metas} + {title} + {favicon} + {css} - %(app_entry) + {app_entry}
- %(config) - %(scripts) + {config} + {scripts}
@@ -59,6 +59,7 @@ def __init__( self, name='__main__', server=None, + static_folder='static', assets_folder=None, assets_url_path='/assets', include_assets_files=True, @@ -84,7 +85,7 @@ def __init__( ) # allow users to supply their own flask server - self.server = server or Flask(name) + self.server = server or Flask(name, static_folder=static_folder) self.url_base_pathname = url_base_pathname self.config = _AttributeDict({ @@ -309,7 +310,7 @@ def _generate_config_html(self): ).format(json.dumps(self._config())) def _generate_meta_html(self): - has_charset = any(filter(lambda x: 'charset' in x, self._meta_tags)) + has_charset = any('charset' in x for x in self._meta_tags) tags = [] if not has_charset: @@ -358,23 +359,25 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument config = self._generate_config_html() metas = self._generate_meta_html() title = getattr(self, 'title', 'Dash') - favicon = ''.format( - flask.url_for('assets.static', filename=self._favicon))\ - if self._favicon else '' + if self._favicon: + favicon = ''.format( + flask.url_for('assets.static', filename=self._favicon)) + else: + favicon = '' return self.interpolate_index( metas, title, css, config, scripts, _app_entry, favicon) def interpolate_index(self, metas, title, css, config, scripts, app_entry, favicon): - return interpolate_str(self.index_string, - metas=metas, - title=title, - css=css, - config=config, - scripts=scripts, - favicon=favicon, - app_entry=app_entry) + return _interpolate(self.index_string, + metas=metas, + title=title, + css=css, + config=config, + scripts=scripts, + favicon=favicon, + app_entry=app_entry) def dependencies(self): return flask.jsonify([ diff --git a/tests/test_integration.py b/tests/test_integration.py index 0dbb131e60..0641e53c9e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,6 +6,7 @@ import dash_core_components as dcc import dash_flow_example import dash +import time from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate @@ -14,6 +15,13 @@ class Tests(IntegrationTests): + def setUp(self): + def wait_for_element_by_id(id): + wait_for(lambda: None is not invincible( + lambda: self.driver.find_element_by_id(id) + )) + return self.driver.find_element_by_id(id) + self.wait_for_element_by_id = wait_for_element_by_id def test_simple_callback(self): app = dash.Dash(__name__) @@ -285,3 +293,54 @@ def test_meta_tags(self): content = meta_tag.get_attribute('content') self.assertEqual(name, meta_info['name']) self.assertEqual(content, meta_info['content']) + + def test_index_customization(self): + app = dash.Dash() + + app.index_string = ''' + + + + {metas} + {title} + {favicon} + {css} + + +
My custom header
+
+ {app_entry} +
+ {config} + {scripts} +
+ + + + + ''' + + app.layout = html.Div('Dash app', id='app') + + self.startServer(app) + + time.sleep(0.5) + + header = self.wait_for_element_by_id('custom-header') + footer = self.wait_for_element_by_id('custom-footer') + + self.assertEqual('My custom header', header.text) + self.assertEqual('My custom footer', footer.text) + + add = self.wait_for_element_by_id('add') + + self.assertEqual('Got added', add.text) + + self.percy_snapshot('custom-index') From eaf91a1fb6d33a0a553e09e6af32ce33b591381a Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 11 Jul 2018 16:44:47 -0400 Subject: [PATCH 17/27] Add test for assets. --- tests/assets/load_first.js | 1 + tests/assets/nested_css/nested.css | 3 ++ tests/assets/nested_js/load_after.js | 1 + tests/assets/nested_js/load_after1.js | 2 ++ tests/assets/reset.css | 1 + tests/test_integration.py | 49 +++++++++++++++++++++++++++ 6 files changed, 57 insertions(+) create mode 100644 tests/assets/load_first.js create mode 100644 tests/assets/nested_css/nested.css create mode 100644 tests/assets/nested_js/load_after.js create mode 100644 tests/assets/nested_js/load_after1.js create mode 100644 tests/assets/reset.css diff --git a/tests/assets/load_first.js b/tests/assets/load_first.js new file mode 100644 index 0000000000..b68378509e --- /dev/null +++ b/tests/assets/load_first.js @@ -0,0 +1 @@ +window.tested = ['load_first']; \ No newline at end of file diff --git a/tests/assets/nested_css/nested.css b/tests/assets/nested_css/nested.css new file mode 100644 index 0000000000..24ba3d2fa2 --- /dev/null +++ b/tests/assets/nested_css/nested.css @@ -0,0 +1,3 @@ +#content { + padding: 8px; +} \ No newline at end of file diff --git a/tests/assets/nested_js/load_after.js b/tests/assets/nested_js/load_after.js new file mode 100644 index 0000000000..6f520fdb85 --- /dev/null +++ b/tests/assets/nested_js/load_after.js @@ -0,0 +1 @@ +window.tested.push('load_after'); \ No newline at end of file diff --git a/tests/assets/nested_js/load_after1.js b/tests/assets/nested_js/load_after1.js new file mode 100644 index 0000000000..ee2f5b5549 --- /dev/null +++ b/tests/assets/nested_js/load_after1.js @@ -0,0 +1,2 @@ +window.tested.push('load_after1'); +document.getElementById('tested').innerHTML = JSON.stringify(window.tested); diff --git a/tests/assets/reset.css b/tests/assets/reset.css new file mode 100644 index 0000000000..8c521ddd5e --- /dev/null +++ b/tests/assets/reset.css @@ -0,0 +1 @@ +body {margin: 0;} diff --git a/tests/test_integration.py b/tests/test_integration.py index 0641e53c9e..6674497933 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,3 +1,4 @@ +import json from multiprocessing import Value import datetime import itertools @@ -344,3 +345,51 @@ def test_index_customization(self): self.assertEqual('Got added', add.text) self.percy_snapshot('custom-index') + + def test_assets(self): + app = dash.Dash(assets_folder='tests/assets') + app.index_string = ''' + + + + {metas} + {title} + {css} + + +
+ {app_entry} +
+ {config} + {scripts} +
+ + + ''' + + app.layout = html.Div([ + html.Div(id='content'), + dcc.Input(id='test') + ], id='layout') + + self.startServer(app) + + body = self.driver.find_element_by_tag_name('body') + + body_margin = body.value_of_css_property('margin') + self.assertEqual('0px', body_margin) + + content = self.wait_for_element_by_id('content') + content_padding = content.value_of_css_property('padding') + self.assertEqual('8px', content_padding) + + tested = self.wait_for_element_by_id('tested') + tested = json.loads(tested.text) + self.assertEqual(3, len(tested)) + + order = ('load_first', 'load_after', 'load_after1') + + for i in range(3): + self.assertEqual(order[i], tested[i]) + + self.percy_snapshot('test assets includes') From 2197e387d3f4d0c55a7fb203dbe90076d53fbcc8 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 11 Jul 2018 18:15:58 -0400 Subject: [PATCH 18/27] Add checks for index, change the format syntax to {%key%}, more tests. --- dash/_utils.py | 2 +- dash/dash.py | 66 +++++++++++++++++++++++++++++++++------ tests/test_integration.py | 65 ++++++++++++++++++++++++++++++-------- 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 55ea16f72d..1c6e5ca51c 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -1,7 +1,7 @@ def interpolate_str(template, **data): s = template for k, v in data.items(): - key = '{' + k + '}' + key = '{%' + k + '%}' s = s.replace(key, v) return s diff --git a/dash/dash.py b/dash/dash.py index 89d2cc69e3..80c4e93071 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -28,16 +28,16 @@ - {metas} - {title} - {favicon} - {css} + {%metas%} + {%title%} + {%favicon%} + {%css%} - {app_entry} + {%app_entry%}
- {config} - {scripts} + {%config%} + {%scripts%}
@@ -51,9 +51,17 @@ ''' +_re_index_entry = re.compile(r'{%app_entry%}') +_re_index_config = re.compile(r'{%config%}') +_re_index_scripts = re.compile(r'{%scripts%}') + +_re_index_entry_id = re.compile(r'(id="react-entry-point")') +_re_index_config_id = re.compile(r'(id="_dash-config")') +_re_index_scripts_id = re.compile(r'(src=".*dash[-_]renderer.*")') + # pylint: disable=too-many-instance-attributes -# pylint: disable=too-many-arguments, too-many-locals +# pylint: disable=too-many-arguments class Dash(object): def __init__( self, @@ -102,6 +110,7 @@ def __init__( # list of dependencies self.callback_map = {} + self._index_string = '' self.index_string = index_string self._meta_tags = meta_tags or [] self._favicon = None @@ -200,6 +209,26 @@ def layout(self, value): self.css._update_layout(layout_value) self.scripts._update_layout(layout_value) + @property + def index_string(self): + return self._index_string + + @index_string.setter + def index_string(self, value): + checks = ( + (_re_index_entry.search(value), 'app_entry'), + (_re_index_config.search(value), 'config',), + (_re_index_scripts.search(value), 'scripts'), + ) + missing = [missing for check, missing in checks if not check] + if missing: + raise Exception( + 'Did you forget to include {} in your index string ?'.format( + ', '.join('{%' + x + '%}' for x in missing) + ) + ) + self._index_string = value + def serve_layout(self): layout = self._layout_value() @@ -364,9 +393,28 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument flask.url_for('assets.static', filename=self._favicon)) else: favicon = '' - return self.interpolate_index( + + index = self.interpolate_index( metas, title, css, config, scripts, _app_entry, favicon) + checks = ( + (_re_index_entry_id.search(index), '#react-entry-point'), + (_re_index_config_id.search(index), '#_dash-configs'), + (_re_index_scripts_id.search(index), 'dash-renderer'), + ) + missing = [missing for check, missing in checks if not check] + + if missing: + plural = 's' if len(missing) > 1 else '' + raise Exception( + 'Missing element{pl} {ids} in index.'.format( + ids=', '.join(missing), + pl=plural + ) + ) + + return index + def interpolate_index(self, metas, title, css, config, scripts, app_entry, favicon): diff --git a/tests/test_integration.py b/tests/test_integration.py index 6674497933..d333a9e469 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,6 +6,7 @@ import dash_html_components as html import dash_core_components as dcc import dash_flow_example + import dash import time @@ -302,18 +303,18 @@ def test_index_customization(self): - {metas} - {title} - {favicon} - {css} + {%metas%} + {%title%} + {%favicon%} + {%css%}
My custom header
- {app_entry} + {%app_entry%}
- {config} - {scripts} + {%config%} + {%scripts%}
@@ -352,16 +356,16 @@ def test_assets(self): - {metas} - {title} - {css} + {%metas%} + {%title%} + {%css%}
- {app_entry} + {%app_entry%}
- {config} - {scripts} + {%config%} + {%scripts%}
@@ -393,3 +397,38 @@ def test_assets(self): self.assertEqual(order[i], tested[i]) self.percy_snapshot('test assets includes') + + def test_invalid_index_string(self): + app = dash.Dash() + + def will_raise(): + app.index_string = ''' + + + + {%metas%} + {%title%} + {%favicon%} + {%css%} + + +
My custom header
+
+
+
+ + + ''' + + with self.assertRaises(Exception) as context: + will_raise() + + app.layout = html.Div() + self.startServer(app) + + exc_msg = str(context.exception) + self.assertTrue('{%app_entry%}' in exc_msg) + self.assertTrue('{%config%}' in exc_msg) + self.assertTrue('{%scripts%}' in exc_msg) + time.sleep(0.5) + print('invalid index string') From ea0d2ca5fd87f377927f7a35b16c00de511575be Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 17 Jul 2018 16:11:46 -0400 Subject: [PATCH 19/27] Change interpolate_index params to kwargs. --- dash/dash.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 80c4e93071..9866cb77dd 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -55,9 +55,9 @@ _re_index_config = re.compile(r'{%config%}') _re_index_scripts = re.compile(r'{%scripts%}') -_re_index_entry_id = re.compile(r'(id="react-entry-point")') -_re_index_config_id = re.compile(r'(id="_dash-config")') -_re_index_scripts_id = re.compile(r'(src=".*dash[-_]renderer.*")') +_re_index_entry_id = re.compile(r'id="react-entry-point"') +_re_index_config_id = re.compile(r'id="_dash-config"') +_re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"') # pylint: disable=too-many-instance-attributes @@ -101,8 +101,7 @@ def __init__( 'routes_pathname_prefix': url_base_pathname, 'requests_pathname_prefix': url_base_pathname, 'include_assets_files': include_assets_files, - 'assets_folder': assets_folder or os.path.join( - os.getcwd(), 'assets'), + 'assets_folder': self.assets_folder, 'assets_external_path': '', 'assets_url_path': assets_url_path, }) @@ -395,7 +394,8 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument favicon = '' index = self.interpolate_index( - metas, title, css, config, scripts, _app_entry, favicon) + metas=metas, title=title, css=css, config=config, + scripts=scripts, app_entry=_app_entry, favicon=favicon) checks = ( (_re_index_entry_id.search(index), '#react-entry-point'), @@ -416,8 +416,8 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument return index def interpolate_index(self, - metas, title, css, config, - scripts, app_entry, favicon): + metas='', title='', css='', config='', + scripts='', app_entry='', favicon=''): return _interpolate(self.index_string, metas=metas, title=title, From 4cf9e5ca2683c4d282ccdb6b6f5f2e7c5454ad38 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 17 Jul 2018 16:22:51 -0400 Subject: [PATCH 20/27] Remove `add_meta_tag`. --- dash/dash.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 9866cb77dd..cb191cb7c3 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -709,9 +709,6 @@ def add_resource(p): elif f == 'favicon.ico': self._favicon = path - def add_meta_tag(self, meta): - self._meta_tags.append(meta) - def run_server(self, port=8050, debug=False, From f6e9922e37e596d1043abfe62be110165d4135a1 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 18 Jul 2018 10:45:07 -0400 Subject: [PATCH 21/27] Block pylint version to 1.9.2 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 282b362edc..f667e354c3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -12,4 +12,4 @@ six plotly>=2.0.8 requests[security] flake8 -pylint +pylint==1.9.2 From 8cc781bb0c21c6f9f7a94b80c1edb3f8b18b87f4 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 23 Jul 2018 17:37:14 -0400 Subject: [PATCH 22/27] Put assets Blueprint in ctor, remove related configs. --- dash/dash.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index cb191cb7c3..951a6a8c30 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -86,24 +86,25 @@ def __init__( See https://github.com/plotly/dash/issues/141 for details. ''', DeprecationWarning) - name = name or 'dash' - - self.assets_folder = assets_folder or os.path.join( + self._assets_folder = assets_folder or os.path.join( os.getcwd(), 'assets' ) # allow users to supply their own flask server self.server = server or Flask(name, static_folder=static_folder) + self.server.register_blueprint( + flask.Blueprint('assets', 'assets', + static_folder=self._assets_folder, + static_url_path=assets_url_path)) + self.url_base_pathname = url_base_pathname self.config = _AttributeDict({ 'suppress_callback_exceptions': False, 'routes_pathname_prefix': url_base_pathname, 'requests_pathname_prefix': url_base_pathname, 'include_assets_files': include_assets_files, - 'assets_folder': self.assets_folder, 'assets_external_path': '', - 'assets_url_path': assets_url_path, }) # list of dependencies @@ -675,7 +676,7 @@ def _setup_server(self): self._generate_css_dist_html() def _walk_assets_directory(self): - walk_dir = self.config.assets_folder + walk_dir = self._assets_folder slash_splitter = re.compile(r'[\\/]+') def add_resource(p): @@ -713,8 +714,4 @@ def run_server(self, port=8050, debug=False, **flask_run_options): - bp = flask.Blueprint('assets', 'assets', - static_folder=self.config.assets_folder, - static_url_path=self.config.assets_url_path) - self.server.register_blueprint(bp) self.server.run(port=port, debug=debug, **flask_run_options) From 0046402b18ea07cce0954c0c3ce1e61ceecca0bc Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 23 Jul 2018 18:00:23 -0400 Subject: [PATCH 23/27] Use `flask.helpers.get_root_path` for assets folder resolution. --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 951a6a8c30..9e39356036 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -87,7 +87,7 @@ def __init__( ''', DeprecationWarning) self._assets_folder = assets_folder or os.path.join( - os.getcwd(), 'assets' + flask.helpers.get_root_path(name), 'assets' ) # allow users to supply their own flask server From 6af0381a72388647ba3245a03a98d2e46e536e5b Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 24 Jul 2018 10:55:00 -0400 Subject: [PATCH 24/27] Add docstring to interpolate_index. --- dash/dash.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/dash/dash.py b/dash/dash.py index 9e39356036..8aeb569ad6 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -419,6 +419,42 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument def interpolate_index(self, metas='', title='', css='', config='', scripts='', app_entry='', favicon=''): + """ + Called to create the initial HTML string that is loaded on page. + Override this method to provide you own custom HTML. + + :Example: + + class MyDash(dash.Dash): + def interpolate_index(self, **kwargs): + return ''' + + + + My App + + +
My custom header
+ {} + {} + {} + + + + '''.format( + kwargs.get('app_entry'), + kwargs.get('config'), + kwargs.get('scripts')) + + :param metas: Collected & formatted meta tags. + :param title: The title of the app. + :param css: Collected & formatted css dependencies as tags. + :param config: Configs needed by dash-renderer. + :param scripts: Collected & formatted scripts tags. + :param app_entry: Where the app will render. + :param favicon: A favicon tag if found in assets folder. + :return: The interpolated HTML string for the index. + """ return _interpolate(self.index_string, metas=metas, title=title, From 2c104a54a1418d7708ec803813e78aba07c90fa8 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 25 Jul 2018 11:02:33 -0400 Subject: [PATCH 25/27] Ensure assets files are ordered, add more test files. --- dash/dash.py | 14 +++++++------- tests/assets/nested_js/load_after1.js | 1 - tests/assets/nested_js/load_after10.js | 1 + tests/assets/nested_js/load_after11.js | 1 + tests/assets/nested_js/load_after2.js | 1 + tests/assets/nested_js/load_after3.js | 1 + tests/assets/nested_js/load_after4.js | 1 + tests/assets/nested_js/load_last.js | 1 + tests/test_integration.py | 9 ++++++--- 9 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 tests/assets/nested_js/load_after10.js create mode 100644 tests/assets/nested_js/load_after11.js create mode 100644 tests/assets/nested_js/load_after2.js create mode 100644 tests/assets/nested_js/load_after3.js create mode 100644 tests/assets/nested_js/load_after4.js create mode 100644 tests/assets/nested_js/load_last.js diff --git a/dash/dash.py b/dash/dash.py index 8aeb569ad6..edc3ccc2bb 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -435,16 +435,16 @@ def interpolate_index(self, **kwargs):
My custom header
- {} - {} - {} + {app_entry} + {config} + {scripts} '''.format( - kwargs.get('app_entry'), - kwargs.get('config'), - kwargs.get('scripts')) + app_entry=kwargs.get('app_entry'), + config=kwargs.get('config'), + scripts=kwargs.get('scripts')) :param metas: Collected & formatted meta tags. :param title: The title of the app. @@ -733,7 +733,7 @@ def add_resource(p): else: base = splitted[0] - for f in files: + for f in sorted(files): if base: path = '/'.join([base, f]) else: diff --git a/tests/assets/nested_js/load_after1.js b/tests/assets/nested_js/load_after1.js index ee2f5b5549..1629d393d7 100644 --- a/tests/assets/nested_js/load_after1.js +++ b/tests/assets/nested_js/load_after1.js @@ -1,2 +1 @@ window.tested.push('load_after1'); -document.getElementById('tested').innerHTML = JSON.stringify(window.tested); diff --git a/tests/assets/nested_js/load_after10.js b/tests/assets/nested_js/load_after10.js new file mode 100644 index 0000000000..fcfdb59ae9 --- /dev/null +++ b/tests/assets/nested_js/load_after10.js @@ -0,0 +1 @@ +window.tested.push('load_after10'); \ No newline at end of file diff --git a/tests/assets/nested_js/load_after11.js b/tests/assets/nested_js/load_after11.js new file mode 100644 index 0000000000..bd11cd28be --- /dev/null +++ b/tests/assets/nested_js/load_after11.js @@ -0,0 +1 @@ +window.tested.push('load_after11'); \ No newline at end of file diff --git a/tests/assets/nested_js/load_after2.js b/tests/assets/nested_js/load_after2.js new file mode 100644 index 0000000000..0b76a55fae --- /dev/null +++ b/tests/assets/nested_js/load_after2.js @@ -0,0 +1 @@ +window.tested.push('load_after2'); \ No newline at end of file diff --git a/tests/assets/nested_js/load_after3.js b/tests/assets/nested_js/load_after3.js new file mode 100644 index 0000000000..d913af94e7 --- /dev/null +++ b/tests/assets/nested_js/load_after3.js @@ -0,0 +1 @@ +window.tested.push('load_after3'); \ No newline at end of file diff --git a/tests/assets/nested_js/load_after4.js b/tests/assets/nested_js/load_after4.js new file mode 100644 index 0000000000..2507e38b00 --- /dev/null +++ b/tests/assets/nested_js/load_after4.js @@ -0,0 +1 @@ +window.tested.push('load_after4'); \ No newline at end of file diff --git a/tests/assets/nested_js/load_last.js b/tests/assets/nested_js/load_last.js new file mode 100644 index 0000000000..285aa60506 --- /dev/null +++ b/tests/assets/nested_js/load_last.js @@ -0,0 +1 @@ +document.getElementById('tested').innerHTML = JSON.stringify(window.tested); diff --git a/tests/test_integration.py b/tests/test_integration.py index d333a9e469..770b9152cb 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -389,11 +389,14 @@ def test_assets(self): tested = self.wait_for_element_by_id('tested') tested = json.loads(tested.text) - self.assertEqual(3, len(tested)) - order = ('load_first', 'load_after', 'load_after1') + order = ('load_first', 'load_after', 'load_after1', + 'load_after10', 'load_after11', 'load_after2', + 'load_after3', 'load_after4', ) - for i in range(3): + self.assertEqual(len(order), len(tested)) + + for i in range(len(tested)): self.assertEqual(order[i], tested[i]) self.percy_snapshot('test assets includes') From 1171f0adfeee9950ec18a34a9fc494cdf392793c Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 25 Jul 2018 12:18:31 -0400 Subject: [PATCH 26/27] Update changelog and version. --- CHANGELOG.md | 5 +++++ dash/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3455701ca7..da263773ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.22.0 - 2018-07-25 +## Added +- Assets files & index customization [#286](https://github.com/plotly/dash/pull/286) +- Raise an error if there is no layout present when the server is running [#294](https://github.com/plotly/dash/pull/294) + ## 0.21.1 - 2018-04-10 ## Added - `aria-*` and `data-*` attributes are now supported in all dash html components. (#40) diff --git a/dash/version.py b/dash/version.py index 8c306aa668..81edede8b4 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.21.1' +__version__ = '0.22.0' From 495762ee782fa64706212fe3e0a97bd1f61879ea Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 25 Jul 2018 12:37:56 -0400 Subject: [PATCH 27/27] pylint fixes. --- dash/dash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 7399e83501..44fad1efee 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -707,7 +707,7 @@ def dispatch(self): def _setup_server(self): if self.config.include_assets_files: self._walk_assets_directory() - + # Make sure `layout` is set before running the server value = getattr(self, 'layout') if value is None: @@ -717,7 +717,7 @@ def _setup_server(self): 'at the time that `run_server` was called. ' 'Make sure to set the `layout` attribute of your application ' 'before running the server.') - + self._generate_scripts_html() self._generate_css_dist_html()