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

Install function for custom templates, template docs update #1250

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
101 changes: 15 additions & 86 deletions docs/source/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,102 +106,31 @@ A few example voila/nbconvert template projects are:
* https://github.com/voila-dashboards/voila-vuetify


Where are Voilà templates located?
----------------------------------
Install a custom template
-------------------------

All Voilà templates are stored as folders with particular configuration/template files inside.
These folders can exist in the standard Jupyter configuration locations, in a folder called ``voila/templates``.
For example:
Suppose you created a custom template called ``mytemplate``, defined in a set of
directories located on your machine at ``/path/to/custom/``.
You can install the custom template for use with Voilà like so:

.. code-block:: bash

~/.local/share/jupyter/voila/templates
~/path/to/env/dev/share/jupyter/voila/templates
/usr/local/share/jupyter/voila/templates
/usr/share/jupyter/voila/templates

Voilà will search these locations for a folder, one per template, where
the folder name defines the template name.

The Voilà template structure
----------------------------

Within each template folder, you can provide your own nbconvert templates, static
files, and HTML templates (for pages such as a 404 error). For example, here is
the folder structure of the base Voilà template (called "default"):

.. code-block:: bash
.. code-block:: python

tree path/to/env/share/jupyter/voila/templates/default/
├── nbconvert_templates
│   ├── base.tpl
│   └── voila.tpl
└── templates
├── 404.html
├── error.html
├── page.html
└── tree.html
from voila.paths import install_custom_template

**To customize the nbconvert template**, store it in a folder called ``templatename/nbconvert_templates/voila.tpl``.
In the case of the default template, we also provide a ``base.tpl`` that our custom template uses as a base.
The name ``voila.tpl`` is special - you cannot name your custom nbconvert something else.
custom_template_name = 'mytemplate'
share_path = '/path/to/custom/'

**To customize the HTML page templates**, store them in a folder called ``templatename/templates/<name>.html``.
These are files that Voilà can serve as standalone HTML (for example, the ``tree.html`` template defines how
folders/files are displayed in ``localhost:8866/voila/tree``). You can override the defaults by providing your
own HTML files of the same name.
install_custom_template(share_path, custom_template_name)

**To configure your Voilà template**, you should add a ``config.json`` file to the root of your template
folder.
This function will try to symlink (preferred) or copy (fallback option) the
directories defining ``mytemplate`` to the paths where voilà keeps other
templates. ``share_path`` should contain the directories
``share/jupyter/nbconvert/templates/mytemplate`` and
``share/jupyter/voila/templates/mytemplate``.

.. todo: Add information on config.json


An example custom template
--------------------------

To show how to create your own custom template, let's create our own nbconvert template.
We'll have two goals:

1. Add an ``<h1>`` header displaying "Our awesome template" to the Voilà dashboard.
2. Add a custom 404.html page that displays an image.

First, we'll create a folder in ``~/.local/share/jupyter/voila/templates`` called ``mytemplate``::

mkdir ~/.local/share/jupyter/voila/templates/mytemplate
cd ~/.local/share/jupyter/voila/templates/mytemplate

Next, we'll copy over the base template files for Voilà, which we'll modify::

cp -r path/to/env/share/jupyter/voila/templates/default/nbconvert_templates ./
cp -r path/to/env/share/jupyter/voila/templates/default/templates ./

We should now have a folder structure like this:

.. code-block:: bash

tree .
├── nbconvert_templates
│   ├── base.tpl
│   └── voila.tpl
└── templates
├── 404.html
├── error.html
├── page.html
└── tree.html

Now, we'll edit ``nbconvert_templates/voila.tpl`` to include a custom H1 header.

As well as ``templates/tree.html`` to include an image.

Finally, we can tell Voilà to use this custom template the next time we use it on
a Jupyter notebook by using the name of the folder in the ``--template`` parameter::

voila mynotebook.ipynb --template=mytemplate


The result should be a Voilà dashboard with your custom modifications made!

Voilà template cookiecutter
-----------------------------

Expand Down
131 changes: 128 additions & 3 deletions voila/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import os
import json
from pathlib import Path
import shutil

from jupyter_core.paths import jupyter_path
import nbconvert.exporters.templateexporter
Expand Down Expand Up @@ -93,16 +95,21 @@ def collect_paths(
return paths


def _default_root_dirs():
def _default_root_dirs(root_first=True):
# We look at the usual jupyter locations, and for development purposes also
# relative to the package directory (first entry, meaning with highest precedence)
root_dirs = []
if DEV_MODE:
root_dirs.append(os.path.abspath(os.path.join(ROOT, '..', 'share', 'jupyter')))
pkg_share_jupyter = os.path.abspath(os.path.join(ROOT, '..', 'share', 'jupyter'))
if DEV_MODE and root_first:
root_dirs.append(pkg_share_jupyter)
if nbconvert.exporters.templateexporter.DEV_MODE:
root_dirs.append(os.path.abspath(os.path.join(nbconvert.exporters.templateexporter.ROOT, '..', '..', 'share', 'jupyter')))

root_dirs.extend(jupyter_path())

if DEV_MODE and not root_first:
root_dirs.append(pkg_share_jupyter)

return root_dirs


Expand All @@ -129,3 +136,121 @@ def _find_template_hierarchy(app_names, template_name, root_dirs):
# if not specified, 'base' is assumed
template_name = 'base'
return template_names


def get_existing_template_dirs(app_names, template_name, root_first=False):
"""
Return a list of available template directories with
template name ``template_name``. ``app_names`` might be
``["nbconvert", "voila"]``, for example. If ``root_first``,
give priority to the current working directory in the search
for a ``share`` directory which will be the destination
for custom templates. If False, set lowest priority to the cwd.
"""
return [
d for d in collect_paths(
app_names,
template_name,
# only use a ``share`` dir in the cwd as a last resort:
root_dirs=_default_root_dirs(root_first=root_first)
)
if os.path.exists(d) and template_name in d
]


def install_custom_template(
share_path,
template_name,
reference_template_name='base',
try_symlink=True,
overwrite=False,
include_root_paths=False
):
"""
Make a custom template available for use with Voilà.

Generate copies of/symbolic links pointing towards custom template files,
which will be located in directories where Voilà expects them. By default,
the template will be copied/symlinked where the Voilà template named "base"
can be found on your machine.

Parameters
----------
share_path : path-like, str
Path to a ``share`` directory containing the custom template to be
installed. Custom templates will be organized in the subdirs:
``share/jupyter/nbconvert/templates/<template_name>`` and
``share/jupyter/voila/templates/<template_name>``.
template_name : str
Name of the custom template
reference_template_name : str (default is "base")
Name of a default Voilà template. This function will try to install the
custom template in the same location as this reference template.
try_symlink : bool
If True, try to make a symlink and fall back on making a copy. Otherwise,
make a copy.
overwrite : bool
Overwrite custom template directories with the same name if they
already exist.
include_root_paths : bool
If True, include the cwd in the search for the ``share`` directory
which will become the destination path for the custom template
directories.
"""
# search for existing custom template and "base" (default) template paths
# that may already be installed on this machine:
app_names = ['nbconvert', 'voila']
custom_template_dirs = get_existing_template_dirs(
app_names, template_name, include_root_paths=include_root_paths
)
reference_template_dirs = get_existing_template_dirs(
app_names, reference_template_name, include_root_paths=include_root_paths
)

# if there are existing jdaviz template dirs but overwrite=False, raise error:
if not overwrite and len(custom_template_dirs):
raise FileExistsError(
f"Existing files found at {custom_template_dirs} which "
f"would be overwritten, but overwrite=False."
)

prefix_targets = [
os.path.join(app_name, "templates") for app_name in app_names
]

if len(reference_template_dirs):
target_dir = Path(reference_template_dirs[0]).absolute().parents[2]
else:
raise FileNotFoundError(f"No {reference_template_name} template found for voila.")

for prefix_target in prefix_targets:
# Path to the source for the custom template to be installed
source = os.path.join(share_path, 'share', 'jupyter', prefix_target, template_name)
parent_dir_of_target = os.path.join(target_dir, prefix_target)
# Path to destination for new custom template
target = os.path.join(parent_dir_of_target, template_name)
abs_source = os.path.abspath(source)
try:
rel_source = os.path.relpath(abs_source, parent_dir_of_target)
except Exception:
# relpath does not work if source/target on different Windows disks.
try_symlink = False

try:
os.remove(target)
except Exception:
try:
shutil.rmtree(target) # Maybe not a symlink
except Exception:
pass

# Cannot symlink without relpath or Windows admin priv in some OS versions.
try:
if try_symlink:
print('making symlink:', rel_source, '->', target)
os.symlink(rel_source, target)
else:
raise OSError('just make copies')
except Exception:
print('making copy:', abs_source, '->', target)
shutil.copytree(abs_source, target)