This tutorial delves into the core concepts of URLs, templates, and forms in Django. We’ll explore URL configuration, learn how to create reusable templates, and master the Django Forms API to handle user input effectively. We’ll also write unit tests throughout the process to ensure our code works as expected.
If you’ve been following along with this series, ensure your models.py
is up-to-date before proceeding:
boards/models.py
class Topic(models.Model):
# other fields...
# Add `auto_now_add=True` to the `last_updated` field
last_updated = models.DateTimeField(auto_now_add=True)
class Post(models.Model):
# other fields...
# Add `null=True` to the `updated_by` field
updated_by = models.ForeignKey(User, null=True, related_name='+')
Then, execute these commands within your activated virtual environment:
python manage.py makemigrations
python manage.py migrate
You can skip the above steps if your updated_by
field already has null=True
and last_updated
has auto_now_add=True
.
Alternatively, you can start with the source code from GitHub, specifically the v0.2-lw release tag:
https://github.com/sibtc/django-beginners-guide/tree/v0.2-lw
Development will continue from that point.
URLs
Now, let’s implement a new page to display all topics associated with a specific board. This aligns with the wireframe from the previous tutorial:
Figure 1: Boards project wireframe listing all topics in the Django board.
We’ll begin by modifying the urls.py file in the myproject directory:
myproject/urls.py
from django.conf.urls import url
from django.contrib import admin
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^boards/(?P<pk>d+)/$', views.board_topics, name='board_topics'),
url(r'^admin/', admin.site.urls),
]
Let’s analyze urlpatterns
and url
. The URL dispatcher and URLconf (URL configuration) are essential Django components. Understanding them is key to routing requests correctly. Django is working on simplifying routing syntax, but for now, in version 1.11 (and likely later versions), this is how it works.
A project can have multiple urls.py files within different apps, but Django needs a root urls.py to start with. This is configured in settings.py:
myproject/settings.py
ROOT_URLCONF = 'myproject.urls'
Django starts searching for a matching URL pattern in the project’s URLconf when it receives a request. It iterates through urlpatterns
, testing the requested URL against each url
entry. The order in urlpatterns
is important – Django stops searching after finding the first match. If no match is found, a 404 exception (Page Not Found) is raised.
Here’s the structure of the url
function:
def url(regex, view, kwargs=None, name=None):
# ...
- regex: A regular expression to match URL patterns. Importantly, these regexes don’t search GET or POST parameters. So, for
http://127.0.0.1:8000/boards/?page=2
, only/boards/
is processed. - view: The view function to handle the request if the URL matches. It can also be the return value of
django.conf.urls.include
to reference external urls.py files for modularity (more on this later). - kwargs: Arbitrary keyword arguments passed to the view. Used for simple customization of reusable views, but not frequently.
- name: A unique identifier for the URL. Always name your URLs! This allows you to change URLs project-wide by just modifying the regex, without hardcoding URLs in views or templates.
Basic URLs
Matching static strings easily creates basic URLs. For example, an “about” page:
from django.conf.urls import url
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^about/$', views.about, name='about'),
]
Deeper structures are also possible:
from django.conf.urls import url
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^about/$', views.about, name='about'),
url(r'^about/company/$', views.about_company, name='about_company'),
url(r'^about/author/$', views.about_author, name='about_author'),
url(r'^about/author/vitor/$', views.about_vitor, name='about_vitor'),
url(r'^about/author/erica/$', views.about_erica, name='about_erica'),
url(r'^privacy/$', views.privacy_policy, name='privacy_policy'),
]
The view functions for these examples follow a similar structure:
def about(request):
# do something...
return render(request, 'about.html')
def about_company(request):
# do something else...
# return some data along with the view...
return render(request, 'about_company.html', {'company_name': 'Simple Complex'})
Advanced URLs
Leveraging regular expressions allows for matching specific data types and creating dynamic URLs.
For a user profile page like github.com/vitorfs
or twitter.com/vitorfs
:
from django.conf.urls import url
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^(?P<username>[w.@+-]+)/$', views.user_profile, name='user_profile'),
]
This matches valid Django usernames. Note that this is a permissive URL pattern due to being defined at the root. To define an /about/ URL, it must be defined before the username URL pattern:
from django.conf.urls import url
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^about/$', views.about, name='about'),
url(r'^(?P<username>[w.@+-]+)/$', views.user_profile, name='user_profile'),
]
If “about” was defined after, it would never be matched because “about” would match the username regex, calling user_profile
instead of about
. This creates a side effect: “about” becomes a forbidden username because a user named “about” would never see their profile.
Sidenote: A good solution for user profile URLs is to add a prefix like /u/vitorfs/ or /@vitorfs/ to avoid conflicts. If you don’t want a prefix, use a list of reserved names like github.com/shouldbee/reserved-usernames. GitHub has a similar issue with the github.com/watching URL.
The point is to create dynamic pages where a portion of the URL serves as an identifier for a specific resource, used to generate the page. This identifier can be an integer ID or a string.
We’ll be working with the Board ID for the Topics page. Review the initial example:
url(r'^boards/(?P<pk>d+)/$', views.board_topics, name='board_topics')
The regex d+
matches an integer. (?P<pk>d+)</pk>
captures this value into a keyword argument named pk. The view function is:
def board_topics(request, pk):
# do something...
Using (?P<pk>d+)</pk>
requires the keyword argument in board_topics
to be named pk. To use a different name:
url(r'^boards/(d+)/$', views.board_topics, name='board_topics')
And the view function:
def board_topics(request, board_id):
# do something...
Or:
def board_topics(request, id):
# do something...
The name doesn’t matter in this case. However, using named parameters is a good practice for readability, especially when capturing multiple IDs.
Sidenote: PK or ID?
PK stands for Primary Key. It’s a shortcut for accessing a model’s primary key. All Django models have this attribute. For most cases, pk
is the same as id
because if you don’t define a primary key, Django creates an AutoField
named id
. If you define a different primary key (e.g., email
), you can access it using obj.email
or obj.pk
.
Using the URLs API
Let’s implement the topic listing page. First, add the URL route:
myproject/urls.py
from django.conf.urls import url
from django.contrib import admin
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^boards/(?P<pk>d+)/$', views.board_topics, name='board_topics'),
url(r'^admin/', admin.site.urls),
]
Now create the view function board_topics
:
boards/views.py
from django.shortcuts import render
from .models import Board
def home(request):
# code suppressed for brevity
def board_topics(request, pk):
board = Board.objects.get(pk=pk)
return render(request, 'topics.html', {'board': board})
Create a new template named topics.html in the templates folder:
templates/topics.html
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ board.name }}</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
<ol class="breadcrumb my-4">
<li class="breadcrumb-item"><a href="/">Boards</a></li>
<li class="breadcrumb-item active">{{ board.name }}</li>
</ol>
</div>
</body>
</html>
Note: For now, we are creating new HTML templates. Reusable templates are covered in the following section.
Access the URL http://127.0.0.1:8000/boards/1/ in a web browser.
Let’s write some tests! Add these tests to the bottom of tests.py:
boards/tests.py
from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import home, board_topics
from .models import Board
class HomeTests(TestCase):
# ...
class BoardTopicsTests(TestCase):
def setUp(self):
Board.objects.create(name='Django', description='Django board.')
def test_board_topics_view_success_status_code(self):
url = reverse('board_topics', kwargs={'pk': 1})
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
def test_board_topics_view_not_found_status_code(self):
url = reverse('board_topics', kwargs={'pk': 99})
response = self.client.get(url)
self.assertEquals(response.status_code, 404)
def test_board_topics_url_resolves_board_topics_view(self):
view = resolve('/boards/1/')
self.assertEquals(view.func, board_topics)
Key points:
setUp
creates a Board instance because Django’s testing suite doesn’t use the live database. It creates a new database, applies migrations, runs tests, and destroys the database. ThesetUp
method prepares the environment.test_board_topics_view_success_status_code
: Tests if an existing Board returns a 200 (success) status code.test_board_topics_view_not_found_status_code
: Tests if a non-existent Board returns a 404 (page not found) status code.test_board_topics_url_resolves_board_topics_view
: Tests if the correct view function is used.
Run the tests:
python manage.py test
Output:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.E...
======================================================================
ERROR: test_board_topics_view_not_found_status_code (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...
boards.models.DoesNotExist: Board matching query does not exist.
----------------------------------------------------------------------
Ran 5 tests in 0.093s
FAILED (errors=1)
Destroying test database for alias 'default'...
The test test_board_topics_view_not_found_status_code failed with a boards.models.DoesNotExist
exception.
In production (DEBUG=False
), this would show a 500 Internal Server Error. We want a 404 Page Not Found. Refactor the view:
boards/views.py
from django.shortcuts import render
from django.http import Http404
from .models import Board
def home(request):
# code suppressed for brevity
def board_topics(request, pk):
try:
board = Board.objects.get(pk=pk)
except Board.DoesNotExist:
raise Http404
return render(request, 'topics.html', {'board': board})
Test again:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.042s
OK
Destroying test database for alias 'default'...
Now it’s working correctly.
This is the default Django 404 page (DEBUG=False
). Later, we can customize it. Django provides a shortcut for this common use case: get_object_or_404
. Refactor the view again:
from django.shortcuts import render, get_object_or_404
from .models import Board
def home(request):
# code suppressed for brevity
def board_topics(request, pk):
board = get_object_or_404(Board, pk=pk)
return render(request, 'topics.html', {'board': board})
Test it:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.052s
OK
Destroying test database for alias 'default'...
No issues. The next step is to create navigation links: homepage to topics page, and topics page back to the homepage.
Start by writing tests for the HomeTests
class:
boards/tests.py
class HomeTests(TestCase):
def setUp(self):
self.board = Board.objects.create(name='Django', description='Django board.')
url = reverse('home')
self.response = self.client.get(url)
def test_home_view_status_code(self):
self.assertEquals(self.response.status_code, 200)
def test_home_url_resolves_home_view(self):
view = resolve('/')
self.assertEquals(view.func, home)
def test_home_view_contains_link_to_topics_page(self):
board_topics_url = reverse('board_topics', kwargs={'pk': self.board.pk})
self.assertContains(self.response, 'href="{0}"'.format(board_topics_url))
A setUp
method is added for HomeTests
as well, creating a Board instance and storing the url
and response
for reuse. test_home_view_contains_link_to_topics_page
uses assertContains
to check if the response body contains the href
of the topics page.
Run the tests:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F.
======================================================================
FAIL: test_home_view_contains_link_to_topics_page (boards.tests.HomeTests)
----------------------------------------------------------------------
# ...
AssertionError: False is not true : Couldn't find 'href="/boards/1/"' in response
----------------------------------------------------------------------
Ran 6 tests in 0.034s
FAILED (failures=1)
Destroying test database for alias 'default'...
Now write the code to make the test pass:
templates/home.html
<tbody>
{% for board in boards %}
<tr>
<td>
<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
<small class="text-muted d-block">{{ board.description }}</small>
</td>
<td class="align-middle">0</td>
<td class="align-middle">0</td>
</tr>
{% endfor %}
</tbody>
The line {{ board.name }}
is changed to:
<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
Always use the <span>{</span><span>%</span><span>url</span><span>%</span><span>}</span>
template tag to generate URLs. The first parameter is the URL name (defined in urls.py), followed by any necessary arguments. A simple URL like the homepage would be <span>{</span><span>%</span><span>url</span><span>'home'</span><span>%</span><span>}</span>
.
Save and run the tests:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.037s
OK
Destroying test database for alias 'default'...
Good! Check it in the browser:
Now the link back. Write the test first:
boards/tests.py
class BoardTopicsTests(TestCase):
# code suppressed for brevity...
def test_board_topics_view_contains_link_back_to_homepage(self):
board_topics_url = reverse('board_topics', kwargs={'pk': 1})
response = self.client.get(board_topics_url)
homepage_url = reverse('home')
self.assertContains(response, 'href="{0}"'.format(homepage_url))
Run the tests:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F.....
======================================================================
FAIL: test_board_topics_view_contains_link_back_to_homepage (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...
AssertionError: False is not true : Couldn't find 'href="/"' in response
----------------------------------------------------------------------
Ran 7 tests in 0.054s
FAILED (failures=1)
Destroying test database for alias 'default'...
Update the board topics template:
templates/topics.html
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ board.name }}</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
<ol class="breadcrumb my-4">
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item active">{{ board.name }}</li>
</ol>
</div>
</body>
</html>
Run the tests:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.061s
OK
Destroying test database for alias 'default'...
URL routing is fundamental.
List of Useful URL Patterns
Here are some commonly used URL patterns:
Pattern | Regex | Example | Valid URL | Captures |
---|---|---|---|---|
Primary Key AutoField | (?P<pk>d+)</pk> |
url(r'^questions/(?P<pk>d+)/$', views.question, name='question')</pk> |
/questions/934/ |
{'pk': '934'} |
Slug Field | (?P<slug>[-w]+)</slug> |
url(r'^posts/(?P<slug>[-w]+)/$', views.post, name='post')</slug> |
/posts/hello-world/ |
{'slug': 'hello-world'} |
Slug Field with Primary Key | (?P<slug>[-w]+)-(?P<pk>d+)</pk></slug> |
url(r'^blog/(?P<slug>[-w]+)-(?P<pk>d+)/$', views.blog_post, name='blog_post')</pk></slug> |
/blog/hello-world-159/ |
{'slug': 'hello-world', 'pk': '159'} |
Django User Username | (?P<username>[w.@+-]+)</username> |
url(r'^profile/(?P<username>[w.@+-]+)/$', views.user_profile, name='user_profile')</username> |
/profile/vitorfs/ |
{'username': 'vitorfs'} |
Year | (?P<year>[0-9]{4})</year> |
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive, name='year')</year> |
/articles/2016/ |
{'year': '2016'} |
Year / Month | (?P<year>[0-9]{4})/(?P<month>[0-9]{2})</month></year> |
url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive, name='month')</month></year> |
/articles/2016/01/ |
{'year': '2016', 'month': '01'} |














More details can be found here: List of Useful URL Patterns.
Reusable Templates
Currently, we’re copying and pasting HTML, which is unsustainable. Let’s refactor our HTML templates by creating a master page (base template).
Create base.html in the templates folder:
templates/base.html
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Django Boards{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/app.css' %}">
</head>
<body>
{% block breadcrumb %}
{% endblock %}
{% block content %}
{% endblock %}
</body>
</html>
Every template will extend this template. <span>{</span><span>%</span><span>block</span><span>%</span><span>}</span>
reserves a space for “child” templates to insert code. <span>{</span><span>%</span><span>block</span><span>title</span><span>%</span><span>}</span>
has a default value (“Django Boards”) used if the child template doesn’t define its own title.
Refactor home.html and topics.html:
templates/home.html
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item active">Boards</li>
{% endblock %}
{% block content %}
Board
Posts
Topics
Last Post
{% for board in boards %}
<tr>
<td>
<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
{{ board.description }}
</td>
0
0
</tr>
{% endfor %}
{% endblock %}
The first line, <span>{</span><span>%</span><span>extends</span><span>'base.html'</span><span>%</span><span>}</span>
, tells Django to use base.html as the master. The blocks insert the page’s unique content.
templates/topics.html
{% extends 'base.html' %}
{% block title %}
{{ board.name }} - {{ block.super }}
{% endblock %}
{% block breadcrumb %}
Boards
{{ board.name }}
{% endblock %}
{% block content %}
New topic
Topic
Starter
Replies
Views
Last Update
{% for topic in board.topics.all %}
<tr>
{{ topic.subject }}
{{ topic.starter.username }}
0
0
{{ topic.last_updated }}
</tr>
{% endfor %}
{% endblock %}
The <span>{</span><span>%</span><span>block</span><span>title</span><span>%</span><span>}</span>
default value is changed. <span>{</span><span>{</span><span>block.super</span><span>}</span><span>}</span>
reuses the default value from the base template. For a “Python” board, the title will be “Python – Django Boards.”
Run the tests:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.067s
OK
Destroying test database for alias 'default'...
Now add a top bar with a menu to base.html:
templates/base.html
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Django Boards{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/app.css' %}">
</head>
<body>
Django Boards
{% block breadcrumb %}
{% endblock %}
{% block content %}
{% endblock %}
</body>
</html>
This HTML uses the Bootstrap 4 Navbar Component.
To change the font of the “logo” (.navbar-brand
), use fonts.google.com. Add the chosen font to base.html:
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Django Boards{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/app.css' %}">
</head>
<body>
Django Boards
{% block breadcrumb %}
{% endblock %}
{% block content %}
{% endblock %}
</body>
</html>
Create a new CSS file, app.css, inside the static/css folder:
static/css/app.css
.navbar-brand {
font-family: 'Peralta', cursive;
}
Forms
Forms handle user input. HTML forms are the standard way to submit data to the server. Form processing is complex, involving data type conversion, validation, sanitation, and security. The Django Forms API simplifies this, automating much of the work and producing more secure code. Always use the Forms API, no matter how simple the form is.
How Not To Implement a Form
Let’s understand the underlying details of form processing. Then, when things go wrong, you know how to troubleshoot them.
Let’s implement this form:
This form processes data for two models: Topic (subject) and Post (message). We also need user authentication to determine who created the topic/post.
For now, abstract some details and focus on saving user input.
First, create a new URL route named new_topic:
myproject/urls.py
from django.conf.urls import url
from django.contrib import admin
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^boards/(?P<pk>d+)/$', views.board_topics, name='board_topics'),
url(r'^boards/(?P<pk>d+)/new/$', views.new_topic, name='new_topic'),
url(r'^admin/', admin.site.urls),
]
This URL structure helps identify the correct Board.
Create the new_topic view function:
boards/views.py
from django.shortcuts import render, get_object_or_404
from .models import Board
def new_topic(request, pk):
board = get_object_or_404(Board, pk=pk)
return render(request, 'new_topic.html', {'board': board})
For now, new_topic is the same as board_topics.
Create a template named new_topic.html:
templates/new_topic.html
{% extends 'base.html' %}
{% block title %}Start a New Topic{% endblock %}
{% block breadcrumb %}
Boards
{{ board.name }}
New topic
{% endblock %}
{% block content %}
{% endblock %}
The breadcrumb ensures navigation, including a link back to **board_