Part3 PDF
Part3 PDF
SERIES
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 1/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Introduction
In this tutorial, we are going to dive deep into two fundamental concepts: URLs and
Forms. In the process, we are going to explore many other concepts like creating
reusable templates and installing third-party libraries. We are also going to write plenty
of unit tests.
If you are following this tutorial series since the first part, coding your project and
following the tutorial step by step, you may need to update your models.py before
starting:
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='+')
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 2/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
If you prefer to use my source code as a starting point, you can grab it on GitHub.
The current state of the project can be found under the release tag v0.2-lw. The link
below will take you to the right place:
https://github.com/sibtc/django-beginners-guide/tree/v0.2-lw
URLs
Proceeding with the development of our application, now we have to implement a new
page to list all the topics that belong to a given Board. Just to recap, below you can
see the wireframe we drew in the previous tutorial:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 3/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Figure 1: Boards project wireframe listing all topics in the Django board.
myproject/urls.py
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topic
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 4/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
url(r'^admin/', admin.site.urls),
]
This time let’s take a moment and analyze the urlpatterns and url .
The URL dispatcher and URLconf (URL configuration) are fundamental parts of a
Django application. In the beginning, it can look confusing; I remember having a hard
time when I first started developing with Django.
In fact, right now the Django Developers are working on a proposal to make simplified
routing syntax. But for now, as per the version 1.11, that’s what we have. So let’s try to
understand how it works.
A project can have many urls.py distributed among the apps. But Django needs a
url.py to use as a starting point. This special urls.py is called root URLconf. It’s
defined in the settings.py file.
myproject/settings.py
ROOT_URLCONF = 'myproject.urls'
When Django receives a request, it starts searching for a match in the project’s
URLconf. It starts with the first entry of the urlpatterns variable, and test the
requested URL against each url entry.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 5/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
If Django finds a match, it will pass the request to the view function, which is the
second parameter of the url . The order in the urlpatterns matters, because
Django will stop searching as soon as it finds a match. Now, if Django doesn’t find a
match in the URLconf, it will raise a 404 exception, which is the error code for Page
Not Found.
regex: A regular expression for matching URL patterns in strings. Note that these
regular expressions do not search GET or POST parameters. In a request to
http://127.0.0.1:8000/boards/?page=2 only /boards/ will be processed.
view: A view function used to process the user request for a matched URL. It also
accepts the return of the django.conf.urls.include function, which is used to
reference an external urls.py file. You can, for example, use it to define a set of app
specific URLs, and include it in the root URLconf using a prefix. We will explore more
on this concept later on.
kwargs: Arbitrary keyword arguments that’s passed to the target view. It is normally
used to do some simple customization on reusable views. We don’t use it very often.
name: A unique identifier for a given URL. This is a very important feature. Always
remember to name your URLs. With this, you can change a specific URL in the whole
project by just changing the regex. So it’s important to never hard code URLs in the
views or templates, and always refer to the URLs by its name.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 6/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Basic URLs
Basic URLs are very simple to create. It’s just a matter of matching strings. For
example, let’s say we wanted to create an “about” page, it could be defined like this:
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^about/$', views.about, name='about'),
]
urlpatterns = [
url(r'^$', views.home, name='home'),
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 7/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Those are some examples of simple URL routing. For all the examples above, the view
function will follow this 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': 'Simp
Advanced URLs
A more advanced usage of URL routing is achieved by taking advantage of the regex
to match certain types of data and create dynamic URLs.
For example, to create a profile page, like many services do like github.com/vitorfs or
twitter.com/vitorfs, where “vitorfs” is my username, we can do the following:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 8/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_pr
]
This will match all valid usernames for a Django User model.
Now observe that the example above is a very permissive URL. That means it will
match lots of URL patterns because it is defined in the root of the URL, with no prefix
like /profile/<username>/. In this case, if we wanted to define a URL named /about/,
we would have do define it before the username URL pattern:
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^about/$', views.about, name='about'),
url(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_pr
]
If the “about” page was defined after the username URL pattern, Django would never
find it, because the word “about” would match the username regex, and the view
user_profile would be processed instead of the about view function.
There are some side effects to that. For example, from now on, we would have to treat
“about” as a forbidden username, because if a user picked “about” as their username,
this person would never see their profile page.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 9/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Sidenote: If you want to design cool URLs for user profiles, the easiest solution to avoid URL collision is
by adding a prefix like /u/vitorfs/, or like Medium does /@vitorfs/, where "@" is the prefix.
If you want no prefix at all, consider using a list of forbidden names like this:
github.com/shouldbee/reserved-usernames. Or another example is an application I developed when I was
learning Django; I created my list at the time: github.com/vitorfs/parsifal/.
Those collisions are very common. Take GitHub for example; they have this URL to list all the repositories
you are currently watching: github.com/watching. Someone registered a username on GitHub with the
name "watching," so this person can't see his profile page. We can see a user with this username exists
by trying this URL: github.com/watching/repositories which was supposed to list the user's repositories,
like mine for example github.com/vitorfs/repositories.
The whole idea of this kind of URL routing is to create dynamic pages where part of
the URL will be used as an identifier for a certain resource, that will be used to
compose a page. This identifier can be an integer ID or a string for example.
Initially, we will be working with the Board ID to create a dynamic page for the Topics.
Let’s read again the example I gave at the beginning of the URLs section:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 10/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
The regex \d+ will match an integer of arbitrary size. This integer will be used to
retrieve the Board from the database. Now observe that we wrote the regex as
(?P<pk>\d+) , this is telling Django to capture the value into a keyword argument
named pk.
Or like this:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 11/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
The name wouldn’t matter. But it’s a good practice to use named parameters because
when we start composing bigger URLs capturing multiple IDs and variables, it will be
easier to read.
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 the most cases, using the pk property is the same as id . That's because if we don't define a
primary key for a model, Django will automatically create an AutoField named id , which will be its
primary key.
If you defined a different primary key for a model, for example, let's say the field email is your primary
key. To access it you could either use obj.email or obj.pk .
It’s time to write some code. Let’s implement the topic listing page (see Figure 1) I
mentioned at the beginning of the URLs section.
myproject/urls.py
urlpatterns = [
url(r'^$', views.home, name='home'),
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 12/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
boards/views.py
def home(request):
# code suppressed for brevity
templates/topics.html
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 13/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Note: For now we are simply creating new HTML templates. No worries, in the following section I will
show you how to create reusable templates.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 14/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Time to write some tests! Edit the tests.py file and add the following tests in the
bottom of the file:
boards/tests.py
class HomeTests(TestCase):
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 15/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
# ...
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)
A few things to note here. This time we used the setUp method. In the setup
method, we created a Board instance to use in the tests. We have to do that because
the Django testing suite doesn’t run your tests against the current database. To run the
tests Django creates a new database on the fly, applies all the model migrations, runs
the tests, and when done, destroys the testing database.
So in the setUp method, we prepare the environment to run the tests, so to simulate
a scenario.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 16/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
----------------------------------------------------------------------
Ran 5 tests in 0.093s
FAILED (errors=1)
Destroying test database for alias 'default'...
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 17/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
In production with DEBUG=False , the visitor would see a 500 Internal Server Error
page. But that’s not the behavior we want.
We want to show a 404 Page Not Found. So let’s refactor our view:
boards/views.py
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 18/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
def home(request):
# code suppressed for brevity
OK
Destroying test database for alias 'default'...
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 19/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
This is the default page Django show while with DEBUG=False . Later on, we can
customize the 404 page to show something else.
Now that’s a very common use case. In fact, Django has a shortcut to try to get an
object, or return a 404 with the object does not exist.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 20/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
def home(request):
# code suppressed for brevity
OK
Destroying test database for alias 'default'...
The next step now is to create the navigation links in the screens. The homepage
should have a link to take the visitor to the topics page of a given Board. Similarly, the
topics page should have a link back to the homepage.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 21/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
boards/tests.py
class HomeTests(TestCase):
def setUp(self):
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 22/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
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.bo
self.assertContains(self.response, 'href="{0}"'.format(board_top
Observe that now we added a setUp method for the HomeTests as well. That’s
because now we are going to need a Board instance and also we moved the url and
response to the setUp, so we can reuse the same response in the new test.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 23/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
....F.
======================================================================
FAIL: test_home_view_contains_link_to_topics_page (boards.tests.HomeTest
----------------------------------------------------------------------
# ...
----------------------------------------------------------------------
Ran 6 tests in 0.034s
FAILED (failures=1)
Destroying test database for alias 'default'...
Now we can write the code that will make this test pass.
templates/home.html
</tbody>
<!-- code suppressed for brevity -->
{{ board.name }}
To:
Always use the {% url %} template tag to compose the applications URLs. The first
parameter is the name of the URL (defined in the URLconf, i.e., the urls.py), then you
can pass an arbitrary number of arguments as needed.
If it were a simple URL, like the homepage, it would be just {% url 'home' %} .
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 25/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
OK
Destroying test database for alias 'default'...
boards/tests.py
class BoardTopicsTests(TestCase):
# code suppressed for brevity...
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 26/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
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)
----------------------------------------------------------------------
Ran 7 tests in 0.054s
FAILED (failures=1)
Destroying test database for alias 'default'...
templates/topics.html
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 27/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
OK
Destroying test database for alias 'default'...
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 28/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
The trick part is the regex. So I prepared a list of the most used URL patterns. You can
always refer to this list when you need a specific URL.
Regex (?P<pk>\d+)
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 29/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Slug Field
Regex (?P<slug>[-\w]+)
Regex (?P<slug>[-\w]+)-(?P<pk>\d+)
Valid /blog/hello-world-159/
URL
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 30/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Regex (?P<username>[\w.@+-]+)
Valid /profile/vitorfs/
URL
Year
Regex (?P<year>[0-9]{4})
Year / Month
Regex (?P<year>[0-9]{4})/(?P<month>[0-9]{2})
Valid /articles/2016/01/
URL
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 31/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Year / Month
You can find more details about those patterns in this post: List of Useful URL
Patterns.
Reusable Templates
Until now we’ve been copying and pasting HTML repeating several parts of the HTML
document, which is not very sustainable in the long run. It’s also a bad practice.
In this section we are going to refactor our HTML templates, creating a master page
and only adding the unique part for each template.
templates/base.html
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 32/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
<div class="container">
<ol class="breadcrumb my-4">
{% block breadcrumb %}
{% endblock %}
</ol>
{% block content %}
{% endblock %}
</div>
</body>
</html>
This is going to be our master page. Every template we create, is going to extend this
special template. Observe now we introduced the {% block %} tag. It is used to
reserve a space in the template, which a “child” template (which extends the master
page) can insert code and HTML within that space.
In the case of the {% block title %} we are also setting a default value, which is
“Django Boards.” It will be used if we don’t set a value for the {% block title %} in
a child template.
templates/home.html
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item active">Boards</li>
{% endblock %}
{% block content %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 33/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Board</th>
<th>Posts</th>
<th>Topics</th>
<th>Last Post</th>
</tr>
</thead>
<tbody>
{% for board in boards %}
<tr>
<td>
<a href="{% url 'board_topics' board.pk %}">{{ board.name }
<small class="text-muted d-block">{{ board.description }}</
</td>
<td class="align-middle">0</td>
<td class="align-middle">0</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
The first line in the home.html template is {% extends 'base.html' %} . This tag is
telling Django to use the base.html template as a master page. After that, we are
using the the blocks to put the unique content of the page.
templates/topics.html
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 34/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
{% extends 'base.html' %}
{% block title %}
{{ board.name }} - {{ block.super }}
{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item active">{{ board.name }}</li>
{% endblock %}
{% block content %}
<!-- just leaving it empty for now. we will add core here soon. -->
{% endblock %}
In the topics.html template, we are changing the {% block title %} default value.
Notice that we can reuse the default value of the block by calling
{{ block.super }} . So here we are playing with the website title, which we defined
in the base.html as “Django Boards.” So for the “Python” board page, the title will be
“Python - Django Boards,” for the “Random” board the title will be “Random - Django
Boards.”
Now let’s run the tests and see we didn’t break anything:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 35/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
OK
Destroying test database for alias 'default'...
Now that we have the base.html template, we can easily add a top bar with a menu:
templates/base.html
<div class="container">
<ol class="breadcrumb my-4">
{% block breadcrumb %}
{% endblock %}
</ol>
{% block content %}
{% endblock %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 36/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
</div>
</body>
</html>
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 37/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
A nice touch I like to add is to change the font in the “logo” ( .navbar-brand ) of the
page.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 38/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 39/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
</head>
<body>
<!-- code suppressed for brevity -->
</body>
</html>
Now create a new CSS file named app.css inside the static/css folder:
static/css/app.css
.navbar-brand {
font-family: 'Peralta', cursive;
}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 40/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Forms
Forms are used to deal with user input. It’s a very common task in any web application
or website. The standard way to do it is through HTML forms, where the user input
some data, submit it to the server, and then the server does something with it.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 41/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Form processing is a fairly complex task because it involves interacting with many
layers of an application. There are also many issues to take care of. For example, all
data submitted to the server comes in a string format, so we have to transform it into a
proper data type (integer, float, date, etc.) before doing anything with it. We have to
validate the data regarding the business logic of the application. We also have to
clean, sanitize the data properly so to avoid security issues such as SQL Injection and
XSS attacks.
Good news is that the Django Forms API makes the whole process a lot easier,
automating a good chunk of this work. Also, the final result is a much more secure
code than most programmers would be able to implement by themselves. So, no
matter how simple the HTML form is, always use the forms API.
At first, I thought about jumping straight to the forms API. But I think it would be a
good idea for us to spend some time trying to understand the underlying details of
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 42/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
form processing. Otherwise, it will end up looking like magic, which is a bad thing,
because when things go wrong, you have no idea where to look for the problem.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 43/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
It’s one of the wireframes we drew in the previous tutorial. I now realize this may be a
bad example to start because this particular form involves processing data of two
different models: Topic (subject) and Post (message).
There’s another important aspect that we haven’t discussed it so far, which is user
authentication. We are only supposed to show this screen for authenticated users. This
way we can tell who created a Topic or a Post.
So let’s abstract some details for now and focus on understanding how to save user
input in the database.
myproject/urls.py
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topic
url(r'^boards/(?P<pk>\d+)/new/$', views.new_topic, name='new_topic'
url(r'^admin/', admin.site.urls),
]
The way we are building the URL will help us identify the correct Board.
boards/views.py
For now, the new_topic view function is looking exactly the same as the
board_topics. That’s on purpose, let’s take a step at a time.
Now we just need a template named new_topic.html to see some code working:
templates/new_topic.html
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
{% endblock %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 45/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
For now we just have the breadcrumb assuring the navigation. Observe that we
included the URL back to the board_topics view.
We still haven’t implemented a way to reach this new page, but if we change the URL
to http://127.0.0.1:8000/boards/2/new/, it should take us to the Python Board:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 46/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Note:
The result may be different for you if you haven't followed the steps from the previous tutorial. In my case,
I have three Board instances in the database, being Django = 1, Python = 2, and Random = 3. Those
numbers are the IDs from the database, used from the URL to identify the right resource.
boards/tests.py
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 47/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
class HomeTests(TestCase):
# ...
class BoardTopicsTests(TestCase):
# ...
class NewTopicTests(TestCase):
def setUp(self):
Board.objects.create(name='Django', description='Django board.'
def test_new_topic_view_success_status_code(self):
url = reverse('new_topic', kwargs={'pk': 1})
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
def test_new_topic_view_not_found_status_code(self):
url = reverse('new_topic', kwargs={'pk': 99})
response = self.client.get(url)
self.assertEquals(response.status_code, 404)
def test_new_topic_url_resolves_new_topic_view(self):
view = resolve('/boards/1/new/')
self.assertEquals(view.func, new_topic)
def test_new_topic_view_contains_link_back_to_board_topics_view(sel
new_topic_url = reverse('new_topic', kwargs={'pk': 1})
board_topics_url = reverse('board_topics', kwargs={'pk': 1})
response = self.client.get(new_topic_url)
self.assertContains(response, 'href="{0}"'.format(board_topics_u
OK
Destroying test database for alias 'default'...
templates/new_topic.html
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 49/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="id_subject">Subject</label>
<input type="text" class="form-control" id="id_subject" name="sub
</div>
<div class="form-group">
<label for="id_message">Message</label>
<textarea class="form-control" id="id_message" name="message" row
</div>
<button type="submit" class="btn btn-success">Post</button>
</form>
{% endblock %}
This is a raw HTML form created by hand using the CSS classes provided by
Bootstrap 4. It looks like this:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 50/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
In the <form> tag, we have to define the method attribute. This instructs the
browser on how we want to communicate with the server. The HTTP spec defines
several request methods (verbs). But for the most part, we will only be using GET and
POST request types.
GET is perhaps the most common request type. It’s used to retrieve data from the
server. Every time you click on a link or type a URL directly into the browser, you are
creating a GET request.
POST is used when we want to change data on the server. So, generally speaking,
every time we send data to the server that will result in a change in the state of a
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 51/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Django protects all POST requests using a CSRF Token (Cross-Site Request Forgery
Token). It’s a security measure to avoid external sites or applications to submit data to
our application. Every time the application receives a POST, it will first look for the
CSRF Token. If the request has no token, or the token is invalid, it will discard the
posted data.
{% csrf_token %}
Is a hidden field that’s submitted along with the other form data:
Another thing, we have to set the name of the HTML inputs. The name will be used to
retrieve the data on the server side.
subject = request.POST['subject']
message = request.POST['message']
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 52/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
So, a naïve implementation of a view that grabs the data from the HTML and starts a
new topic can be written like this:
if request.method == 'POST':
subject = request.POST['subject']
message = request.POST['message']
topic = Topic.objects.create(
subject=subject,
board=board,
starter=user
)
post = Post.objects.create(
message=message,
topic=topic,
created_by=user
)
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 53/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
This view is only considering the happy path, which is receiving the data and saving it
into the database. But there are some missing parts. We are not validating the data.
The user could submit an empty form or a subject that’s bigger than 255 characters.
So far we are hard-coding the User fields because we haven’t implemented the
authentication yet. But there’s an easy way to identify the logged in user. We will get to
that part in the next tutorial. Also, we haven’t implemented the view where we will list
all the posts within a topic, so upon success, we are redirecting the user to the page
where we list all the board topics.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 54/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
It looks like it worked. But we haven’t implemented the topics listing yet, so there’s
nothing to see here. Let’s edit the templates/topics.html file to do a proper listing:
templates/topics.html
{% extends 'base.html' %}
{% block title %}
{{ board.name }} - {{ block.super }}
{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item active">{{ board.name }}</li>
{% endblock %}
{% block content %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 55/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Topic</th>
<th>Starter</th>
<th>Replies</th>
<th>Views</th>
<th>Last Update</th>
</tr>
</thead>
<tbody>
{% for topic in board.topics.all %}
<tr>
<td>{{ topic.subject }}</td>
<td>{{ topic.starter.username }}</td>
<td>0</td>
<td>0</td>
<td>{{ topic.last_updated }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 56/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
We are using for the first time the topics property in the Board model. The topics
property is created automatically by Django using a reverse relationship. In the
previous steps, we created a Topic instance:
# ...
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 57/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
topic = Topic.objects.create(
subject=subject,
board=board,
starter=user
)
In the line board=board , we set the board field in Topic model, which is a
ForeignKey(Board) . With that, now our Board instance is aware that it has an
Topic instance associated with it.
Another important thing to note is that, inside Python code, we have to use
parenthesis: board.topics.all() , because all() is a method. When writing
code using the Django Template Language, in an HTML template file, we don’t use
parenthesis, so it’s just board.topics.all .
{{ topic.starter.username }}
Just create a path through the property using dots. We can pretty much access any
property of the User model. If we wanted the user’s email, we could use
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 58/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
topic.starter.email .
Since we are already modifying the topics.html template, let’s create the button that
takes us to the new topic screen:
templates/topics.html
{% block content %}
<div class="mb-4">
<a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New
</div>
<table class="table">
<!-- code suppressed for brevity -->
</table>
{% endblock %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 59/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
We can include a test to make sure the user can reach the New topic view from this
page:
boards/tests.py
class BoardTopicsTests(TestCase):
# ...
def test_board_topics_view_contains_navigation_links(self):
board_topics_url = reverse('board_topics', kwargs={'pk': 1})
homepage_url = reverse('home')
new_topic_url = reverse('new_topic', kwargs={'pk': 1})
response = self.client.get(board_topics_url)
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 60/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
self.assertContains(response, 'href="{0}"'.format(homepage_url)
self.assertContains(response, 'href="{0}"'.format(new_topic_url
Before we code the previous form example in a Django way, let’s write some tests for
the form processing:
boards/tests.py
class NewTopicTests(TestCase):
def setUp(self):
Board.objects.create(name='Django', description='Django board.'
User.objects.create_user(username='john', email='[email protected]',
# ...
def test_csrf(self):
url = reverse('new_topic', kwargs={'pk': 1})
response = self.client.get(url)
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 61/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
self.assertContains(response, 'csrfmiddlewaretoken')
def test_new_topic_valid_post_data(self):
url = reverse('new_topic', kwargs={'pk': 1})
data = {
'subject': 'Test title',
'message': 'Lorem ipsum dolor sit amet'
}
response = self.client.post(url, data)
self.assertTrue(Topic.objects.exists())
self.assertTrue(Post.objects.exists())
def test_new_topic_invalid_post_data(self):
'''
Invalid post data should not redirect
The expected behavior is to show the form again with validation
'''
url = reverse('new_topic', kwargs={'pk': 1})
response = self.client.post(url, {})
self.assertEquals(response.status_code, 200)
def test_new_topic_invalid_post_data_empty_fields(self):
'''
Invalid post data should not redirect
The expected behavior is to show the form again with validation
'''
url = reverse('new_topic', kwargs={'pk': 1})
data = {
'subject': '',
'message': ''
}
response = self.client.post(url, data)
self.assertEquals(response.status_code, 200)
self.assertFalse(Topic.objects.exists())
self.assertFalse(Post.objects.exists())
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 62/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
First thing, the tests.py file is already starting to get big. We will improve it soon,
breaking the tests into several files. But for now, let’s keep working on it.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 63/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
django.utils.datastructures.MultiValueDictKeyError: "'subject'"
======================================================================
FAIL: test_new_topic_invalid_post_data_empty_fields (boards.tests.NewTop
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/vitorfs/Development/myproject/django-beginners-guide/boar
self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200
----------------------------------------------------------------------
Ran 15 tests in 0.512s
We have one failing test and one error. Both related to invalid user input. Instead of
trying to fix it with the current implementation, let’s make those tests pass using the
Django Forms API.
The Forms API is available in the module django.forms . Django works with two
types of forms: forms.Form and forms.ModelForm . The Form class is a general
purpose form implementation. We can use it to process data that are not directly
associated with a model in our application. A ModelForm is a subclass of Form ,
and it’s associated with a model class.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 64/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Let’s create a new file named forms.py inside the boards’ folder:
boards/forms.py
class NewTopicForm(forms.ModelForm):
message = forms.CharField(widget=forms.Textarea(), max_length=4000)
class Meta:
model = Topic
fields = ['subject', 'message']
This is our first form. It’s a ModelForm associated with the Topic model. The
subject in the fields list inside the Meta class is referring to the subject field
in the Topic class. Now observe that we are defining an extra field named message .
This refers to the message in the Post we want to save.
boards/views.py
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 65/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
This is how we use the forms in a view. Let me remove the extra noise so we can focus
on the core of the form processing:
if request.method == 'POST':
form = NewTopicForm(request.POST)
if form.is_valid():
topic = form.save()
return redirect('board_topics', pk=board.pk)
else:
form = NewTopicForm()
return render(request, 'new_topic.html', {'form': form})
First we check if the request is a POST or a GET. If the request came from a POST, it
means the user is submitting some data to the server. So we instantiate a form
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 66/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Then, we ask Django to verify the data, check if the form is valid if we can save it in the
database: if form.is_valid(): . If the form was valid, we proceed to save the data
in the database using form.save() . The save() method returns an instance of the
Model saved into the database. So, since this is a Topic form, it will return the Topic
that was created: topic = form.save() . After that, the common path is to redirect
the user somewhere else, both to avoid the user re-submitting the form by pressing F5
and also to keep the flow of the application.
Now, if the data was invalid, Django will add a list of errors to the form. After that, the
view does nothing and returns in the last statement:
return render(request, 'new_topic.html', {'form': form}) . That means we
have to update the new_topic.html to display errors properly.
If the request was a GET, we just initialize a new and empty form using
form = NewTopicForm() .
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 67/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
OK
Destroying test database for alias 'default'...
The Django Forms API does much more than processing and validating the data. It
also generates the HTML for us.
Let’s update the new_topic.html template to fully use the Django Forms API:
templates/new_topic.html
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">Post</button>
</form>
{% endblock %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 68/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Well, our previous form was looking better, right? We are going to fix it in a moment.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 69/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
It can look broken right now but trust me; there’s a lot of things behind it right now.
And it’s extremely powerful. For example, if our form had 50 fields, we could render all
the fields just by typing {{ form.as_p }} .
And more, using the Forms API, Django will validate the data and add error messages
to each field. Let’s try submitting an empty form:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 70/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Note:
If you see something like this: when you submit the form, that's not Django. It's
your browser doing a pre-validation. To disable it add the novalidate attribute to your form tag:
<form method="post" novalidate>
You can keep it; there's no problem with it. It's just because our form is very simple right now, and we
don't have much data validation to see.
Another important thing to note is that: there is no such a thing as "client-side validation." JavaScript
validation or browser validation is just for usability purpose. And also to reduce the number of requests to
the server. Data validation should always be done on the server side, where we have full control over the
data.
It also handles help texts, which can be defined both in a Form class or in a Model
class:
boards/forms.py
class NewTopicForm(forms.ModelForm):
message = forms.CharField(
widget=forms.Textarea(),
max_length=4000,
help_text='The max length of the text is 4000.'
)
class Meta:
model = Topic
fields = ['subject', 'message']
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 71/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
boards/forms.py
class NewTopicForm(forms.ModelForm):
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 72/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
message = forms.CharField(
widget=forms.Textarea(
attrs={'rows': 5, 'placeholder': 'What is on your mind?'}
),
max_length=4000,
help_text='The max length of the text is 4000.'
)
class Meta:
model = Topic
fields = ['subject', 'message']
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 73/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
When working with Bootstrap or any other Front-End library, I like to use a Django
package called django-widget-tweaks. It gives us more control over the rendering
process, keeping the defaults and just adding extra customizations on top of it.
myproject/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'widget_tweaks',
'boards',
]
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 74/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
templates/new_topic.html
{% extends 'base.html' %}
{% load widget_tweaks %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{% csrf_token %}
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text }}
</small>
{% endif %}
</div>
{% endfor %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 75/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
There it is! So, here we are using the django-widget-tweaks. First, we load it in the
template by using the {% load widget_tweaks %} template tag. Then the usage:
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 76/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
The render_field tag is not part of Django; it lives inside the package we installed.
To use it we have to pass a form field instance as the first parameter, and then after we
can add arbitrary HTML attributes to complement it. It will be useful because then we
can assign classes based on certain conditions.
Now to implement the Bootstrap 4 validation tags, we can change the new_topic.html
template:
templates/new_topic.html
{% if form.is_bound %}
{% if field.errors %}
</div>
{% endfor %}
{% else %}
{% render_field field class="form-control is-valid" %}
{% endif %}
{% else %}
{% render_field field class="form-control" %}
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text }}
</small>
{% endif %}
</div>
{% endfor %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 78/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 79/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Invalid: we add the .is-invalid CSS class and add error messages in an
element with a class .invalid-feedback . The form field and the messages are
rendered in red.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 80/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Valid: we add the .is-valid CSS class so to paint the form field in green, giving
feedback to the user that this field is good to go.
The template code looks a little bit complicated, right? Well, the good news is that we
can reuse this snippet across the project.
myproject/
|-- myproject/
| |-- boards/
| |-- myproject/
| |-- templates/
| | |-- includes/ <-- here!
| | |-- base.html
| | |-- home.html
| | |-- new_topic.html
| | +-- topics.html
| +-- manage.py
+-- venv/
templates/includes/form.html
{% load widget_tweaks %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 81/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
<div class="form-group">
{{ field.label_tag }}
{% if form.is_bound %}
{% if field.errors %}
{% render_field field class="form-control is-invalid" %}
{% for error in field.errors %}
<div class="invalid-feedback">
{{ error }}
</div>
{% endfor %}
{% else %}
{% render_field field class="form-control is-valid" %}
{% endif %}
{% else %}
{% render_field field class="form-control" %}
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text }}
</small>
{% endif %}
</div>
{% endfor %}
templates/new_topic.html
{% extends 'base.html' %}
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 82/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{% csrf_token %}
{% include 'includes/form.html' %}
<button type="submit" class="btn btn-success">Post</button>
</form>
{% endblock %}
Now we are using Django Forms; we can add more tests to make sure it is running
smoothly:
boards/tests.py
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 83/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
class NewTopicTests(TestCase):
# ... other tests
Now we are using the assertIsInstance method for the first time. Basically we are
grabbing the form instance in the context data, and checking if it is a NewTopicForm .
In the last test, we added the self.assertTrue(form.errors) to make sure the
form is showing errors when the data is invalid.
Conclusions
In this tutorial, we focused on URLs, Reusable Templates, and Forms. As usual, we
also implement several test cases. That’s how we develop with confidence.
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 84/85
9/8/2019 A Complete Beginner's Guide to Django - Part 3
Our tests file is starting to get big, so in the next tutorial, we are going to refactor it to
improve the maintainability so to sustain the growth of our code base.
We are also reaching a point where we need to interact with the logged in user. In the
next tutorial, we are going to learn everything about authentication and how to protect
our views and resources.
I hope you enjoyed the third part of this tutorial series! The fourth part is coming out
next week, on Sep 25, 2017. If you would like to get notified when the fourth part is
out, you can subscribe to our mailing list.
The source code of the project is available on GitHub. The current state of the project
can be found under the release tag v0.3-lw. The link below will take you to the right
place:
https://github.com/sibtc/django-beginners-guide/tree/v0.3-lw
https://simpleisbetterthancomplex.com/series/2017/09/18/a-complete-beginners-guide-to-django-part-3.html 85/85