Unit 3: Templates, Debugging and Testing
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 torender(). - Templates live in a known search path: the
DIRSlist and theAPP_DIRSflag inside theTEMPLATESsetting insettings.py. - Convention for app templates:
myapp/templates/myapp/detail.html— the app name is repeated insidetemplates/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 defaultDjangoTemplatesbackend.
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
TEMPLATESsetting controls discovery.
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 compiledTemplate;.render(context)returns a string. - Context processors: callables that inject variables into every template —
authsuppliesuserandperms, 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:
blog/
├── templates/
│ └── blog/
│ ├── base.html
│ └── post_list.html
└── views.py- The
render()shortcut:render(request, template_name, context)combines loading, rendering and wrapping in anHttpResponsewith status 200.
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:
ListViewwithtemplate_name = 'blog/post_list.html'andcontext_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<script>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__.pyandmyapp/templatetags/blog_extras.py, then{% load blog_extras %}.
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 triesa['b'], thena.b, thena[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
@propertyon the model or compute it in the view. - Restricted names: variables cannot begin with an underscore, and attributes with
alters_data = True(such asModel.delete) are refused.
F. for loop and if-else statements
These two tags carry almost all in-template logic.
{% 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 %}{% for %}specifics:forloop.counter(1-based),forloop.counter0(0-based),forloop.first,forloop.last,forloop.revcounter, andforloop.parentloopwhen nested.{% empty %}runs when the sequence is empty.{% for x in qs reversed %}iterates backwards. There is nobreakorcontinue.{% if %}specifics: supportsand,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)— validatereport_typeagainst 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.htmldefines the holes:
<!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:
{% 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.html→base_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 requestGET/POST/COOKIES/META, and the URL patterns tried on a 404. Never enable it in production — the page leaksSECRET_KEYand database credentials.ALLOWED_HOSTS: must list your domains onceDEBUG = False, or every request returnsDisallowedHost(400).- Logging over
print: configure theLOGGINGdict;logger.debug()output is routed to console or file and can be silenced per environment.
import logging
logger = logging.getLogger(__name__)
logger.warning("Empty queryset for user %s", request.user.id)- Interactive breakpoints: insert
breakpoint()(Python 3.7+) and runpython manage.py runserver --noreloadso the auto-reloader does not detach the debugger;n,s,c,p varstep and inspect. manage.py shellandshell_plus: reproduce ORM behaviour outside the request cycle.print(qs.query)shows the generated SQL;connection.querieslists 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'inOPTIONSturns 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.pythrough 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 testfinds anytest*.pyfile; scope it asmanage.py test blog.tests.PostModelTests.test_str. - Test database: created as
test_<dbname>, migrated, then destroyed; add--keepdbto reuse it and cut startup time.
B. Writing Model, View and Template Tests
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). setUpversussetUpTestData:setUpruns before every test method;setUpTestDatais 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 testthencoverage reportshows 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:
assertContainsverifies text presence, not layout or CSS; JavaScript behaviour needsLiveServerTestCasewith Selenium;TestCaserollback hides bugs that only appear on real commits. - Isolation discipline: never let tests hit external APIs — patch with
unittest.mock.patch— and useoverride_settingsrather than editingsettings.pyfor a test.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →