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.

Complete source code for this chapter can be found on Github.

Pages app

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. Instead we can 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 make our new app.

(msg) $ python manage.py startapp pages

Then immediately update our settings.py file. I often forget to do this so it’s a good practice to just immediately add new apps to the settings.py file whenever you create them.

# msg_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',
]

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.

# msg_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.

(msg) $ touch pages/urls.py

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

# 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.

# 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 with superuser

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

Tests

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:

  • home
  • signup
  • login
  • logout

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.

# 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_name(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 = '[email protected]'

    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.

(msg) $ python manage.py test

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 yourself or rely on a Content Delivery Network (CDN). The second approach is simpler to implement 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 copy and paste here.

<!-- templates/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="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

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

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" 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.

Homepage with Bootstrap

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 not on learning HTML, CSS, and Bootstrap.

<!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="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <title>{% block title %}Social Messaging App{% endblock title %}</title>
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
      <a class="navbar-brand" href="{% url 'home' %}">Social Msg</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#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 class="nav-item">
              <a class="nav-link dropdown-toggle" href="#" 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="#">Change password</a>
                <div class="dropdown-divider"></div>
                <a class="dropdown-item" href="{% url 'logout' %}">Log out</a>
              </div>
            </li>
          </ul>
        {% else %}
          <form class="form-inline ml-auto">
            <a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
            <a href="{% url 'signup' %}" class="btn btn-primary ml-2">Sign up</a>
          </form>
        {% endif %}
      </div>
    </nav>
    {% block content %}
    {% endblock %}

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" 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

You may have noticed we don’t have any side margins our webpage content. If we wrap <div class="container"> around our block content Bootstrap will automatically provide appropriate side margins.

Here’s the updated code.

<!-- templates/base.html -->
...
<div class="container">
  {% block content %}
  {% endblock %}
</div>
...

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.

Homepage home margins

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 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.

<!-- 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

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.

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.

(msg) $ pipenv install django-crispy-forms

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

# msg_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'crispy_forms',
    '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.

# msg_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 }}.

<!-- 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.

<!-- 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

Our messaging 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.

Continue on to Chapter 11: Password Change and Reset.