Skip to content

Commit

Permalink
Serve template CSS files per Document (#1479)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Sep 17, 2020
1 parent 2e0c2ae commit fd764ae
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 26 deletions.
28 changes: 24 additions & 4 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def js_files(self):
if any('ace' in jsf for jsf in js_files):
js_files.append('/panel_dist/post_require.js')
return js_files

def css_files(self):
from ..config import config
files = super(Resources, self).css_files
Expand All @@ -58,10 +58,30 @@ def css_files(self):
def conffilter(value):
return json.dumps(OrderedDict(value)).replace('"', '\'')

Resources.css_raw = property(css_raw)
Resources.js_files = property(js_files)
Resources.css_files = property(css_files)

class PanelResources(Resources):

def __init__(self, extra_css_files=None, **kwargs):
super(PanelResources, self).__init__(**kwargs)
self._extra_css_files = extra_css_files

@property
def css_raw(self):
raw = super(PanelResources, self).css_raw
for cssf in self._extra_css_files:
if not os.path.isfile(cssf):
continue
with open(cssf) as f:
css_txt = f.read()
if css_txt not in raw:
raw.append(css_txt)
return raw


_env = get_env()
_env.filters['json'] = lambda obj: Markup(json.dumps(obj))
_env.filters['conffilter'] = conffilter

Resources.css_raw = property(css_raw)
Resources.js_files = property(js_files)
Resources.css_files = property(css_files)
58 changes: 42 additions & 16 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@
from types import FunctionType, MethodType

from bokeh.document.events import ModelChangedEvent
from bokeh.embed.server import server_html_page_for_session
from bokeh.server.server import Server
from bokeh.server.views.session_handler import SessionHandler
from bokeh.server.views.static_handler import StaticHandler
from bokeh.server.urls import per_app_patterns
from bokeh.settings import settings
from tornado.websocket import WebSocketHandler
from tornado.web import RequestHandler, StaticFileHandler
from tornado.web import RequestHandler, StaticFileHandler, authenticated
from tornado.wsgi import WSGIContainer

from .resources import PanelResources
from .state import state


Expand Down Expand Up @@ -61,6 +67,39 @@ def _eval_panel(panel, server_id, title, location, doc):
doc = as_panel(panel)._modify_doc(server_id, title, doc, location)
return doc


class PanelDocHandler(SessionHandler):
"""
Implements a custom Tornado handler for document display page
overriding the default bokeh DocHandler to replace the default
resources with a Panel resources object.
"""

@authenticated
async def get(self, *args, **kwargs):
session = await self.get_session()

mode = settings.resources(default="server")
css_files = session.document.template_variables.get('template_css_files')
resource_opts = dict(mode=mode, extra_css_files=css_files)
if mode == "server":
resource_opts.update({
'root_url': self.application._prefix,
'path_versioner': StaticHandler.append_version
})
resources = PanelResources(**resource_opts)

page = server_html_page_for_session(
session, resources=resources, title=session.document.title,
template=session.document.template,
template_variables=session.document.template_variables
)

self.set_header("Content-Type", 'text/html')
self.write(page)

per_app_patterns[0] = (r'/?', PanelDocHandler)

#---------------------------------------------------------------------
# Public API
#---------------------------------------------------------------------
Expand Down Expand Up @@ -368,18 +407,9 @@ def do_stop(*args, **kwargs):
class StoppableThread(threading.Thread):
"""Thread class with a stop() method."""

def __init__(self, io_loop=None, timeout=1000, **kwargs):
from tornado import ioloop
def __init__(self, io_loop=None, **kwargs):
super(StoppableThread, self).__init__(**kwargs)
self._stop_event = threading.Event()
self.io_loop = io_loop
self._cb = ioloop.PeriodicCallback(self._check_stopped, timeout)
self._cb.start()

def _check_stopped(self):
if self.stopped:
self._cb.stop()
self.io_loop.stop()

def run(self):
if hasattr(self, '_target'):
Expand All @@ -400,8 +430,4 @@ def run(self):
del self._Thread__target, self._Thread__args, self._Thread__kwargs

def stop(self):
self._stop_event.set()

@property
def stopped(self):
return self._stop_event.is_set()
self.io_loop.add_callback(self.io_loop.stop)
27 changes: 21 additions & 6 deletions panel/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=Tr
else:
doc.template = self.template
doc._template_variables.update(self._render_variables)
doc._template_variables['template_css_files'] = css_files = (
doc._template_variables.get('template_css_files', [])
)
for cssf in self._css_files:
css_files.append(str(cssf))
return doc

def _repr_mimebundle_(self, include=None, exclude=None):
Expand Down Expand Up @@ -201,6 +206,10 @@ def _repr_mimebundle_(self, include=None, exclude=None):

return render_template(doc, comm, manager)

@property
def _css_files(self):
return []

#----------------------------------------------------------------
# Public API
#----------------------------------------------------------------
Expand Down Expand Up @@ -236,6 +245,7 @@ def save(self, filename, title=None, resources=None, embed=False,
"""
if embed:
raise ValueError("Embedding is not yet supported on Template.")

return save(self, filename, title, resources, self.template,
self._render_variables, embed, max_states, max_opts,
embed_json, json_prefix, save_path, load_path)
Expand Down Expand Up @@ -320,8 +330,6 @@ class BasicTemplate(BaseTemplate):
__abstract = True

def __init__(self, **params):
if self._css and self._css not in config.css_files:
config.css_files.append(self._css)
template = self._template.read_text()
if 'header' not in params:
params['header'] = ListLike()
Expand All @@ -330,17 +338,24 @@ def __init__(self, **params):
if 'sidebar' not in params:
params['sidebar'] = ListLike()
super(BasicTemplate, self).__init__(template=template, **params)
if self.theme:
theme = self.theme.find_theme(type(self))
if theme and theme.css and theme.css not in config.css_files:
config.css_files.append(theme.css)
self._update_vars()
self.main.param.watch(self._update_render_items, ['objects'])
self.sidebar.param.watch(self._update_render_items, ['objects'])
self.header.param.watch(self._update_render_items, ['objects'])
self.param.watch(self._update_vars, ['title', 'header_background',
'header_color'])

@property
def _css_files(self):
css_files = []
if self._css and self._css not in config.css_files:
css_files.append(self._css)
if self.theme:
theme = self.theme.find_theme(type(self))
if theme and theme.css and theme.css not in config.css_files:
css_files.append(theme.css)
return css_files

def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=True):
doc = super(BasicTemplate, self)._init_doc(doc, comm, title, notebook, location)
if self.theme:
Expand Down
28 changes: 28 additions & 0 deletions panel/tests/test_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import time

from tempfile import NamedTemporaryFile

import pytest
import requests

Expand All @@ -10,6 +12,7 @@
from panel.models import HTML as BkHTML
from panel.pane import Markdown
from panel.io.server import StoppableThread
from panel.template import Template


def test_get_server(html_server_session):
Expand Down Expand Up @@ -73,6 +76,7 @@ def test_kill_all_servers(html_server_session, markdown_server_session):
assert server_1._stopped
assert server_2._stopped


def test_multiple_titles(multiple_apps_server_sessions):
"""Serve multiple apps with a title per app."""
session1, session2 = multiple_apps_server_sessions(
Expand All @@ -84,3 +88,27 @@ def test_multiple_titles(multiple_apps_server_sessions):
with pytest.raises(KeyError):
session1, session2 = multiple_apps_server_sessions(
slugs=('app1', 'app2'), titles={'badkey': 'APP1', 'app2': 'APP2'})


def test_template_css():
t = Template("{% extends base %}")
t.add_panel('A', 1)
css = ".test { color: 'green' }"
ntf = NamedTemporaryFile()
with open(ntf.name, 'w') as f:
f.write(css)
t.add_variable('template_css_files', [ntf.name])

loop = IOLoop()
server = StoppableThread(
target=t._get_server, io_loop=loop,
args=(5009, None, None, loop, False, True, None, False, None)
)
server.start()

# Wait for server to start
time.sleep(1)

r = requests.get("http://localhost:5009/")
assert css in r.content.decode('utf-8')
server.stop()

0 comments on commit fd764ae

Please sign in to comment.