Skip to content

Commit

Permalink
Merge pull request #724 from yuvipanda/profile_template
Browse files Browse the repository at this point in the history
Allow building more complex profile_list templates
  • Loading branch information
consideRatio committed May 17, 2023
2 parents 0c837e2 + 63dc6cc commit 8766d36
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 70 deletions.
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ repos:
rev: v3.0.0-alpha.9-for-vscode
hooks:
- id: prettier
# Don't run prettier on our jinja2 template files. We run djlint instead
exclude: "kubespawner/templates/.*\\.html"

# Misc...
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand All @@ -59,6 +61,17 @@ repos:
hooks:
- id: flake8

# Lint our jinja2 templates
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.24.0
hooks:
- id: djlint-jinja
files: "kubespawner/templates/.*\\.html"
types_or:
- html
args:
- --reformat

# pre-commit.ci config reference: https://pre-commit.ci/#configuration
ci:
autoupdate_schedule: monthly
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include LICENSE
include README.md
graft kubespawner/templates
110 changes: 40 additions & 70 deletions kubespawner/spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from urllib.parse import urlparse

import escapism
from jinja2 import BaseLoader, Environment
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PackageLoader
from jupyterhub.spawner import Spawner
from jupyterhub.traitlets import Callable, Command
from jupyterhub.utils import exponential_backoff, maybe_future
Expand Down Expand Up @@ -1474,83 +1474,41 @@ def _validate_image_pull_secrets(self, proposal):
""",
)

profile_form_template = Unicode(
"""
<style>
/*
.profile divs holds two div tags: one for a radio button, and one
for the profile's content.
*/
#kubespawner-profiles-list .profile {
display: flex;
flex-direction: row;
font-weight: normal;
border-bottom: 1px solid #ccc;
padding-bottom: 12px;
}
additional_profile_form_template_paths = List(
default=[],
help="""
Additional paths to search for jinja2 templates when rendering profile_form.
#kubespawner-profiles-list .profile .radio {
padding: 12px;
}
These directories will be searched before the default `templates/` directory
shipped with kubespawner with the default template.
/* .option divs holds a label and a select tag */
#kubespawner-profiles-list .profile .option {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 12px;
}
Any file named `form.html` in these directories will be used to render the
profile options form.
""",
config=True,
)

#kubespawner-profiles-list .profile .option label {
font-weight: normal;
margin-right: 8px;
min-width: 96px;
}
</style>
<div class='form-group' id='kubespawner-profiles-list'>
{%- for profile in profile_list %}
{#- Wrap everything in a <label> so clicking anywhere selects the option #}
<label for='profile-item-{{ profile.slug }}' class='profile'>
<div class='radio'>
<input type='radio' name='profile' id='profile-item-{{ profile.slug }}' value='{{ profile.slug }}' {% if profile.default %}checked{% endif %} />
</div>
<div>
<h3>{{ profile.display_name }}</h3>
{%- if profile.description %}
<p>{{ profile.description }}</p>
{%- endif %}
{%- if profile.profile_options %}
<div>
{%- for k, option in profile.profile_options.items() %}
<div class='option'>
<label for='profile-option-{{profile.slug}}-{{k}}'>{{option.display_name}}</label>
<select name="profile-option-{{profile.slug}}-{{k}}" class="form-control">
{%- for k, choice in option['choices'].items() %}
<option value="{{ k }}" {% if choice.default %}selected{%endif %}>{{ choice.display_name }}</option>
{%- endfor %}
</select>
</div>
{%- endfor %}
</div>
{%- endif %}
</div>
</label>
{%- endfor %}
</div>
""",
config=True,
help="""
Jinja2 template for constructing profile list shown to user.
profile_form_template = Unicode(
"",
config=True,
help="""
Literal Jinja2 template for constructing profile list shown to user.
Used when `profile_list` is set.
The contents of `profile_list` are passed in to the template.
This should be used to construct the contents of a HTML form. When
posted, this form is expected to have an item with name `profile` and
the value the index of the profile in `profile_list`.
When this traitlet is not set, the default template `form.html` from the
directory `kubespawner/templates` is used. Admins can override this by
setting the `additional_profile_form_template_paths` config to a directory
with jinja2 templates, and any file named `form.html` in there will be used
instead of the default.
Using additional_profile_form_template_paths is recommended instead of
this.
""",
)

Expand Down Expand Up @@ -2951,9 +2909,21 @@ def _env_keep_default(self):

def _render_options_form(self, profile_list):
self._profile_list = self._init_profile_list(profile_list)
profile_form_template = Environment(loader=BaseLoader).from_string(
self.profile_form_template

loader = ChoiceLoader(
[
FileSystemLoader(self.additional_profile_form_template_paths),
PackageLoader("kubespawner", "templates"),
]
)

env = Environment(loader=loader)
if self.profile_form_template != "":
# Admin has custom set the profile_form_template as a templated string
# so we use that directly
profile_form_template = env.from_string(self.profile_form_template)
else:
profile_form_template = env.get_template("form.html")
return profile_form_template.render(profile_list=self._profile_list)

async def _render_options_form_dynamically(self, current_spawner):
Expand Down
33 changes: 33 additions & 0 deletions kubespawner/templates/form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<style>{% include 'style.css' %}</style>
<div class="form-group" id="kubespawner-profiles-list">
{%- for profile in profile_list %}
{#- Wrap everything in a label tag so clicking anywhere selects the option #}
<label for="profile-item-{{ profile.slug }}" class="profile">
<div class="radio">
<input type="radio"
name="profile"
id="profile-item-{{ profile.slug }}"
value="{{ profile.slug }}"
{% if profile.default %}checked{% endif %} />
</div>
<div>
<h3>{{ profile.display_name }}</h3>
{%- if profile.description %}<p>{{ profile.description }}</p>{%- endif %}
{%- if profile.profile_options %}
<div>
{%- for k, option in profile.profile_options.items() %}
<div class="option">
<label for="profile-option-{{ profile.slug }}-{{ k }}">{{ option.display_name }}</label>
<select name="profile-option-{{ profile.slug }}-{{ k }}" class="form-control">
{%- for k, choice in option['choices'].items() %}
<option value="{{ k }}" {% if choice.default %}selected{% endif %}>{{ choice.display_name }}</option>
{%- endfor %}
</select>
</div>
{%- endfor %}
</div>
{%- endif %}
</div>
</label>
{%- endfor %}
</div>
29 changes: 29 additions & 0 deletions kubespawner/templates/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
.profile divs holds two div tags: one for a radio button, and one
for the profile's content.
*/
#kubespawner-profiles-list .profile {
display: flex;
flex-direction: row;
font-weight: normal;
border-bottom: 1px solid #ccc;
padding-bottom: 12px;
}

#kubespawner-profiles-list .profile .radio {
padding: 12px;
}

/* .option divs holds a label and a select tag */
#kubespawner-profiles-list .profile .option {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 12px;
}

#kubespawner-profiles-list .profile .option label {
font-weight: normal;
margin-right: 8px;
min-width: 96px;
}
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ Documentation = "https://jupyterhub-kubespawner.readthedocs.io"
Source = "https://github.com/jupyterhub/kubespawner"
Issues = "https://github.com/jupyterhub/kubespawner/issues"

# Explicitly include our profile_list templates, as hatch doesn't
# respect MANIFEST.in.
# Documentation: https://hatch.pypa.io/latest/config/build/#artifacts
[tool.hatch.build]
artifacts = [
"kubespawner/templates/*"
]

# black is used for autoformatting Python code
#
Expand Down Expand Up @@ -121,3 +128,11 @@ tag_template = "{new_version}"

[[tool.tbump.file]]
src = "kubespawner/_version.py"

# djlint is used for autoformatting jinja templates
#
# ref: https://www.djlint.com/docs/formatter/
#
[tool.djlint]
indent = 2
profile = "jinja"

0 comments on commit 8766d36

Please sign in to comment.