Chapter 23 · What changed

23.7 What changed in templates

What changed in Django’s template language and in the helpers that build safe HTML, from 5.0 to this commit: template partials, {% querystring %}, {{ forloop.length }}, Library.simple_block_tag, the deprecated two dots in a variable, the scheme |urlize assumes, the create methods a template no longer calls, and format_html without arguments.

A template is compiled once into a tree of nodes, which renders with a context (Templates), and the changes fall at both ends. The parser gained a store a tag can fill for the compiled template, on which partials are built, and a warning for two dots in a variable; rendering gained {% querystring %} and a loop’s length, and stopped calling the methods of a queryset and of the user manager that create rows.

Template partials and the parser’s extra data

Since 6.0 a template can hold named fragments of itself. {% partialdef ship_row %} … {% endpartialdef %} defines one, which renders nothing where it stands unless the tag adds "inline"; {% partial ship_row %} renders it with the context at that point; and a name such as "fleet/port_detail.html#ship_row", given to Engine.get_template and so to render_to_string and {% include %}, loads the fragment alone, a PartialTemplate. The store they rely on came in 5.1: Parser.extra_data, a dictionary a compile function may fill, which Template.compile_nodelist keeps as Template.extra_data once the parse succeeds; {% partialdef %} files each fragment there under "partials". Template partials: {% partialdef %} and {% partial %} tells how a partial is made, found and rendered.

Compiling: two dots, and tags made from functions

In {{ port..name }} the lookup’s middle part is an empty name, tried as a key, an attribute and an index like any other. The release notes call this normally a mistake, and since 6.1 Parser.parse warns with RemovedInDjango2028Warning as it compiles a variable tag whose variable holds two dots in a row; a comment there says the warning becomes an error when the deprecation ends. The check is made only when the expression starts from a Variable (FilterExpression.is_var), so a string literal such as {{ "a..b" }}, translated or not, does not warn, since a fix in 6.1.1. The arguments of filters and tags are not checked: {{ x|default:port..name }} and {% if port..name %} compile without a warning. Variables and filters: FilterExpression and Variable tells the lookup.

Library.simple_block_tag (5.2) makes a tag with a body from a function: the compile function parses up to the end tag, and at each render the body, rendered in the same context, is passed as the function’s "content" argument. parse_bits, which matches a tag’s words against the function’s signature, no longer takes the takes_context flag (6.1): each decorator given takes_context checks for a first parameter "context" as it runs, and strikes it from the list it passes. Libraries: Library, custom tags and {% load %} tells both.

Rendering: {% querystring %}, the loop and "alters_data"

{% querystring %}, new in 5.1, writes a link’s query string from the request’s and the template’s values, so that a link to the next page keeps the rest of the query. Its positional arguments must be mappings, and with none the request’s HttpRequest.GET stands in; each mapping, then the keyword arguments, is applied in order to an empty QueryDict, where None removes a key. 6.0 made the result begin with "?" whatever it holds, and accepted several positional mappings: on a request for "/ports/?q=brig&page=2", {% querystring page=3 %} renders "?q=brig&page=3", and {% querystring q=None page=None %} renders "?". The built-in tags has the tag, and {{ forloop.length }} (6.0), the sequence’s length, which ForNode.render sets before the first item. {% csp_nonce_attr %} (6.1) is told in What changed in security: Content Security Policy and signing.

A callable a variable’s lookup finds is not called if it carries a true "alters_data": the value becomes the engine’s "string_if_invalid", unless a true "do_not_call_in_templates", tested first, keeps the callable itself. In 5.2 the flag was set on QuerySet.create, QuerySet.bulk_create, QuerySet.get_or_create, QuerySet.update_or_create, UserManager.create_user, UserManager.create_superuser and their coroutine twins, to prevent side effects when a template is rendered, say the release notes. A manager’s copy of a queryset’s method carries it too, being made with functools.wraps: given Port’s manager as ports, {{ ports.create }} renders the empty string, and the table stays empty.

The function urlize, behind the filters |urlize and |urlizetrunc, links an address that begins "http://" or "https://", and a name with no scheme, such as "www.harbour.org" or one ending in one of the seven original top-level domains. Such a name still gets "http://", with a RemovedInDjango2028Warning since 6.0, unless the transitional setting URLIZE_ASSUME_HTTPS is true, which gives it "https://". Django 2028 gives every such name "https://" and removes the setting, which 6.0 deprecated with the change. Setting it raises no warning: the settings object warns for the other transitional settings, not this one. What still warns: the deprecations and the releases that end them lists it.

Table 23.3 The changes to the helpers of Autoescaping and safe strings.
name what changed release where now
escapeseq what changed a new filter: each item through conditional_escape, for a list joined where autoescaping is off release 5.0 where now escapeseq django/template/defaultfilters.py
format_html what changed given no arguments, it would escape nothing and return its string marked safe; deprecated, then a TypeError release 5.0, 6.0 where now format_html django/utils/html.py
format_html_join what changed an item that is a mapping is passed to format_html as keyword arguments release 5.2 where now format_html_join django/utils/html.py
SafeString.__add__ what changed returns NotImplemented for a right operand that is not a str, so the operand’s __radd__ is tried release 5.2 where now SafeString.__add__ django/utils/safestring.py

Removed, and the engines’ checks

The length_is filter was removed in 5.1, and a template that uses it now fails to compile with a TemplateSyntaxError that says "Invalid filter: 'length_is'" (Parser.find_filter, The built-in filters). Since 5.1 a template backend has BaseEngine.check, which returns no messages unless overridden, and the check check_templates calls it on every engine; DjangoTemplates.check reports a "string_if_invalid" that is not a string, "templates.E002", and a library name two modules use, "templates.W003" (check_templates, DjangoTemplates.check, Template engines: TEMPLATES and the backends).