Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serve template CSS files per Document #1479

Merged
merged 3 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()