kciebiera

View on GitHub

Lab 4: Django — Views, URLs & Templates

Introduction

In Labs 1–3 you built a server, structured content, and added style — all by hand. You have experienced exactly the pain that web frameworks exist to solve: routing, templating, static file serving, and request parsing were all written from scratch.

Django is a “batteries-included” Python web framework. In this lab you will recreate what you built in Labs 1–3 in a fraction of the code, and then learn how Django separates layout from content using template inheritance — so a change to the <nav> updates every page at once.

The Goal: A running Django project with multiple URL routes, views rendered from templates, and a shared base layout.

The Theory

Django follows the MVT pattern (Model–View–Template):

Template inheritance adds one more idea:

Setup

uv add django
django-admin startproject mysite .
uv run python manage.py runserver

Open http://127.0.0.1:8000 — you should see the Django rocket launch page.

Key files created:

mysite/
    settings.py   ← project configuration
    urls.py       ← root URL table
manage.py         ← project CLI

Create your first app:

uv run python manage.py startapp pages

Register it in mysite/settings.py:

INSTALLED_APPS = [
    ...
    'pages',   # TODO: add this line
]

Phase 1: Your First View

Open pages/views.py. Django views are just functions that take a request and return a response.

from django.http import HttpResponse

def home(request):
    # TODO: Return an HttpResponse with "<h1>Hello from Django!</h1>"
    pass

Wire it to a URL. Create pages/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    # TODO: Add a path for "" (empty string = root) that calls views.home
    # path("", views.home, name="home"),
]

Include pages.urls in mysite/urls.py:

from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    # TODO: Add: path("", include("pages.urls"))
]

🧪 Visit http://127.0.0.1:8000/ — you should see your heading.

Phase 2: Template Context & URL Parameters

Hardcoding HTML in a view is no better than your socket server. Templates separate logic from presentation.

Create the directory pages/templates/pages/. Inside it, create home.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ page_title }}</title>
</head>
<body>
    <h1>{{ heading }}</h1>
    <p>The server time is: {{ server_time }}</p>
</body>
</html>

{{ variable }} is a Django template placeholder. Update your view:

from django.shortcuts import render
import datetime

def home(request):
    context = {
        # TODO: Add "page_title", "heading", and "server_time" keys
        # server_time should be datetime.datetime.now()
    }
    return render(request, "pages/home.html", context)

Now add two more views that demonstrate URL parameters and template loops:

def about(request):
    # TODO: render pages/about.html with a context containing a list of skills
    # skills = ["Python", "HTTP", "HTML", "CSS"]
    pass

def greet(request, name):
    # TODO: render pages/greet.html passing the name
    pass

Add the routes in pages/urls.py:

urlpatterns = [
    path("", views.home, name="home"),
    # TODO: path for "about/"
    # TODO: path for "greet/<str:name>/" that captures the name
]

Create pages/templates/pages/about.html using a template loop:

<ul>
    {% for skill in skills %}
        <li>{{ skill }}</li>
    {% endfor %}
</ul>

Create pages/templates/pages/greet.html:

<h1>Hello, {{ name }}!</h1>
<!-- TODO: Add a link back to home using {% url 'home' %} -->

🧪 Visit /greet/Alice/ and /greet/Bob/ — each should show a personalised greeting. Reload the home page — the time should update on every refresh.

Phase 3: Template Inheritance

Every page currently duplicates <html>, <head>, and any navigation. Template inheritance solves this with a single base layout.

Create pages/templates/pages/base.html:

<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Site{% endblock %} — MyApp</title>
    <link rel="stylesheet" href="{% static 'pages/style.css' %}">
    {% block extra_head %}{% endblock %}
</head>
<body>
    <header>
        <nav>
            <a href="{% url 'home' %}">Home</a>
            <a href="{% url 'about' %}">About</a>
            <a href="{% url 'projects' %}">Projects</a>
        </nav>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>{% block footer_text %}© 2025 My Site{% endblock %}</p>
    </footer>
</body>
</html>

Refactor home.html, about.html, and greet.html to extend the base:

{% extends "pages/base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
    <h1>{{ heading }}</h1>
    <p>Server time: {{ server_time }}</p>
{% endblock %}

🧪 Reload each page. The nav and footer should now appear without being in the child templates. Open DevTools → Elements and confirm the full DOM is present.

Phase 4: Template Tags, Filters & Static Files

Add a projects view. In pages/views.py:

def projects(request):
    project_list = [
        {"name": "Socket Server",  "lang": "Python", "year": 2025, "done": True},
        {"name": "HTML Profile",   "lang": "HTML",   "year": 2025, "done": True},
        {"name": "CSS Layout",     "lang": "CSS",    "year": 2025, "done": True},
        {"name": "Django App",     "lang": "Python", "year": 2025, "done": False},
    ]
    context = {
        # TODO: pass project_list and a count of done projects
    }
    return render(request, "pages/projects.html", context)

Create pages/templates/pages/projects.html extending the base:

{% extends "pages/base.html" %}
{% block title %}Projects{% endblock %}

{% block content %}
<h1>Projects ({{ done_count }} complete)</h1>

<table>
    <thead>
        <tr><th>Name</th><th>Language</th><th>Year</th><th>Status</th></tr>
    </thead>
    <tbody>
        {% for project in project_list %}
        <tr>
            <td>{{ project.name }}</td>
            <td>{{ project.lang|lower }}</td>
            <td>{{ project.year }}</td>
            <td>{% if project.done %}✅ Done{% else %}🔄 In progress{% endif %}</td>
        </tr>
        {% empty %}
        <tr><td colspan="4">No projects yet.</td></tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

The |lower is a filter — it transforms the value before displaying it. Try a few more in your template: |upper, |length, {{ project.year|add:1 }}.

Now wire up static files. In mysite/settings.py, confirm:

STATIC_URL = "/static/"

Create pages/static/pages/style.css and paste the CSS from your Lab 3 stylesheet. The {% static %} tag in base.html generates the correct URL automatically.

🧪 Verify in DevTools → Network that style.css loads with status 200.

Phase 5: A Second App & Multi-App Routing

A Django project can host many apps, each owning its own models, views, URLs, and templates. Right now your project has one app (pages). In this phase you will add a second app — blog — and see how the root URL router delegates to each.

5.1 Create the blog App

uv run python manage.py startapp blog

Register it in mysite/settings.py:

INSTALLED_APPS = [
    ...
    'pages',
    'blog',   # ← add this
]

5.2 Add App-Level URLs

Create blog/urls.py:

from django.urls import path
from . import views

app_name = "blog"   # URL namespace — required for {% url 'blog:…' %}

urlpatterns = [
    path("",             views.post_list,   name="post-list"),
    path("<int:pk>/",    views.post_detail, name="post-detail"),
]

Wire both apps into mysite/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/",  admin.site.urls),
    path("",        include("pages.urls")),   # handles /  /about/  /greet/…
    path("blog/",   include("blog.urls")),    # handles /blog/  /blog/42/
]

Everything under /blog/ is now owned by blog/urls.py. Django strips the blog/ prefix before forwarding to the app — the app patterns only see the remaining part.

5.3 Add Two Views to blog

Open blog/views.py:

from django.http import HttpResponse
from django.shortcuts import render

POSTS = [
    {"pk": 1, "title": "Hello Django",    "body": "My first post."},
    {"pk": 2, "title": "URL Routing",     "body": "How include() works."},
    {"pk": 3, "title": "Template Tricks", "body": "Extends and blocks."},
]

def post_list(request):
    return render(request, "blog/post_list.html", {"posts": POSTS})

def post_detail(request, pk):
    post = next((p for p in POSTS if p["pk"] == pk), None)
    if post is None:
        return HttpResponse("Post not found", status=404)
    return render(request, "blog/post_detail.html", {"post": post})

5.4 Create Templates

Create the directory structure:

blog/
    templates/
        blog/              ← namespace subdirectory (same as app name)
            post_list.html
            post_detail.html

blog/templates/blog/post_list.html — extend your existing pages/base.html:

{% extends "pages/base.html" %}
{% block title %}Blog{% endblock %}

{% block content %}
<h1>Blog Posts</h1>
<ul>
    {% for post in posts %}
    <li>
        <a href="{% url 'blog:post-detail' pk=post.pk %}">{{ post.title }}</a>
    </li>
    {% empty %}
    <li>No posts yet.</li>
    {% endfor %}
</ul>
{% endblock %}

blog/templates/blog/post_detail.html:

{% extends "pages/base.html" %}
{% block title %}{{ post.title }}{% endblock %}

{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
<a href="{% url 'blog:post-list' %}">← All posts</a>
{% endblock %}

Note the namespace prefix blog: in every {% url %} tag. Without it Django would raise NoReverseMatch once two apps both have a pattern named post-list.

In pages/templates/pages/base.html, add a link to the blog:

<nav>
    <a href="{% url 'home' %}">Home</a>
    <a href="{% url 'about' %}">About</a>
    <a href="{% url 'blog:post-list' %}">Blog</a>
</nav>

🧪 Visit http://127.0.0.1:8000/blog/ — you should see the post list. Click a title to go to the detail page. The <nav> should work on both the pages and blog templates since they both extend the same base.html.

5.6 Confirm Namespace Isolation

Open uv run python manage.py shell and verify that both namespaces resolve correctly without colliding:

from django.urls import reverse

reverse("home")                           # → "/"
reverse("blog:post-list")                 # → "/blog/"
reverse("blog:post-detail", kwargs={"pk": 2})  # → "/blog/2/"

Try reverse("post-list") (no namespace) — it will raise NoReverseMatch because the name is only registered under the blog namespace.

Submission

Final checks:

  1. Routes /, /about/, /greet/<name>/, and /projects/ all work.
  2. Every page extends base.html — zero duplicated <html>/<head>/<body> tags in child templates.
  3. The projects page uses {% for %}, {% if %}, {% empty %}, and at least two filters.
  4. CSS from Lab 3 is served as a static file.
  5. /blog/ lists all posts and /blog/<pk>/ shows a single post.
  6. {% url %} tags use the blog: namespace prefix — no hard-coded URLs anywhere.
  7. Both apps coexist in INSTALLED_APPS and mysite/urls.py.

Exploration: Use {% include %} to extract the project table row into a partial file pages/templates/pages/_project_row.html and include it from the loop with {% include "pages/_project_row.html" %}. The page should render identically.