Templates
Render HTML templates using Jinja2.
- Overview
- Template files
- Template context
- Built-in globals
- Built-in filters
- Custom globals and filters
- Custom template extensions
- Rendering templates manually
- Custom Jinja environment
- FAQs
- Forms
- Installation
Overview
Plain uses Jinja2 for template rendering. You can refer to the Jinja documentation for all of the features available.
Templates are typically used with TemplateView or one of its subclasses.
# app/views.py
from plain.views import TemplateView
class ExampleView(TemplateView):
template_name = "example.html"
def get_template_context(self):
context = super().get_template_context()
context["message"] = "Hello, world!"
return context
<!-- app/templates/example.html -->
{% extends "base.html" %}
{% block content %}
<h1>{{ message }}</h1>
{% endblock %}
Template files
Template files can live in two locations:
app/templates/- Your app's templates (highest priority){package}/templates/- Templates inside any installed package
All template directories are merged together, so you can override templates from installed packages by creating a file with the same name in app/templates/.
Template context
When using TemplateView, you pass data to templates by overriding get_template_context().
class ProductView(TemplateView):
template_name = "product.html"
def get_template_context(self):
context = super().get_template_context()
context["product"] = Product.objects.get(id=self.url_kwargs["id"])
context["related_products"] = Product.objects.filter(category=context["product"].category)[:5]
return context
The context is then available in your template:
<h1>{{ product.name }}</h1>
<ul>
{% for item in related_products %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
Built-in globals
Plain provides several global functions available in all templates:
| Global | Description |
|---|---|
asset(path) |
Returns the URL for a static asset |
url(name, *args, **kwargs) |
Reverses a URL by name |
Paginator |
The Paginator class for pagination |
now() |
Returns the current datetime |
timedelta |
The timedelta class for date math |
localtime(dt) |
Converts a datetime to local time |
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
<a href="{{ url('product_detail', id=product.id) }}">View</a>
<p>Generated at {{ now() }}</p>
Built-in filters
Plain includes several filters for common operations:
| Filter | Description |
|---|---|
strftime(format) |
Formats a datetime |
strptime(format) |
Parses a string to datetime |
fromtimestamp(ts) |
Creates datetime from timestamp |
fromisoformat(s) |
Creates datetime from ISO string |
localtime(tz) |
Converts to local timezone |
timeuntil |
Human-readable time until a date |
timesince |
Human-readable time since a date |
json_script(id) |
Outputs JSON safely in a script tag |
islice(stop) |
Slices iterables (useful for dicts) |
pluralize(singular, plural) |
Returns plural suffix based on count |
<p>Posted {{ post.created_at|timesince }} ago</p>
<p>{{ items|length }} item{{ items|length|pluralize }}</p>
<p>{{ 5 }} ox{{ 5|pluralize("en") }}</p>
{{ data|json_script("page-data") }}
Custom globals and filters
You can register your own globals and filters in app/templates.py (or {package}/templates.py). These files are automatically imported when the template environment loads.
# app/templates.py
from plain.templates import register_template_filter, register_template_global
@register_template_filter
def camel_case(value):
"""Convert a string to CamelCase."""
return value.replace("_", " ").title().replace(" ", "")
@register_template_global
def app_version():
"""Return the current app version."""
return "1.0.0"
Now you can use these in templates:
<p>{{ "my_variable"|camel_case }}</p> <!-- outputs: MyVariable -->
<footer>Version {{ app_version() }}</footer>
You can also register non-callable values as globals by providing a name:
from plain.templates import register_template_global
register_template_global("1.0.0", name="APP_VERSION")
Custom template extensions
For more complex template features, you can create Jinja extensions. The InclusionTagExtension base class makes it easy to create custom tags that render their own templates.
# app/templates.py
from plain.templates import register_template_extension
from plain.templates.jinja.extensions import InclusionTagExtension
from plain.runtime import settings
@register_template_extension
class AlertExtension(InclusionTagExtension):
tags = {"alert"}
template_name = "components/alert.html"
def get_context(self, context, *args, **kwargs):
return {
"message": args[0] if args else "",
"type": kwargs.get("type", "info"),
}
<!-- app/templates/components/alert.html -->
<div class="alert alert-{{ type }}">{{ message }}</div>
<!-- Usage in any template -->
{% alert "Something happened!" type="warning" %}
Rendering templates manually
You can render templates outside of views using the Template class.
from plain.templates import Template
html = Template("email/welcome.html").render({
"user_name": "Alice",
"activation_url": "https://example.com/activate/abc123",
})
If the template file doesn't exist, a TemplateFileMissing exception is raised.
Custom Jinja environment
By default, Plain uses a DefaultEnvironment that configures Jinja2 with sensible defaults:
- Autoescaping enabled for security
- StrictUndefined so undefined variables raise errors
- Auto-reload in debug mode
- Loop controls extension (
break,continue) - Debug extension
You can customize the environment by creating your own class and pointing to it in settings:
# app/jinja.py
from plain.templates.jinja.environments import DefaultEnvironment
class CustomEnvironment(DefaultEnvironment):
def __init__(self):
super().__init__()
# Add your customizations here
self.globals["CUSTOM_SETTING"] = "value"
# app/settings.py
TEMPLATES_JINJA_ENVIRONMENT = "app.jinja.CustomEnvironment"
FAQs
Why am I getting "undefined variable" errors?
Plain uses Jinja's StrictUndefined mode, which raises an error when you reference a variable that doesn't exist in the context. This helps catch typos and missing data early. Make sure you're passing all required variables in get_template_context().
Why does my template show an error about a callable?
Plain's template environment prevents accidentally rendering callables (functions, methods) directly. If you see an error like "X is callable, did you forget parentheses?", you probably need to add () to call the function:
<!-- Wrong -->
{{ user.get_full_name }}
<!-- Correct -->
{{ user.get_full_name() }}
How do I use Jinja's loop controls?
Plain enables the loopcontrols extension by default, so you can use break and continue in loops:
{% for item in items %}
{% if item.skip %}
{% continue %}
{% endif %}
{% if item.stop %}
{% break %}
{% endif %}
<p>{{ item.name }}</p>
{% endfor %}
Where can I learn more about Jinja2?
The Jinja2 documentation covers all the template syntax, including conditionals, loops, macros, and inheritance.
Forms
Forms are rendered manually using the bound field attributes:
<form method="post">
<div>
<label for="{{ form.email.html_id }}">Email</label>
<input
type="email"
name="{{ form.email.html_name }}"
id="{{ form.email.html_id }}"
value="{{ form.email.value }}"
>
{% for error in form.email.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
<button type="submit">Submit</button>
</form>
Each bound field provides: html_name, html_id, value, errors, field, initial.
Installation
The plain.templates module is included with Plain by default. No additional installation is required.