Unit 3: Templates, Debugging and Testing

INT253 — Web Development In Python Using Django 11 min read

I. Orientation: The Presentation Layer in Django's MTV Architecture

Django (initial public release July 2005, from the Lawrence Journal-World newspaper) separates an application into Model, Template and View — its MTV variant of MVC. The template is the presentation layer: a text file (usually HTML, but also CSV, XML, plain text or email bodies) containing static markup plus placeholder syntax that a view fills with data at request time. The template engine is deliberately restricted in power so that presentation logic cannot silently become business logic.

Defining properties and conventions that later sections rely on:

  • Templates are text, not Python: DTL is not executed as Python. {% %} and {{ }} are parsed into a node tree and rendered against a context object.
  • The context is a dict-like mapping: Context({'name': 'Asha'}) or, more commonly, the plain dict passed as the third argument to render().
  • Templates live in a known search path: the DIRS list and the APP_DIRS flag inside the TEMPLATES setting in settings.py.
  • Convention for app templates: myapp/templates/myapp/detail.html — the app name is repeated inside templates/ to namespace the file and avoid collisions between apps.
  • Design philosophy: no arbitrary Python in templates, silent failure on missing variables (rendered as the empty string, or TEMPLATE_STRING_IF_INVALID), and automatic HTML escaping of variable output.
  • Alternative engine: Jinja2 is supported via django.template.backends.jinja2.Jinja2; DTL is the default DjangoTemplates backend.

II. Templates in Django — Definition, Setup and Rendering

A. Introduction to Templates in Django

A template decouples what data is shown from how it is shown, so designers can edit HTML without touching Python.

  • Problem solved: returning HttpResponse("<h1>" + title + "</h1>") from a view mixes markup with logic and breaks as soon as the page grows.
  • Engine configuration: the TEMPLATES setting controls discovery.
PYTHON
TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [BASE_DIR / 'templates'],   # project-wide templates
    'APP_DIRS': True,                   # also search <app>/templates/
    'OPTIONS': {'context_processors': [
        'django.template.context_processors.request',
        'django.contrib.auth.context_processors.auth',
    ]},
}]
  • Two-step lifecycle: compile (parse the file into nodes, done once and cached) then render (walk the nodes with a context). get_template() returns the compiled Template; .render(context) returns a string.
  • Context processors: callables that inject variables into every template — auth supplies user and perms, so {{ user.username }} works without the view passing it.

B. Creating Templates

Creating a template means placing a file on the search path and pointing a view at it.

  • Directory layout:
TEXT
blog/
├── templates/
│   └── blog/
│       ├── base.html
│       └── post_list.html
└── views.py
  • The render() shortcut: render(request, template_name, context) combines loading, rendering and wrapping in an HttpResponse with status 200.
PYTHON
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.filter(published=True).order_by('-created')
    return render(request, 'blog/post_list.html', {'posts': posts, 'heading': 'Latest'})
  • Class-based equivalent: ListView with template_name = 'blog/post_list.html' and context_object_name = 'posts'; its default template name is <app>/<model>_list.html.
  • Common failure: TemplateDoesNotExist — raised when the path string does not match a file under any searched directory; the debug page lists every directory tried, in order.

C. Working with Django Template Language (DTL)

DTL has exactly four constructs, and everything else is built from them.

  • Variables — {{ }}: {{ post.title }} resolves by dictionary lookup, then attribute lookup, then list-index lookup, then callable invocation (called with no arguments).
  • Tags — {% %}: control flow and logic, e.g. {% for %}, {% if %}, {% url %}, {% csrf_token %}. Some are paired ({% block %}…{% endblock %}), some standalone.
  • Filters — |: transform a value on output. {{ title|upper }}, {{ body|truncatewords:30 }}, {{ price|floatformat:2 }}, {{ created|date:"d M Y" }}, {{ name|default:"Guest" }}. Filters chain left to right: {{ body|striptags|truncatechars:80 }}.
  • Comments: {# inline #} and {% comment %}…{% endcomment %}; neither reaches the browser.
  • Autoescaping: <script> becomes &lt;script&gt; automatically. Override deliberately with {{ html|safe }} or {% autoescape off %} — only for content you trust, since it reopens XSS risk.

D. Using template tags

Tags are the executable vocabulary of a template; the most-used ones fall into four groups.

  • Flow control: {% if %}, {% for %}, {% with total=order.items.count %} (caches an expensive lookup under a name).
  • Structural: {% extends %}, {% block %}, {% include 'blog/_card.html' with post=p %}.
  • URLs and static files: {% url 'post_detail' pk=post.pk %} reverses a named route so hardcoded paths never appear in markup; {% load static %} then {% static 'css/site.css' %} builds the correct static URL.
  • Security: {% csrf_token %} is mandatory inside every POST form, otherwise the CSRF middleware returns HTTP 403.
  • Custom tags and filters: create myapp/templatetags/__init__.py and myapp/templatetags/blog_extras.py, then {% load blog_extras %}.
PYTHON
from django import template
register = template.Library()

@register.filter
def reading_time(text, wpm=200):
    return max(1, len(text.split()) // wpm)   # {{ post.body|reading_time }}

E. Django variables

Variable resolution follows a fixed, documented order and fails quietly.

  • Lookup order: for {{ a.b }} Django tries a['b'], then a.b, then a[b] as an integer index — first success wins.
  • Dot notation only: {{ items.0 }} gets the first element; {{ items[0] }} is a syntax error. Method calls take no arguments: {{ posts.count }}, never {{ posts.count() }}.
  • Silent failure: an unresolvable variable renders as '', so a typo produces a blank page region rather than an exception — a frequent source of "missing data" bugs.
  • Callables: a zero-argument method is invoked; one with required arguments cannot be called from a template — expose it as a @property on the model or compute it in the view.
  • Restricted names: variables cannot begin with an underscore, and attributes with alters_data = True (such as Model.delete) are refused.

F. for loop and if-else statements

These two tags carry almost all in-template logic.

DJANGO
{% for post in posts %}
  <article class="{% cycle 'odd' 'even' %}">
    <h2>{{ forloop.counter }}. {{ post.title }}</h2>
    {% if post.views > 1000 %}
      <span>Popular</span>
    {% elif post.views > 100 %}
      <span>Rising</span>
    {% else %}
      <span>New</span>
    {% endif %}
  </article>
{% empty %}
  <p>No posts yet.</p>
{% endfor %}
  1. {% for %} specifics: forloop.counter (1-based), forloop.counter0 (0-based), forloop.first, forloop.last, forloop.revcounter, and forloop.parentloop when nested. {% empty %} runs when the sequence is empty. {% for x in qs reversed %} iterates backwards. There is no break or continue.
  2. {% if %} specifics: supports and, or, not, ==, !=, <, >, <=, >=, in, not in, is, is not. Falsy values are '', 0, None, empty lists and empty querysets. Arithmetic is unavailable, so {% if a + b > 10 %} is invalid — compute in the view or use {{ a|add:b }}.

G. Dynamic Templates in Django

A dynamic template is one whose output depends on the request, the database or the user rather than on fixed markup.

  • URL parameter to context: path('post/<int:pk>/', views.detail)get_object_or_404(Post, pk=pk){{ post.title }}, giving one template for thousands of pages.
  • Querysets rendered lazily: the SQL query fires during rendering when the {% for %} tag first iterates the queryset, not when the view builds the dict.
  • Request-aware branching: {% if user.is_authenticated %} shows a logout link, {% else %} a login link.
  • Choosing a template at runtime: render(request, 'reports/%s.html' % report_type, ctx) — validate report_type against a whitelist, otherwise a crafted value becomes a path-traversal hole.
  • Reducing query cost: Post.objects.select_related('author') in the view stops {{ post.author.name }} from firing one extra query per row (the N+1 problem).

H. Working with Template inheritance

Inheritance replaces copy-pasted markup with a single skeleton that children override — DRY applied to HTML.

  • base.html defines the holes:
DJANGO
<!DOCTYPE html>
<html><head><title>{% block title %}My Blog{% endblock %}</title></head>
<body>
  {% include 'blog/_nav.html' %}
  <main>{% block content %}{% endblock %}</main>
</body></html>
  • Child fills them:
DJANGO
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} — {{ block.super }}{% endblock %}
{% block content %}<h1>{{ post.title }}</h1>{{ post.body|linebreaks }}{% endblock %}
  • Hard rules: {% extends %} must be the first tag in the file; a block name may appear only once per template; content outside a block in a child is discarded.
  • {{ block.super }}: renders the parent's version of the block, so a child can append to rather than replace it.
  • Three-level pattern: base.htmlbase_section.html (per section) → leaf page.
  • {% include %} versus {% extends %}: include pulls a fragment into the current template with the current context (partials, cards, forms); extends puts the current template inside a parent skeleton.

III. Debugging Django Applications

A. Purpose and Principle

Debugging in Django is mostly about surfacing the request/response state at the moment of failure, and Django ships tooling that does this for you.

  • DEBUG = True: produces the yellow traceback page with local variables per frame, the settings dump, the request GET/POST/COOKIES/META, and the URL patterns tried on a 404. Never enable it in production — the page leaks SECRET_KEY and database credentials.
  • ALLOWED_HOSTS: must list your domains once DEBUG = False, or every request returns DisallowedHost (400).
  • Logging over print: configure the LOGGING dict; logger.debug() output is routed to console or file and can be silenced per environment.
PYTHON
import logging
logger = logging.getLogger(__name__)
logger.warning("Empty queryset for user %s", request.user.id)
  • Interactive breakpoints: insert breakpoint() (Python 3.7+) and run python manage.py runserver --noreload so the auto-reloader does not detach the debugger; n, s, c, p var step and inspect.
  • manage.py shell and shell_plus: reproduce ORM behaviour outside the request cycle. print(qs.query) shows the generated SQL; connection.queries lists every query executed.
  • Django Debug Toolbar: a sidebar panel showing SQL count and duration, templates rendered with their context, cache hits, signals and request timing — the fastest way to spot N+1 queries.
  • Template-specific debugging: {% debug %} dumps the full context; 'string_if_invalid': 'INVALID: %s' in OPTIONS turns silent variable failures into visible markers.
  • Reading the traceback: work bottom-up — the last frame is the raising line, the frames above trace the call path from wsgi.py through middleware, URL resolver, view, then template node.

IV. Testing in Django

A. Purpose and Principle

Django's test framework wraps Python's unittest, adding a throwaway test database, a test client that simulates HTTP, and per-test transaction rollback so tests never see each other's data.

  • Base classes: SimpleTestCase (no DB), TestCase (DB, each test wrapped in a transaction and rolled back), TransactionTestCase (real commits, for testing transactional behaviour), LiveServerTestCase (runs a live server for Selenium).
  • Discovery: python manage.py test finds any test*.py file; scope it as manage.py test blog.tests.PostModelTests.test_str.
  • Test database: created as test_<dbname>, migrated, then destroyed; add --keepdb to reuse it and cut startup time.

B. Writing Model, View and Template Tests

PYTHON
from django.test import TestCase
from django.urls import reverse
from .models import Post

class PostViewTests(TestCase):
    def setUp(self):
        self.post = Post.objects.create(title='Hello', body='x', published=True)

    def test_list_page_shows_post(self):
        response = self.client.get(reverse('post_list'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'blog/post_list.html')
        self.assertContains(response, 'Hello')
        self.assertQuerySetEqual(response.context['posts'], [self.post])
  • The test client: self.client.get(), .post(url, data), .login(username=..., password=...); it bypasses the network but runs the full middleware, URL and view stack.
  • Django-specific assertions: assertContains, assertNotContains, assertRedirects, assertTemplateUsed, assertFormError, assertNumQueries (guards against query regressions).
  • setUp versus setUpTestData: setUp runs before every test method; setUpTestData is a classmethod run once per class, which is faster for read-only fixtures.
  • Fixtures and factories: fixtures = ['posts.json'] loads serialised data; Model.objects.create() in code or a factory library is usually clearer and less brittle.
  • Coverage: coverage run --source='.' manage.py test then coverage report shows untested lines — useful as a gap-finder, not as a quality score.

C. Applications and Limitations

  • Where it pays off: model constraints and __str__, form validation, permission checks (an anonymous GET on a protected view must redirect to the login URL), URL reversing, and template rendering.
  • Blind spots: assertContains verifies text presence, not layout or CSS; JavaScript behaviour needs LiveServerTestCase with Selenium; TestCase rollback hides bugs that only appear on real commits.
  • Isolation discipline: never let tests hit external APIs — patch with unittest.mock.patch — and use override_settings rather than editing settings.py for a test.