From c50decc7028f0dc8825632e551da94212afb8027 Mon Sep 17 00:00:00 2001 From: Mark Gibbs <delsim@users.noreply.github.com> Date: Tue, 9 Apr 2019 22:49:16 -0700 Subject: [PATCH] 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 --- .gitignore | 2 +- demo/demo/assets/image_one.png | Bin 0 -> 989 bytes demo/demo/plotly_apps.py | 11 ++++- demo/demo/settings.py | 11 ++++- demo/demo/templates/demo_nine.html | 33 ++++++++++++++ demo/demo/templates/index.html | 1 + demo/demo/urls.py | 1 + dev_requirements.txt | 4 +- django_plotly_dash/assets/some_asset | 0 django_plotly_dash/dash_wrapper.py | 48 +++++++++++++++++--- django_plotly_dash/finders.py | 59 +++++++++++++++++++++--- django_plotly_dash/middleware.py | 43 ++++++++++++++++++ django_plotly_dash/tests.py | 37 +++++++++++++++ django_plotly_dash/urls.py | 3 +- django_plotly_dash/util.py | 16 +++++++ django_plotly_dash/views.py | 14 +++++- docs/configuration.rst | 41 +++++++++++++++++ docs/faq.rst | 25 +++++++++++ docs/index.rst | 1 + docs/installation.rst | 11 +++-- docs/local_assets.rst | 65 +++++++++++++++++++++++++++ prepare_demo | 5 ++- requirements.txt | 10 ++--- 23 files changed, 412 insertions(+), 29 deletions(-) create mode 100644 demo/demo/assets/image_one.png create mode 100644 demo/demo/templates/demo_nine.html create mode 100644 django_plotly_dash/assets/some_asset create mode 100644 docs/local_assets.rst diff --git a/.gitignore b/.gitignore index 6bc3d3d..8a683ef 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ coverage.xml *.log local_settings.py */db.sqlite3 -demo/static/* +demo/staticfiles/* # Flask stuff: instance/ diff --git a/demo/demo/assets/image_one.png b/demo/demo/assets/image_one.png new file mode 100644 index 0000000000000000000000000000000000000000..6b839e88512e88de381b09ea0f0fc6072e2c42a9 GIT binary patch literal 989 zcmeAS@N?(olHy`uVBq!ia0y~yU`PRB4mJh`hJr^^Ll_ts*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l<H3=9nHC7!;n?2lR41eA@QZQswxz`!6`;u=xnoS&PUnpeW$ zT$GwvlA5AWo>`Ki;O^-g5Z=fq&cMLz>gnPbQgQ3;T}N-FM22G@kGpoOX`b*7sM1_0 zae9K%t|#Rj{<793uZ3EctGwbCxhu4*MB^8eBCBjuho7TDNrzwH#e)-PhN~P**VnxL zDyQ^D#`Nd>%toJ$EdI?Xuh}c1;JG6IGJh=#Qy`OIhfs&Z6bF_?EJ^~Z0;&Ry8jTz- zoGuDp3QU0%O8<0tS|pjBo$aAEnWOzMNBdz3d3kre>CKNV{`YhIZ|pj%l)O=bam(GL zjS}5QmHxNyv+Xc!`&coDVTO-dWl4$4_0pG>a@?&<>S}5n4IeA!9Fu7L^VZDVoU>u^ z;>9Jg+n?@uy-P+?QjsAqH+S#e9oMZ-NE~i$W&QT$i-Az5!R)h};?{4UlbxU6|FUG( z-o1O@ym;~8!Uciwu&@^veGXD*&z%!u;OFD3IsWGTdtrvlFH52)_^4erI-8rHZ+`sh z>(X=Q&;R!8+?KmJMvuEi<@c{&91Z^d{`->U<m5OSCVDK<_0XF7=<8Qg+2ao%JyK#U zC@R`ysW<<;FatX~du2t%iRaJL!?Z-_&7WVKmDR;C<7^tM)1-q56Br^^U#&U5XU`r1 zhUS9_(G$A6yPxg*{kxhep}f4j`2LqKCX656yjk<~&i3uw&!$gtu&Zddv9tU4k#A+# z>ilKPkNtC-mip5#yjNF*tM=sAy5FT9N)s6v=FFS-t1nX~yXWuTbLY-Id+nfb;oa*0 zyWcMkTb-H6!BAXW%+T=lYpFzXRG)P6hK(CProBnuoGZ;xP+8e|J#z8eWvz;y=RaH6 z*xaeH5a(j8n&+O()8F4e(`#wXb;bSuzIDIL%gZ?%{{H=Y=K1G6yLTrq4NBY^_4Mpn z-tw|C3uEKO=_l4}f6Xv?Wxd~h@x^W0U;CRTJh^n~((dzujemkxh9v1te|+PHgzo7@ zsgXV^LNT$isVhUW>I>&HD4v`=Svbu|l8=YSK!j`K>eb!ba*Hb~XNHG|#~00I{Hr;m j?rkW^$#li}ufnz6I+7wc8a^{HFfe$!`njxgN@xNAjP97M literal 0 HcmV?d00001 diff --git a/demo/demo/plotly_apps.py b/demo/demo/plotly_apps.py index 9ca7346..de3952a 100644 --- a/demo/demo/plotly_apps.py +++ b/demo/demo/plotly_apps.py @@ -84,6 +84,7 @@ def callback_size(dropdown_color, dropdown_size): a2 = DjangoDash("Ex2", serve_locally=True) + a2.layout = html.Div([ dcc.RadioItems(id="dropdown-one", 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, change_col, datetime.fromtimestamp(0.001*timestamp)) -liveOut = DjangoDash("LiveOutput", - )#serve_locally=True) +liveOut = DjangoDash("LiveOutput") def _get_cache_key(state_uid): return "demo-liveout-s6-%s" % state_uid @@ -309,3 +309,10 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs): return {'data':traces, #'layout': go.Layout } + +localState = DjangoDash("LocalState", + serve_locally=True) + +localState.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')), + html.Img(src='assets/image_two.png'), + ]) diff --git a/demo/demo/settings.py b/demo/demo/settings.py index e906b6a..a24315d 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -42,10 +42,14 @@ INSTALLED_APPS = [ 'bootstrap4', 'django_plotly_dash.apps.DjangoPlotlyDashConfig', + 'dpd_static_support', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -53,6 +57,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django_plotly_dash.middleware.BaseMiddleware', + 'django_plotly_dash.middleware.ExternalRedirectionMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -134,13 +139,15 @@ PLOTLY_DASH = { "view_decorator" : None, # Specify a function to be used to wrap each of the dpd view functions "cache_arguments" : True, # True for cache, False for session-based argument propagation + + #"serve_locally" : True, # True to serve assets locally, False to use their unadulterated urls (eg a CDN) } # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'demo', 'static'), @@ -178,6 +185,7 @@ STATICFILES_FINDERS = [ 'django_plotly_dash.finders.DashAssetFinder', 'django_plotly_dash.finders.DashComponentFinder', + 'django_plotly_dash.finders.DashAppDirectoryFinder', ] # Plotly components containing static content that should @@ -189,4 +197,5 @@ PLOTLY_COMPONENTS = [ 'dash_bootstrap_components', 'dash_renderer', 'dpd_components', + 'dpd_static_support', ] diff --git a/demo/demo/templates/demo_nine.html b/demo/demo/templates/demo_nine.html new file mode 100644 index 0000000..d21244e --- /dev/null +++ b/demo/demo/templates/demo_nine.html @@ -0,0 +1,33 @@ +{%extends "base.html"%} +{%load plotly_dash%} + +{%block title%}Demo Nine - Serving local assets{%endblock%} + +{%block content%} +<h1>Serving Local Assets</h1> +<p> + This example demonstrates serving local assets as part of a Dash app. +</p> +<p> + The extra files are specified using the standard plotly dash approach, and are + made available through the standard Django staticfiles infrastructure. This means that + they will be served the same way as other static files through a reverse proxy. +</p> +<p></p> +<div class="card bg-light border-dark"> + <div class="card-body"> + <p><span>{</span>% load plotly_dash %}</p> + <p><div class="<span>{</span>% plotly_class name="LocalState"%}"> + <p class="ml-3"><span>{</span>% plotly_app name="LocalState" ratio=0.3 %}</p> + <p><\div> + </div> +</div> +<p></p> +<div class="card border-dark"> + <div class="card-body"> + <div class="{%plotly_class name="LocalState"%}"> + {%plotly_app name="LocalState" ratio=0.3 %} + </div> + </div> +</div> +{%endblock%} diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html index d6e727a..f020630 100644 --- a/demo/demo/templates/index.html +++ b/demo/demo/templates/index.html @@ -15,5 +15,6 @@ <li><a class="btn btn-primary btnspace" href="{%url "demo-six"%}">Demo Six</a> - simple html injection example</li> <li><a class="btn btn-primary btnspace" href="{%url "demo-seven"%}">Demo Seven</a> - dash-bootstrap-components example</li> <li><a class="btn btn-primary btnspace" href="{%url "demo-eight"%}">Demo Eight</a> - Django session state example</li> + <li><a class="btn btn-primary btnspace" href="{%url "demo-nine"%}">Demo Nine</a> - local serving of assets</li> </ul> {%endblock%} diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 0db3cf9..0e4f215 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -44,6 +44,7 @@ urlpatterns = [ url('^demo-six', dash_example_1_view, name="demo-six"), url('^demo-seven', TemplateView.as_view(template_name='demo_seven.html'), name="demo-seven"), url('^demo-eight', session_state_view, {'template_name':'demo_eight.html'}, name="demo-eight"), + url('^demo-nine', TemplateView.as_view(template_name='demo_nine.html'), name="demo-nine"), url('^admin/', admin.site.urls), url('^django_plotly_dash/', include('django_plotly_dash.urls')), diff --git a/dev_requirements.txt b/dev_requirements.txt index a943b78..3ee1d37 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,9 +1,10 @@ channels>=2.0 channels-redis daphne -Django>=2.0 +Django>=2.0,<2.2 django-bootstrap4 django-redis +dpd-static-support grip pandas pylint @@ -16,4 +17,5 @@ redis sphinx sphinx-autobuild twine +whitenoise diff --git a/django_plotly_dash/assets/some_asset b/django_plotly_dash/assets/some_asset new file mode 100644 index 0000000..e69de29 diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index e0177f8..47ba434 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -39,6 +39,8 @@ from plotly.utils import PlotlyJSONEncoder from .app_name import app_name, main_view_label from .middleware import EmbeddedHolder +from .util import static_asset_path +from .util import serve_locally as serve_locally_setting uid_counter = 0 @@ -84,11 +86,12 @@ class DjangoDash: To use, construct an instance of DjangoDash() in place of a Dash() one. ''' #pylint: disable=too-many-instance-attributes - def __init__(self, name=None, serve_locally=False, + def __init__(self, name=None, serve_locally=None, expanded_callbacks=False, add_bootstrap_links=False, suppress_callback_exceptions=False, **kwargs): # pylint: disable=unused-argument, too-many-arguments + if name is None: global uid_counter # pylint: disable=global-statement uid_counter += 1 @@ -104,19 +107,40 @@ class DjangoDash: add_usable_app(self._uid, self) + if serve_locally is None: + self._serve_locally = serve_locally_setting() + else: + self._serve_locally = serve_locally + self._expanded_callbacks = expanded_callbacks - self._serve_locally = serve_locally self._suppress_callback_exceptions = suppress_callback_exceptions if add_bootstrap_links: from bootstrap4.bootstrap import css_url bootstrap_source = css_url()['href'] - self.css.append_script({'external_url':[bootstrap_source,]}) + + if self._serve_locally: + # Ensure package is loaded; if not present then pip install dpd-static-support + import dpd_static_support + hard_coded_package_name = "dpd_static_support" + base_file_name = bootstrap_source.split('/')[-1] + + self.css.append_script({'external_url': [bootstrap_source,], + 'relative_package_path' : base_file_name, + 'namespace': hard_coded_package_name, + }) + else: + self.css.append_script({'external_url':[bootstrap_source,],}) # Remember some caller info for static files caller_frame = inspect.stack()[1] self.caller_module = inspect.getmodule(caller_frame[0]) self.caller_module_location = inspect.getfile(self.caller_module) + self.assets_folder = "assets" + + def get_asset_static_url(self, asset_path): + module_name = self.caller_module.__name__ + return static_asset_path(module_name, asset_path) def as_dash_instance(self, cache_id=None): ''' @@ -205,6 +229,16 @@ class DjangoDash: self._expanded_callbacks = True return self.callback(output, inputs, state, events) + def get_asset_url(self, asset_name): + '''URL of an asset associated with this component + + Use a placeholder and insert later + ''' + + return "assets/" + str(asset_name) + + #return self.as_dash_instance().get_asset_url(asset_name) + class PseudoFlask: 'Dummy implementation of a Flask instance, providing stub functionality' def __init__(self): @@ -234,7 +268,8 @@ class WrappedDash(Dash): # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, base_pathname=None, replacements=None, ndid=None, - expanded_callbacks=False, serve_locally=False, **kwargs): + expanded_callbacks=False, serve_locally=False, + **kwargs): self._uid = ndid @@ -245,7 +280,8 @@ class WrappedDash(Dash): kwargs['url_base_pathname'] = self._base_pathname kwargs['server'] = self._notflask - super(WrappedDash, self).__init__(**kwargs) + super(WrappedDash, self).__init__(__name__, + **kwargs) self.css.config.serve_locally = serve_locally self.scripts.config.serve_locally = serve_locally @@ -507,6 +543,8 @@ class WrappedDash(Dash): def interpolate_index(self, **kwargs): #pylint: disable=arguments-differ + print("IN INTERPOLATE INDEX") + if not self._return_embedded: resp = super(WrappedDash, self).interpolate_index(**kwargs) return resp diff --git a/django_plotly_dash/finders.py b/django_plotly_dash/finders.py index d7b9457..d9a5cb4 100644 --- a/django_plotly_dash/finders.py +++ b/django_plotly_dash/finders.py @@ -33,9 +33,10 @@ from django.contrib.staticfiles.utils import get_files from django.core.files.storage import FileSystemStorage from django.conf import settings -from django.apps import apps #pylint: disable=unused-import +from django.apps import apps from django_plotly_dash.dash_wrapper import all_apps +from django_plotly_dash.util import full_asset_path class DashComponentFinder(BaseFinder): 'Find static files in components' @@ -104,9 +105,45 @@ class DashComponentFinder(BaseFinder): for component_name in self.locations: storage = self.storages[component_name] for path in get_files(storage, ignore_patterns + self.ignore_patterns): - print("DashAssetFinder", path, storage) yield path, storage +class DashAppDirectoryFinder(BaseFinder): + 'Find static fies in application subdirectories' + + def __init__(self): + # get all registered apps + + self.locations = [] + self.storages = OrderedDict() + + self.ignore_patterns = ["*.py", "*.pyc",] + + for app_config in apps.get_app_configs(): + + path_directory = os.path.join(app_config.path, 'assets') + + if os.path.isdir(path_directory): + + storage = FileSystemStorage(location=path_directory) + + storage.prefix = full_asset_path(app_config.name, "") + + self.locations.append(app_config.name) + self.storages[app_config.name] = storage + + super(DashAppDirectoryFinder, self).__init__() + + #pylint: disable=redefined-builtin + def find(self, path, all=False): + return [] + + def list(self, ignore_patterns): + for component_name in self.locations: + storage = self.storages[component_name] + for path in get_files(storage, ignore_patterns + self.ignore_patterns): + yield path, storage + + class DashAssetFinder(BaseFinder): 'Find static files in asset directories' @@ -114,27 +151,34 @@ class DashAssetFinder(BaseFinder): def __init__(self): - # Get all registered apps + # Ensure urls are loaded + root_urls = settings.ROOT_URLCONF + importlib.import_module(root_urls) - self.apps = all_apps() + # Get all registered django dash apps - self.subdir = 'assets' + self.apps = all_apps() self.locations = [] self.storages = OrderedDict() self.ignore_patterns = ["*.py", "*.pyc",] + added_locations = {} + for app_slug, obj in self.apps.items(): + caller_module = obj.caller_module location = obj.caller_module_location - path_directory = os.path.join(os.path.dirname(location), self.subdir) + subdir = obj.assets_folder + + path_directory = os.path.join(os.path.dirname(location), subdir) if os.path.isdir(path_directory): component_name = app_slug storage = FileSystemStorage(location=path_directory) - path = "dash/assets/%s" % component_name + path = full_asset_path(obj.caller_module.__name__,"") storage.prefix = path self.locations.append(component_name) @@ -151,3 +195,4 @@ class DashAssetFinder(BaseFinder): storage = self.storages[component_name] for path in get_files(storage, ignore_patterns + self.ignore_patterns): yield path, storage + diff --git a/django_plotly_dash/middleware.py b/django_plotly_dash/middleware.py index 89000ff..44feb8b 100644 --- a/django_plotly_dash/middleware.py +++ b/django_plotly_dash/middleware.py @@ -25,6 +25,8 @@ SOFTWARE. ''' +from .util import serve_locally + #pylint: disable=too-few-public-methods class EmbeddedHolder: @@ -98,3 +100,44 @@ class BaseMiddleware: response = request.dpd_content_handler.adjust_response(response) return response + + +# Bootstrap4 substitutions, if available +try: + from dpd_static_support.mappings import substitutions as dpd_ss_substitutions + substitutions += dpd_ss_substitutions +except Exception as e: + pass + + +class ExternalRedirectionMiddleware: + 'Middleware to force redirection in third-party content through rewriting' + + def __init__(self, get_response): + self.get_response = get_response + + substitutions = [] + + if serve_locally(): + substitutions += dpd_ss_substitutions + + self._encoding = "utf-8" + + self.substitutions = [(self._encode(source), + self._encode(target)) for source, target in substitutions] + + def __call__(self, request): + + response = self.get_response(request) + + content = response.content + + for source, target in self.substitutions: + content = content.replace(source, target) + + response.content = content + return response + + def _encode(self, string): + return string.encode(self._encoding) + diff --git a/django_plotly_dash/tests.py b/django_plotly_dash/tests.py index f63f1fb..eb67234 100644 --- a/django_plotly_dash/tests.py +++ b/django_plotly_dash/tests.py @@ -67,6 +67,14 @@ def test_demo_routing(): assert pipe_ws_endpoint_name() == 'ws/channel' assert insert_demo_migrations() +def test_local_serving(settings): + 'Test local serve settings' + + from django_plotly_dash.util import serve_locally, static_asset_root, full_asset_path + assert serve_locally() == settings.DEBUG + assert static_asset_root() == 'dpd/assets' + assert full_asset_path('fred.jim', 'harry') == 'dpd/assets/fred/jim/harry' + @pytest.mark.django_db def test_direct_access(client): 'Check direct use of a stateless application using demo test data' @@ -244,3 +252,32 @@ def test_argument_settings(settings, client): assert get_initial_arguments(None, None) is None assert store_initial_arguments(client, None) is None assert get_initial_arguments(client, None) is None + +def test_middleware_artifacts(): + 'Import and vaguely exercise middleware objects' + + from django_plotly_dash.middleware import EmbeddedHolder, ContentCollector + + eh = EmbeddedHolder() + eh.add_css("some_css") + eh.add_config("some_config") + eh.add_scripts("some_scripts") + + assert eh.config == 'some_config' + + cc = ContentCollector() + + assert cc._encode("fred") == b'fred' + +def test_finders(): + 'Import and vaguely exercise staticfiles finders' + + from django_plotly_dash.finders import DashComponentFinder, DashAppDirectoryFinder, DashAssetFinder + + dcf = DashComponentFinder() + dadf = DashAppDirectoryFinder() + daf = DashAssetFinder() + + assert dcf is not None + assert dadf is not None + assert daf is not None diff --git a/django_plotly_dash/urls.py b/django_plotly_dash/urls.py index 9bf179f..7e2a10e 100644 --- a/django_plotly_dash/urls.py +++ b/django_plotly_dash/urls.py @@ -27,7 +27,7 @@ SOFTWARE. from django.urls import path from django.views.decorators.csrf import csrf_exempt -from .views import routes, layout, dependencies, update, main_view, component_suites, component_component_suites +from .views import routes, layout, dependencies, update, main_view, component_suites, component_component_suites, asset_redirection from .app_name import app_name, main_view_label @@ -48,6 +48,7 @@ for base_type, args, name_prefix, url_ending, name_suffix in [('instance', {}, ' ('', main_view, main_view_label, '', ), ('_dash-component-suites', component_suites, 'component-suites', '/<slug:component>/<resource>', ), ('_dash-component-suites', component_component_suites, 'component-component-suites', '/<slug:component>/_components/<resource>', ), + ('assets', asset_redirection, 'asset-redirect', '/<path:path>', ), ]: route_name = '%s%s%s' % (name_prefix, name, name_suffix) diff --git a/django_plotly_dash/util.py b/django_plotly_dash/util.py index d180bd1..15fb9d7 100644 --- a/django_plotly_dash/util.py +++ b/django_plotly_dash/util.py @@ -27,6 +27,7 @@ import uuid from django.conf import settings from django.core.cache import cache +from django.contrib.staticfiles.templatetags.staticfiles import static def _get_settings(): try: @@ -95,3 +96,18 @@ def get_initial_arguments(request, cache_id=None): return cache.get(cache_id) return request.session[cache_id] + +def static_asset_root(): + return _get_settings().get('static_asset_root','dpd/assets') + +def full_asset_path(module_name, asset_path): + path_contrib = "%s/%s/%s" %(static_asset_root(), + "/".join(module_name.split(".")), + asset_path) + return path_contrib + +def static_asset_path(module_name, asset_path): + return static(full_asset_path(module_name, asset_path)) + +def serve_locally(): + return _get_settings().get('serve_locally', settings.DEBUG) diff --git a/django_plotly_dash/views.py b/django_plotly_dash/views.py index 814eed7..27148e1 100644 --- a/django_plotly_dash/views.py +++ b/django_plotly_dash/views.py @@ -27,6 +27,7 @@ SOFTWARE. import json from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import redirect from .models import DashApp from .util import get_initial_arguments @@ -117,8 +118,6 @@ def component_suites(request, resource=None, component=None, extra_element="", * else: redone_url = "/static/dash/component/%s/%s%s" %(component, extra_element, resource) - print("Redirecting to :", redone_url) - return HttpResponseRedirect(redirect_to=redone_url) def app_assets(request, **kwargs): @@ -145,3 +144,14 @@ def add_to_session(request, template_name="index.html", **kwargs): request.session['django_plotly_dash'] = django_plotly_dash return TemplateResponse(request, template_name, {}) + +def asset_redirection(request, path, ident=None, stateless=False, **kwargs): + 'Redirect static assets for a component' + + X, app = DashApp.locate_item(ident, stateless) + + # Redirect to a location based on the import path of the module containing the DjangoDash app + static_path = X.get_asset_static_url(path) + + return redirect(static_path) + diff --git a/docs/configuration.rst b/docs/configuration.rst index 3883afe..b1b01f8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -30,6 +30,9 @@ below. # Flag to control location of initial argument storage "cache_arguments": True, + + # Flag controlling local serving of assets + "serve_locally': settings.DEBUG, } Defaults are inserted for missing values. It is also permissible to not have any ``PLOTLY_DASH`` entry in @@ -44,10 +47,13 @@ file finders # Staticfiles finders for locating dash app assets and related files STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'django_plotly_dash.finders.DashAssetFinder', 'django_plotly_dash.finders.DashComponentFinder', + 'django_plotly_dash.finders.DashAppDirectoryFinder', ] and also providing a list of components used @@ -74,6 +80,41 @@ and also providing a list of components used This list should be extended with any additional components that the applications use, where the components have files that have to be served locally. +Furthermore, middleware should be added for redirection of external assets from +underlying packages, such as ``dash-bootstrap-components``. With the standard +Django middleware, along with ``whitenoise``, the entry within the ``settings.py`` +file will look something like + +.. code-block:: python + + # Standard Django middleware with the addition of both + # whitenoise and django_plotly_dash items + + MIDDLEWARE = [ + + 'django.middleware.security.SecurityMiddleware', + + 'whitenoise.middleware.WhiteNoiseMiddleware', + + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + + 'django_plotly_dash.middleware.BaseMiddleware', + 'django_plotly_dash.middleware.ExternalRedirectionMiddleware', + + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ] + + +Individual apps can set their ``serve_locally`` flag. However, it is recommended to use +the equivalent global ``PLOTLY_DASH`` setting to provide a common approach for all +static assets. See :ref:`local_assets` for more information on how local assets are configured +and served as part of the standard Django staticfiles approach, along with details on the +integration of other components and some known issues. + .. _endpoints: Endpoints diff --git a/docs/faq.rst b/docs/faq.rst index c6afa2f..7a2fcc1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -32,3 +32,28 @@ in this github `issue <https://github.com/GibbsConsulting/django-plotly-dash/iss Yes. See the :ref:`view_decoration` configuration setting and :ref:`access_control` section. +* What settings are needed to run the server in debug mode? + +The ``prepare_demo`` script in the root of the git repository contains the full set of commands +for running the server in debug mode. In particular, the debug server is launched with the ``--nostatic`` option. This +will cause the staticfiles to be served from the collected files in the ``STATIC_ROOT`` location rather than the normal +``runserver`` behaviour of serving directly from the various +locations in the ``STATICFILES_DIRS`` list. + +* Is use of the ``get_asset_url`` function optional for including static assets? + +No, it is needed. Consider this example (it is part of ``demo-nine``): + +.. code-block:: python + + localState = DjangoDash("LocalState", + serve_locally=True) + + localState.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')), + html.Img(src='/assets/image_two.png'), + ]) + +The first ``Img`` will have its source file correctly served up by Django as a standard static file. However, the second image will +not be rendered as the path will be incorrect. + +See the :ref:`local_assets` section for more information on `configuration` with local assets. diff --git a/docs/index.rst b/docs/index.rst index 5776165..cb77a7d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ Contents template_tags dash_components configuration + local_assets demo_notes access_control faq diff --git a/docs/installation.rst b/docs/installation.rst index cf4d554..2a75a19 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -17,12 +17,17 @@ Then, add ``django_plotly_dash`` to ``INSTALLED_APPS`` in the Django ``settings. ... ] -Further, if the :ref:`header and footer <plotly_header_footer>` tags are in use -then ``django_plotly_dash.middleware.BaseMiddleware`` should be added to ``MIDDLEWARE`` in the same file. This can be safely added now even if not used. - The project directory name ``django_plotly_dash`` can also be used on its own if preferred, but this will stop the use of readable application names in the Django admin interface. +Further, if the :ref:`header and footer <plotly_header_footer>` tags are in use +then ``django_plotly_dash.middleware.BaseMiddleware`` should be added to ``MIDDLEWARE`` in the same file. This +can be safely added now even if not used. + +If assets are being served locally through the use of the global ``serve_locally`` or on a per-app basis, then +``django_plotly_dash.middleware.ExternalRedirectionMiddleware`` should be added, along with the ``whitenoise`` package whose +middleware should also be added as per the instructions for that package. + The application's routes need to be registered within the routing structure by an appropriate ``include`` statement in a ``urls.py`` file:: diff --git a/docs/local_assets.rst b/docs/local_assets.rst new file mode 100644 index 0000000..137f0c2 --- /dev/null +++ b/docs/local_assets.rst @@ -0,0 +1,65 @@ +.. _local_assets: + +Local assets +============ + +Local ploty dash assets are integrated into the standard Django staticfiles structure. This requires additional +settings for both staticfiles finders and middleware, and also providing a list of the components used. The +specific steps are listed in the :ref:`configuration` section. + +Individual applications can set a ``serve_locally`` flag but the use of the global setting in the ``PLOTLY_DASH`` +variable is recommended. + +Additional components +--------------------- + +Some components, such as ``dash-bootstrap-components``, require external packages such as Bootstrap to be supplied. In +turn this can be achieved using for example the ``bootstrap4`` Django application. As a consequence, dependencies on +external URLs are introduced. + +This can be avoided by use of the ``dpd-static-support`` package, which supplies mappings to locally served versions of +these assets. Installation is through the standard ``pip`` approach + +.. code-block:: bash + + pip install dpd-static-support + +and then the package should be added as both an installed app and to the ``PLOTLY_COMPONENTS`` list +in ``settings.py``, along with the associated middleware + +.. code-block:: python + + INSTALLED_APPS = [ + ... + 'dpd_static_support', + ] + + MIDDLEWARE = [ + ... + 'django_plotly_dash.middleware.ExternalRedirectionMiddleware', + ] + + PLOTLY_COMPONENTS = [ + ... + 'dpd_static_support' + ] + +Note that the middleware can be safely added even if the ``serve_locally`` functionality is not in use. + +Known issues +------------ + +Absolute paths to assets will not work correctly. For example: + +.. code-block:: python + + app.layout = html.Div([html.Img(src=localState.get_asset_url('image_one.png')), + html.Img(src='assets/image_two.png'), + html.Img(src='/assets/image_three.png'), + ]) + +Of these three images, both ``image_one.png`` and ``image_two.png`` will be served up - through the static files +infrastructure - from the ``assets`` subdirectory relative to the code defining the ``app`` object. However, when +rendered the application will attempt to load ``image_three.png`` using an absolute path. This is unlikely to +be the desired result, but does permit the use of absolute URLs within the server. + diff --git a/prepare_demo b/prepare_demo index 5023168..0eec48b 100755 --- a/prepare_demo +++ b/prepare_demo @@ -5,4 +5,7 @@ cd demo ./manage.py migrate ./manage.py shell < configdb.py # Add a superuser if needed ./manage.py collectstatic -i "*.py" -i "*.pyc" --noinput --link -./manage.py runserver +# +# Run debug server. Use the nostatic flag to enable the use of whitenose rather than the standard Django debug handling +# +./manage.py runserver --nostatic diff --git a/requirements.txt b/requirements.txt index 3783c07..baf08da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -dash -dash-core-components -dash-html-components -dash-renderer +dash==0.38 +dash-core-components==0.43.1 +dash-html-components==0.13.5 +dash-renderer==0.19 plotly dpd-components dash-bootstrap-components -Django>=2 +Django>=2,<2.2 Flask>=1.0.2 -- GitLab