Chapter 7. Prettification: Layout and Styling, and What to Test About It

We’re starting to think about releasing the first version of our site, but we’re a bit embarrassed by how ugly it looks at the moment. In this chapter, we’ll cover some of the basics of styling, including integrating an HTML/CSS framework called Bootstrap. We’ll learn how static files work in Django, and what we need to do about testing them.

What to Functionally Test About Layout and Style

Our site is undeniably a bit unattractive at the moment (Figure 7-1).

Note

If you spin up your dev server with manage.py runserver, you may run into a database error “table lists_item has no column named list_id”. You need to update your local database to reflect the changes we made in models.py. Use manage.py migrate.

We can’t be adding to Python’s reputation for being ugly, so let’s do a tiny bit of polishing. Here’s a few things we might want:

  • A nice large input field for adding new and existing lists
  • A large, attention-grabbing, centered box to put it in

How do we apply TDD to these things? Most people will tell you you shouldn’t test aesthetics, and they’re right. It’s a bit like testing a constant, in that tests usually wouldn’t add any value.

Our homepage
Figure 7-1. Our homepage, looking a little ugly…

But we can test the implementation of our aesthetics—just enough to reassure ourselves that things are working. For example, we’re going to use Cascading Style Sheets (CSS) for our styling, and they are loaded as static files. Static files can be a bit tricky to configure (especially, as we’ll see later, when you move off your own PC and onto a hosting site), so we’ll want some kind of simple “smoke test” that the CSS has loaded. We don’t have to test fonts and colours and every single pixel, but we can do a quick check that the main input box is aligned the way we want it on each page, and that will give us confidence that the rest of the styling for that page is probably loaded too.

We start with a new test method inside our functional test:

functional_tests/tests.py (ch07l001)

class NewVisitorTest(LiveServerTestCase):
    [...]


    def test_layout_and_styling(self):
        # Edith goes to the home page
        self.browser.get(self.live_server_url)
        self.browser.set_window_size(1024, 768)

        # She notices the input box is nicely centered
        inputbox = self.browser.find_element_by_id('id_new_item')
        self.assertAlmostEqual(
            inputbox.location['x'] + inputbox.size['width'] / 2,
            512,
            delta=5
        )

A few new things here. We start by setting the window size to a fixed size. We then find the input element, look at its size and location, and do a little maths to check whether it seems to be positioned in the middle of the page. assertAlmostEqual helps us to deal with rounding errors by letting us specify that we want our arithmetic to work to within plus or minus five pixels.

If we run the functional tests, we get:

$ python3 manage.py test functional_tests
Creating test database for alias 'default'...
.F
======================================================================
FAIL: test_layout_and_styling (functional_tests.tests.NewVisitorTest)
 ---------------------------------------------------------------------
Traceback (most recent call last):
  File "/workspace/superlists/functional_tests/tests.py", line 104, in
test_layout_and_styling
    delta=5
AssertionError: 111.0 != 512 within 5 delta

 ---------------------------------------------------------------------
Ran 2 tests in 9.188s

FAILED (failures=1)
Destroying test database for alias 'default'...

That’s the expected failure. Still, this kind of FT is easy to get wrong, so let’s use a quick-and-dirty “cheat” solution, to check that the FT also passes when the input box is centered. We’ll delete this code again almost as soon as we’ve used it to check the FT:

lists/templates/home.html (ch07l002)

<form method="POST" action="/lists/new">
    <p style="text-align: center;">
        <input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
    </p>
    {% csrf_token %}
</form>

That passes, which means the FT works. Let’s extend it to make sure that the input box is also center-aligned on the page for a new list:

functional_tests/tests.py (ch07l003)

    # She starts a new list and sees the input is nicely
    # centered there too
    inputbox.send_keys('testing\n')
    inputbox = self.browser.find_element_by_id('id_new_item')
    self.assertAlmostEqual(
        inputbox.location['x'] + inputbox.size['width'] / 2,
        512,
        delta=5
    )

That gives us another test failure:

  File "/workspace/superlists/functional_tests/tests.py", line 114, in
test_layout_and_styling
    delta=5
AssertionError: 111.0 != 512 within 5 delta

Let’s commit just the FT:

$ git add functional_tests/tests.py
$ git commit -m "first steps of FT for layout + styling"

Now it feels like we’re justified in finding a “proper” solution to our need for some better styling for our site. We can back out our hacky <p style="text-align: center">:

$ git reset --hard

Warning

git reset --hard is the “take off and nuke the site from orbit” Git command, so be careful with it—it blows away all your un-committed changes. Unlike almost everything else you can do with Git, there’s no way of going back after this one.

Prettification: Using a CSS Framework

Design is hard, and doubly so now that we have to deal with mobile, tablets, and so forth. That’s why many programmers, particularly lazy ones like me, are turning to CSS frameworks to solve some of those problems for them. There are lots of frameworks out there, but one of the earliest and most popular is Twitter’s Bootstrap. Let’s use that.

You can find bootstrap at http://getbootstrap.com/.

We’ll download it and put it in a new folder called static inside the lists app:[8]

$ wget -O bootstrap.zip https://github.com/twbs/bootstrap/releases/download/\
v3.1.0/bootstrap-3.1.0-dist.zip
$ unzip bootstrap.zip
$ mkdir lists/static
$ mv dist lists/static/bootstrap
$ rm bootstrap.zip

Bootstrap comes with a plain, uncustomised installation in the dist folder. We’re going to use that for now, but you should really never do this for a real site—vanilla Bootstrap is instantly recognisable, and a big signal to anyone in the know that you couldn’t be bothered to style your site. Learn how to use LESS and change the font, if nothing else! There is info in Bootstrap’s docs, or there’s a good guide here.

Our lists folder will end up looking like this:

$ tree lists
lists
├── __init__.py
├── __pycache__
│   └── [...]
├── admin.py
├── models.py
├── static
│   └── bootstrap
│       ├── css
│       │   ├── bootstrap.css
│       │   ├── bootstrap.css.map
│       │   ├── bootstrap.min.css
│       │   ├── bootstrap-theme.css
│       │   ├── bootstrap-theme.css.map
│       │   └── bootstrap-theme.min.css
│       ├── fonts
│       │   ├── glyphicons-halflings-regular.eot
│       │   ├── glyphicons-halflings-regular.svg
│       │   ├── glyphicons-halflings-regular.ttf
│       │   └── glyphicons-halflings-regular.woff
│       └── js
│           ├── bootstrap.js
│           └── bootstrap.min.js
├── templates
│   ├── home.html
│   └── list.html
├── tests.py
├── urls.py
└── views.py

If we have a look at the “Getting Started” section of the Bootstrap documentation, you’ll see it wants our HTML template to include something like this:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Bootstrap 101 Template</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- Bootstrap -->
        <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
      </head>
      <body>
        <h1>Hello, world!</h1>
        <script src="http://code.jquery.com/jquery.js"></script>
        <script src="js/bootstrap.min.js"></script>
      </body>
    </html>

We already have two HTML templates. We don’t want to be adding a whole load of boilerplate code to each, so now feels like the right time to apply the “Don’t repeat yourself” rule, and bring all the common parts together. Thankfully, the Django template language makes that easy using something called template inheritance.

Django Template Inheritance

Let’s have a little review of what the differences are between home.html and list.html:

$ diff lists/templates/home.html lists/templates/list.html
7,8c7,8
<         <h1>Start a new To-Do list</h1>
<         <form method="POST" action="/lists/new">
---
>         <h1>Your To-Do list</h1>
>         <form method="POST" action="/lists/{{ list.id }}/add_item">
11a12,18
>
>         <table id="id_list_table">
>             {% for item in list.item_set.all %}
>                 <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
>             {% endfor %}
>         </table>
>

They have different header texts, and their forms use different URLs. On top of that, list.html has the additional <table> element.

Now that we’re clear on what’s in common and what’s not, we can make the two templates inherit from a common “superclass” template. We’ll start by making a copy of home.html:

$ cp lists/templates/home.html lists/templates/base.html

We make this into a base template which just contains the common boilerplate, and mark out the “blocks”, places where child templates can customise it:

lists/templates/base.html

<html>
<head>
    <title>To-Do lists</title>
</head>

<body>
    <h1>{% block header_text %}{% endblock %}</h1>
    <form method="POST" action="{% block form_action %}{% endblock %}">
        <input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
        {% csrf_token %}
    </form>
    {% block table %}
    {% endblock %}
</body>
</html>

The base template defines a series of areas called “blocks”, which will be places that other templates can hook in and add their own content. Let’s see how that works in practice, by changing home.html so that it “inherits from” base.html:

lists/templates/home.html

{% extends 'base.html' %}

{% block header_text %}Start a new To-Do list{% endblock %}

{% block form_action %}/lists/new{% endblock %}

You can see that lots of the boilerplate HTML disappears, and we just concentrate on the bits we want to customise. We do the same for list.html:

lists/templates/list.html

{% extends 'base.html' %}

{% block header_text %}Your To-Do list{% endblock %}

{% block form_action %}/lists/{{ list.id }}/add_item{% endblock %}

{% block table %}
    <table id="id_list_table">
        {% for item in list.item_set.all %}
            <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
        {% endfor %}
    </table>
{% endblock %}

That’s a refactor of the way our templates work. We rerun the FTs to make sure we haven’t broken anything…

AssertionError: 111.0 != 512 within 5 delta

Sure enough, they’re still getting to exactly where they were before. That’s worthy of a commit:

$ git diff -b
# the -b means ignore whitespace, useful since we've changed some html indenting
$ git status
$ git add lists/templates # leave static, for now
$ git commit -m "refactor templates to use a base template"

Integrating Bootstrap

Now it’s much easier to integrate the boilerplate code that Bootstrap wants—we won’t add the JavaScript yet, just the CSS:

lists/templates/base.html (ch07l006)

<!DOCTYPE html>
<html lang="en">

<head>
    <title>To-Do lists</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
[...]

Rows and Columns

Finally, let’s actually use some of the Bootstrap magic! You’ll have to read the documentation yourself, but should be able to use a combination of the grid system and the text-center class to get what we want:

lists/templates/base.html (ch07l007)

<body>
<div class="container">

    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <div class="text-center">
                <h1>{% block header_text %}{% endblock %}</h1>
                <form method="POST" action="{% block form_action %}{% endblock %}">
                    <input name="item_text" id="id_new_item"
                           placeholder="Enter a to-do item"
                    />
                    {% csrf_token %}
                </form>
            </div>
        </div>
    </div>

    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            {% block table %}
            {% endblock %}
        </div>
    </div>

</div>
</body>

(If you’ve never seen an HTML tag broken up over several lines, that <input> may be a little shocking. It is definitely valid, but you don’t have to use it if you find it offensive. ;)

Tip

Take the time to browse through the Bootstrap documentation, if you’ve never seen it before. It’s a shopping trolley brimming full of useful tools to use in your site.

Does that work?

AssertionError: 111.0 != 512 within 5 delta

Hmm. No. Why isn’t our CSS loading?

Static Files in Django

Django, and indeed any web server, needs to know two things to deal with static files:

  1. How to tell when a URL request is for a static file, as opposed to for some HTML that’s going to be served via a view function
  2. Where to find the static file the user wants

In other words, static files are a mapping from URLs to files on disk.

For item 1, Django lets us define a URL “prefix” to say that any URLs which start with that prefix should be treated as requests for static files. By default, the prefix is /static/. It’s defined in settings.py:

superlists/settings.py

[...]

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/

STATIC_URL = '/static/'

The rest of the settings we will add to this section are all to do with item 2: finding the actual static files on disk.

While we’re using the Django development server (manage.py runserver), we can rely on Django to magically find static files for us—it’ll just look in any subfolder of one of our apps called static.

You now see why we put all the Bootstrap static files into lists/static. So why are they not working at the moment? It’s because we’re not using the /static/ URL prefix. Have another look at the link to the CSS in base.html:

lists/templates/base.html

<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">

To get this to work, we need to change it to:

lists/templates/base.html

<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">

When runserver sees the request, it knows that it’s for a static file because it begins with /static/. It then tries to find a file called bootstrap/css/bootstrap.min.css, looking in each of our app folders for subfolders called static, and it should find it at lists/static/bootstrap/css/bootstrap.min.css.

So if you take a look manually, you should see it works, as in Figure 7-2.

The list page with centered header
Figure 7-2. Our site starts to look a little better…

Switching to StaticLiveServerTestCase

If you run the FT though, it won’t pass:

AssertionError: 111.0 != 512 within 5 delta

That’s because, although runserver automagically finds static files, LiveServerTestCase doesn’t. Never fear though, the Django developers have made a more magical test class called StaticLiveServerTestCase (see the docs).

Let’s switch to that:

functional_tests/tests.py

@@ -1,8 +1,8 @@
-from django.test import LiveServerTestCase
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
 from selenium import webdriver
 from selenium.webdriver.common.keys import Keys

-class NewVisitorTest(LiveServerTestCase):
+class NewVisitorTest(StaticLiveServerTestCase):

And now it will now find the new CSS, which will get our test to pass:

$ python3 manage.py test functional_tests
Creating test database for alias 'default'...
..
 ---------------------------------------------------------------------
Ran 2 tests in 9.764s

Note

At this point, Windows users may see some (harmless, but distracting) error messages that say socket.error: [WinError 10054] An existing connection was forcibly closed by the remote host. Add a self.browser.refresh() just before the self.browser.quit() in tearDown to get rid of them. The issue is being tracked in this bug on the Django tracker.

Hooray!

Using Bootstrap Components to Improve the Look of the Site

Let’s see if we can do even better, using some of the other tools in Bootstrap’s panoply.

Jumbotron!

Bootstrap has a class called jumbotron for things that are meant to be particularly prominent on the page. Let’s use that to embiggen the main page header and the input form:

lists/templates/base.html (ch07l009)

    <div class="col-md-6 col-md-offset-3 jumbotron">
        <div class="text-center">
            <h1>{% block header_text %}{% endblock %}</h1>
            <form method="POST" action="{% block form_action %}{% endblock %}">
                [...]

Tip

When hacking about with design and layout, it’s best to have a window open that we can hit refresh on, frequently. Use python3 manage.py runserver to spin up the dev server, and then browse to http://localhost:8000 to see your work as we go.

Large Inputs

The jumbotron is a good start, but now the input box has tiny text compared to everything else. Thankfully, Bootstrap’s form control classes offer an option to set an input to be “large”:

lists/templates/base.html (ch07l010)

<input name="item_text" id="id_new_item"
       class="form-control input-lg"
       placeholder="Enter a to-do item"
/>

Table Styling

The table text also looks too small compared to the rest of the page now. Adding the Bootstrap table class improves things:

lists/templates/list.html (ch07l011)

    <table id="id_list_table" class="table">

Using Our Own CSS

Finally I’d like to just offset the input from the title text slightly. There’s no ready-made fix for that in Bootstrap, so we’ll make one ourselves. That will require specifying our own CSS file:

lists/templates/base.html

<head>
    <title>To-Do lists</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <link href="/static/base.css" rel="stylesheet" media="screen">
</head>

We create a new file at lists/static/base.css, with our new CSS rule. We’ll use the id of the input element, id_new_item, to find it and give it some styling:

lists/static/base.css

#id_new_item {
    margin-top: 2ex;
}

All that took me a few goes, but I’m reasonably happy with it now (Figure 7-3).

If you want to go further with customising Bootstrap, you need to get into compiling LESS. I definitely recommend taking the time to do that some day. LESS and other pseudo-CSS-alikes like SCSS are a great improvement on plain old CSS, and a useful tool even if you don’t use Bootstrap. I won’t cover it in this book, but you can find resources on the Internets. Here’s one, for example.

A last run of the functional tests, to see if everything still works OK?

$ python3 manage.py test functional_tests
Creating test database for alias 'default'...
..
 ---------------------------------------------------------------------
Ran 2 tests in 10.084s

OK
Destroying test database for alias 'default'...
Screenshot of lists page with big styling
Figure 7-3. The lists page, with all big chunks…

That’s it! Definitely time for a commit:

$ git status # changes tests.py, base.html, list.html + untracked lists/static
$ git add .
$ git status # will now show all the bootstrap additions
$ git commit -m "Use Bootstrap to improve layout"

What We Glossed Over: collectstatic and Other Static Directories

We saw earlier that the Django dev server will magically find all your static files inside app folders, and serve them for you. That’s fine during development, but when you’re running on a real web server, you don’t want Django serving your static content—using Python to serve raw files is slow and inefficient, and a web server like Apache or Nginx can do this all for you. You might even decide to upload all your static files to a CDN, instead of hosting them yourself.

For these reasons, you want to be able to gather up all your static files from inside their various app folders, and copy them into a single location, ready for deployment. This is what the collectstatic command is for.

The destination, the place where the collected static files go, is defined in settings.py as STATIC_ROOT. In the next chapter we’ll be doing some deployment, so let’s actually experiment with that now. We’ll change its value to a folder just outside our repo—I’m going to make it a folder just next to the main source folder:

workspace
│    ├── superlists
│    │    ├── lists
│    │    │     ├── models.py
│    │    │
│    │    ├── manage.py
│    │    ├── superlists
│    │
│    ├── static
│    │    ├── base.css
│    │    ├── etc...

The logic is that the static files folder shouldn’t be a part of your repository—we don’t want to put it under source control, because it’s a duplicate of all the files that are inside lists/static.

Here’s a neat way of specifying that folder, making it relative to the location of project base directory:

superlists/settings.py (ch07l018)

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/

STATIC_URL = '/static/'
STATIC_ROOT = os.path.abspath(os.path.join(BASE_DIR, '../static'))

Take a look at the top of the settings file, and you’ll see how that BASE_DIR variable is helpfully defined for us, using __file__ (which itself is a really, really useful Python built-in).

Anyway, let’s try running collectstatic:

$ python3 manage.py collectstatic

You have requested to collect static files at the destination
location as specified in your settings:

/workspace/static

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel:
yes

[...]
Copying '/workspace/superlists/lists/static/bootstrap/fonts/glyphicons-halfling
s-regular.svg'

74 static files copied to '/workspace/static'.

And if we look in ../static, we’ll find all our CSS files:

$ tree ../static/
../static/
├── admin
│   ├── css
│   │   ├── base.css

[...]

│       └── urlify.js
├── base.css
└── bootstrap
    ├── css
    │   ├── bootstrap.css
    │   ├── bootstrap.min.css
    │   ├── bootstrap-theme.css
    │   └── bootstrap-theme.min.css
    ├── fonts
    │   ├── glyphicons-halflings-regular.eot
    │   ├── glyphicons-halflings-regular.svg
    │   ├── glyphicons-halflings-regular.ttf
    │   └── glyphicons-halflings-regular.woff
    └── js
        ├── bootstrap.js
        └── bootstrap.min.js

10 directories, 74 files

collectstatic has also picked up all the CSS for the admin site. It’s one of Django’s powerful features, and we’ll find out all about it one day, but we’re not ready to use that yet, so let’s disable it for now:

superlists/settings.py

INSTALLED_APPS = (
    #'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'lists',
)

And we try again:

$ rm -rf ../static/
$ python3 manage.py collectstatic --noinput
Copying '/workspace/superlists/lists/static/base.css'
Copying '/workspace/superlists/lists/static/bootstrap/js/bootstrap.min.js'
Copying '/workspace/superlists/lists/static/bootstrap/js/bootstrap.js'
[...]

13 static files copied to '/workspace/static'.

Much better.

Anyway, now we know how to collect all the static files into a single folder, where it’s easy for a web server to find them. We’ll find out all about that, including how to test it, in the next chapter!

For now let’s save our changes to settings.py:

$ git diff # should show changes in settings.py*
$ git commit -am "set STATIC_ROOT in settings and disable admin"

A Few Things That Didn’t Make It

Inevitably this was only a whirlwind tour of styling and CSS, and there were several topics that I’d hoped to cover in more depth that didn’t make it. Here’s a few candidates for further study:

  • Customising bootstrap with LESS
  • The {% static %} template tag, for more DRY and less hard-coded URLs
  • Client-side packaging tools, like bower


[8] On Windows, you may not have wget and unzip, but I’m sure you can figure out how to download Bootstrap, unzip it, and put the contents of the dist folder into the lists/static/bootstrap folder.

Get Test-Driven Development with Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.