CodeOath
← All posts
Python65 min total · 18 parts

Django Fundamentals: The ORM, Migrations, and Shipping a Real App

Part 11 of 18 · ~2 min

Templates: Rendering a Tool Without Trusting It

{% extends "base.html" %}

{% block content %}
  <h1>Available Tools</h1>
  <ul>
    {% for tool in tools %}
      {% include "catalog/_tool_card.html" %}
    {% empty %}
      <li>Nothing listed yet in your neighborhood.</li>
    {% endfor %}
  </ul>
{% endblock %}
{# catalog/_tool_card.html — one card, reused everywhere a tool shows up #}
<li>
  <strong>{{ tool.name }}</strong> — listed by {{ tool.owner.username }}
  <p>{{ tool.description|linebreaks }}</p>
  {% if user.is_authenticated %}
    <a href="{% url 'request-loan' tool.pk %}">Request to borrow</a>
  {% endif %}
</li>

There's no assignment operator inside a Django template, and no way to write a general Python expression either — on purpose, not as a missing feature. A decision that can't be expressed in the template language has nowhere to go but back into a view, which is a testable Python function; buried in a template, that same decision would only ever get exercised by clicking through the page by hand. {{ tool.owner.username }} reads as if . always means attribute access, but each segment is actually a small waterfall underneath: try it as a dictionary key first, fall back to a plain attribute, fall back again to a numeric index, and if whatever's finally found turns out to be callable, call it with no arguments before moving to the next segment.

extends vs. include, and why they're not interchangeable

{% extends %} gives every page sharing base.html one skeleton with page-specific blocks dropped in — the right tool when the question is "what does every page on this site have in common." {% include %} drops one template's fully rendered output into another verbatim — the right tool when the question is "this exact fragment, a tool card here, needs to look identical wherever it shows up." ToolShed's tool card gets included from the browse page, a member's own listings page, and search results; writing it three times instead would mean three places to remember to update the day it changes.

Escaping, and the one time you turn it off

{{ tool.description }}               {# auto-escaped — a listing containing <script> renders as literal text #}
{{ tool.description|linebreaks }}     {# newlines become <p>/<br> — still escaped first #}
{{ trusted_announcement|safe }}       {# opts OUT of escaping — only for content you actually trust #}

Every variable gets this escaping automatically, with no opt-in required — a neighbor who types <script>alert(1)</script> into the free-text description field on their tool listing gets those angle brackets converted to &lt; and &gt; on the way to the page, so the browser displays the text instead of running it. |safe is the one filter that reverses that by request, for a single value at a time, and it should only ever land on something ToolShed itself generated or that's already been through a separate sanitizing step — never on a field a visitor typed into directly.