django-string-renderer
Render Django templates written by the users of your site, typically in a rich text editor such as TinyMCE: e-mails, notifications, page snippets...
Rendering a string with Django takes two lines. Rendering a string that a non-developer wrote in a WYSIWYG editor is another story: the editor escapes < and >, inserts non-breaking spaces, wraps tags in formatting or in paragraphs, and a broken template must not break the page or the e-mail that displays it. This package takes care of that:
- comparisons that survive rich text editors, which escape
<and>:{% if recipient.age >= 18 %}works when stored as{% if recipient.age >= 18 %}; - automatic cleanup of the artifacts left by editors inside template tags;
- errors rendered as a readable message instead of an exception, or handled the way you choose;
- a validator for form and model fields, which reports the line of the error;
- optional allowlists of template tags and filters;
- a recommended TinyMCE configuration, checked against what TinyMCE actually stores.
Requirements
This package is tested against Python 3.10 to 3.14 and Django 5.2 to 6.1. It uses no models, and requires no other settings than a django engine in the TEMPLATES setting.
Installation
pip install django-string-renderer
Then add stringrenderer to your INSTALLED_APPS setting, which provides the translations of its messages:
INSTALLED_APPS = [
...,
'stringrenderer',
]
Usage
Build a renderer from a string, then render it with the context of your choice:
from stringrenderer import StringTemplateRenderer
renderer = StringTemplateRenderer("Hello {{ recipient.first_name }} {{ recipient.last_name }}!")
rendered_content = renderer.render_template(context=dict(recipient=recipient_1), request=request)
rendered_content = renderer.render_template(context=dict(recipient=recipient_2))
The template is built from the string the first time it is checked or rendered, and cached by the renderer for the next renderings. It is built again when an attribute of the renderer, such as template_string, is changed.
Check the syntax of a template, with the same arguments as the renderer:
from stringrenderer import check_template_syntax
is_valid, syntax_error = check_template_syntax("{% if recipient.age -gte 18 %}Adult{% endif %}")
The error is the one that prevents building the template, usually a TemplateSyntaxError, or None when the template is valid.
Or validate a form or model field:
from django.db import models
from stringrenderer.validators import TemplateSyntaxValidator
class Newsletter(models.Model):
subject = models.CharField(max_length=200, validators=[TemplateSyntaxValidator(auto_escape=False)])
body = models.TextField(validators=[TemplateSyntaxValidator(extra_tags=['i18n'])])
The validator takes the arguments of the renderer, which must match the ones used to render the field; invalid arguments raise an error when the validator is created, and the configuration errors, such as an unknown tag library in extra_tags, when a value is validated. Any error that prevents building the template makes the value invalid, since Django raises other errors than TemplateSyntaxError for some invalid templates, e.g. a StopIteration for {% if _("x %}; their messages are replaced by a generic one. The error message gives the line of the error when it is known: This template is not valid (line 3): Unclosed tag on line 3: 'if'. Looking for one of: elif, else, endif.
Arguments
| Argument | Default | Effect |
|---|---|---|
extra_tags |
None |
Names of the template tag libraries loaded before the template, e.g. ['i18n']. |
auto_escape |
True |
Whether the variables are escaped. Use False for plain text, such as the subject of an e-mail. |
engine_name |
'django' |
Name of the template engine in the TEMPLATES setting. |
spaceless |
True |
Whether the whitespace between HTML tags is removed, like the spaceless tag does. Beware that <b>John</b> <i>Doe</i> is then rendered as "JohnDoe". |
unescape_quotes_in_text |
True |
The HTML entities of quotes (" and ') are always replaced by quotes inside the template tags. Whether they are also replaced in the rest of the template, which breaks the HTML attributes that contain escaped quotes. |
remove_block_tag_paragraphs |
False |
Whether the paragraphs that contain nothing but block tags producing no output (e.g. <p>{% if ... %}</p>, <p>{% endfor %}</p> or <p>{% ... as variable %}</p>) are replaced by their tags, so that no empty paragraphs are rendered. The tags of a block, e.g. {% if %}, {% else %} and {% endif %}, are moved out of their paragraphs only if none of them stays inside a paragraph, so that the paragraphs stay balanced. |
on_error |
None |
What to do when the template cannot be built or rendered, see Errors. |
allowed_tags |
None |
Names of the template tags that the template may use, as a list or a set, see Restricting tags and filters. |
allowed_filters |
None |
Names of the filters that the template may use, as a list or a set. |
The defaults keep the behavior of the previous versions. For a new project, spaceless=False, unescape_quotes_in_text=False and remove_block_tag_paragraphs=True are better choices for templates written in a rich text editor.
Errors
By default, a template that cannot be built or rendered is rendered as an HTML error message, unless the DEBUG setting is true, in which case the error is raised. The on_error argument changes this:
on_error |
Behavior |
|---|---|
None |
HTML error message, or the error is raised when DEBUG is true. |
'raise' |
The error is always raised. |
'html' |
HTML error message, even when DEBUG is true. |
'text' |
Plain text error message, which is not marked as safe. |
| A callable | Called with the error; its result is rendered. |
The error messages are escaped, and never mention the tags that the renderer adds around the template. The configuration errors, such as an unknown engine_name or an unknown tag library in extra_tags, are always raised.
Restricting tags and filters
The authors of the templates may not need every tag and filter, and some of them give access to more than intended: {% include %} renders any template of the site, and |safe disables escaping. The allowed_tags and allowed_filters arguments restrict them:
renderer = StringTemplateRenderer(
template_string,
allowed_tags=['if', 'for', 'with', 'firstof', 'now'],
allowed_filters=['date', 'default', 'length', 'lower', 'upper', 'title'],
)
The closing and intermediate tags, such as endif or else, do not need to be listed, unlike the tags of the libraries loaded with extra_tags. A template that uses another tag or filter cannot be built: the error is handled like any syntax error, and reported by check_template_syntax() and by the validator.
Keep in mind that the variables of a template can call the methods of the objects of its context that take no arguments (except the ones marked with alters_data). Give the templates written by users a context that only holds what they may access.
Templates written in a rich text editor
Comparison operators
Rich text editors escape the < and > characters, even inside template tags: {% if recipient.age >= 18 %} is stored as {% if recipient.age >= 18 %}, which Django cannot parse. The renderer decodes them inside the block tags (see Automatic cleanup), so the comparisons can be typed as usual.
These operators can also be used, in any block tag, and are not escaped by editors:
| Operator | Meaning |
|---|---|
-lt |
< |
-lte |
<= |
-gt |
> |
-gte |
>= |
{% if recipient.age is None or recipient.age -lt 18 %}Please ask your parents to confirm your participation.{% endif %}
Automatic cleanup
When a template cannot be built, the renderer cleans its tags from the artifacts of rich text editors, and builds it again. Inside the tags, outside of the string literals, the cleanup:
- decodes the escaped comparison characters (
<and>) in the block tags; - replaces the comparison operators above, even when the same one is used twice, or after a string literal;
- replaces the non-breaking spaces by spaces, and removes the invisible characters;
- removes the HTML tags added by formatting, e.g.
{{ <strong>recipient.first_name</strong> }}, and replaces the line breaks and the other HTML tags by spaces, e.g.{% firstof recipient.nickname<br>recipient.first_name %}; - replaces the quotes around string literals by straight ones, whether typographic or stored as HTML entities, e.g.
{% if recipient.first_name == “John” %},{% if recipient.first_name == “John” %}or{% if recipient.first_name == 'John' %}, and keeps the content of the literals as is.
The text, the comments and the content of the verbatim blocks are never changed. Since the cleanup only applies to the templates that cannot be built otherwise, the templates that worked with the previous versions are rendered exactly as before.
Recommended TinyMCE configuration
TinyMCE alters some template code with its default configuration. With django-tinymce, use the following settings:
TINYMCE_DEFAULT_CONFIG = {
# Store characters as is, rather than as named HTML entities.
"entity_encoding": "raw",
# Do not rewrite URLs, which may contain template code.
"convert_urls": False,
"relative_urls": False,
"remove_script_host": False,
# ... the rest of your configuration.
}
Here is what TinyMCE 7.8 stores with its default configuration, compared to the recommended one:
| Content | Default configuration | Recommended configuration |
|---|---|---|
{% if recipient.first_name == "Hélène" %} |
{% if recipient.first_name == "Hélène" %}: the comparison is always false, without any error. |
Unchanged. |
<a href="https://example.com/events/?id={{ event.pk }}"> edited on https://example.com/notifications/add/ |
<a href="../../events/?id={{ event.pk }}">: a broken link in an e-mail. |
Unchanged. |
| Two spaces typed in a tag | , removed by the cleanup. |
A non-breaking space character, which Django handles. |
>= typed in a tag |
>=, decoded by the cleanup. |
>=, decoded by the cleanup. |
Do not use the protect option of TinyMCE to keep the template tags untouched: it hides them in the editor.
Run scripts/capture_tinymce_fixtures.py to capture what another version of TinyMCE stores; the tests use these captures.
Help for template authors
You may adapt this summary for the help of your site.
- Variables are written between double curly braces, and replaced by their value:
Hello {{ recipient.first_name }}!. - Filters change the display of a variable, after a vertical bar:
{{ event.start_date|date:"d.m.Y" }},{{ recipient.nickname|default:"friend" }}. - Conditions show content depending on a value:
{% if recipient.age -gte 18 %}Adult{% else %}Minor{% endif %}. Use<,<=,>and>=(or-lt,-lte,-gtand-gte) to compare values,==and!=to test equality, andand,or,notto combine conditions. - Loops repeat content for each item of a list:
{% for event in events %}{{ event.title }}{% empty %}No event.{% endfor %}. - The names of the variables and of the tags are case-sensitive, and are always in English.
Testing
Get the source code from GitHub, then run:
pip install -r requirements.txt -r requirements-dev.txt
python runtests.py
This runs the test suite and checks the type annotations with mypy, with the locally installed versions of Python and Django.
The tests check that every template that version 0.5.0 could build is still prepared exactly the same way. You can run this check against the templates of your own project, with its settings, so that its template tag libraries are available:
DJANGO_SETTINGS_MODULE=myproject.settings PYTHONPATH=.:/path/to/myproject python -m tests.compatibility corpus.json
where corpus.json holds a list of objects with a template key, and the auto_escape and extra_tags keys used to render it.
Projects Using this App
This app is used in the following projects:
- MyGym Web: a web platform for managing sports clubs. django-string-renderer is used to render the messages and emails addressed to the members.
Metadata
Release files for django-string-renderer 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_string_renderer-0.6.0.tar.gz | 31.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_string_renderer-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 50.5 kB
Release files / django_string_renderer-0.6.0.tar.gz
| Download URL | django_string_renderer-0.6.0.tar.gz |
|---|---|
| Size | 31.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0fff682024714474975656ae676d81ef0ff9b8be39746ac8f3e0897da8b7f13c
|
|
BLAKE2b-256 checksum How to use checksums |
3e579c1f4fe245ff7beaff0ff096b8aca9b977e26c0af3bed9afed5e59a44366
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / django_string_renderer-0.6.0-py3-none-any.whl
| Download URL | django_string_renderer-0.6.0-py3-none-any.whl |
|---|---|
| Size | 18.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
76bb230d4cb0e7ca25366c87240994d85cb08d0f072bc107a54f1184baecd439
|
|
BLAKE2b-256 checksum How to use checksums |
03db070253ab381a504210a31a29cd7fd76349c1b9641cbb1262e388ddb0a887
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log