Django 1.8 Tutorial – 2. Polishing the App with Static Files, CSS, JS, etc.

The last article took several hours to write, so I’m going to take a break from writing and editing for a while. These tutorial posts will still happen, but they’ll be harder to read.

The previous tutorial created a “comment system”, and while it was a reasonable example of using ModelForms and generic View classes, it didn’t look like a real comment system. This tutorial polishes the original and makes it more like a real web app.

Here’s what it looks like:

It’s still not “nice”, but it’s getting there.

Serving Static Files

The first time I read about DJ-Static and the issue of serving static files, I was baffled. Wasn’t the application going to run on a web server? They serve static files all day long.

Well, it turns out Django isn’t a web server – it’s an application that maps requests for URLs to functions. You may have already had this “aha moment”: app servers are not web servers.

Web pages, however, are typically served from web servers, and also typically have links to images, CSS, and JS files, usually on the same server.

Django supports this through it’s static file server. The big, somewhat confusing, issue is: where these static files are stored.

It turns out they can be stored in many different places. The path in the URL is *not* the path in the file system. I know that sounds weird – but think of it like the way we use templates. The server searches for templates, by name, in multiple locations in the file system. URLs to static files map to static files in the Django application, but the application server searches for them.

This searching behavior allows us to store some CSS files in the app’s directory, under the ‘static/’ folder.

Static Files Can Override Other Static Files

My configuration will search for files in the filesystem first, in the ‘static/’ directory in the project root, and then search for static files in the app’s ‘static/’.

So, for example, if you were looking for “/static/comment/main.css”, it would first look for minimal/static/comment/main.css. If that didn’t exist, it would look for minimal/comment/static/comment/main.css, and deliver that.

This allows designers to override the programmers 🙂 An app can come with some static CSS and JS files, but these files can be overridden by identically named files in the project’s static/ directory.

Our static file config is this:

STATIC_URL = '/static/'

    os.path.abspath(BASE_DIR + '/static/'),

STATIC_URL is the path prefix that indicates that we want a static file.

STATICFILES_DIRS is an iterable with a list of search paths. The paths must be absolute, so we’re using the BASE_DIR variable, which was set earlier in the script.

Our static file is in minimal/comment/static/comment/comment.css, and in templates, it’s written as:

 href="{% static "comment/comment.css" %}" 

It’s not quite “standard” yet – there should be a “css” directory – but it’s close enough.

The file’s contents are:

.comment {
    border: 1px solid silver;
    margin: 10px;
    padding: 10px;
    width: 300px;
label {
    display: block;

There’s not much to it. The .comment style block draws the comment, but with a box around it. The label style forces the form widgets to appear below the labels. CSS without some context is nonsense, though. We’ll get back to this later.

Improvements to the App’s Model and Views

Let’s look at the changes, starting with the model. We removed the title field and replaced it with a snippet from the start of the text.

class Comment(models.Model):
    author = models.ForeignKey(auth.models.User)
    text = models.TextField(max_length=1024)

    def get_absolute_url(self):
        return reverse('comment_detail', kwargs={'pk': str(})

    def get_first_chars(self):
        return "%s..." % (self.text[0:30],)

    title = property(get_first_chars)

After making this change, you need to do a “./ makemigrations” and a “./ migrate” to sync the db.

Next, we modified I put a form in there, even though good practice says to use I’ll be bad today.

I added

from django import forms

The added a CommentForm, and altered the CommentList view:

class CommentForm(forms.Form):
    author = forms.ModelChoiceField(
    text = forms.CharField(

class CommentList(ListView):
    model = Comment

    # adding a form to a listview
    def get_context_data(self, **kwargs):
        form = CommentForm
        context = super(CommentList, self).get_context_data(**kwargs)
        context['form'] = form
        return context

Then, below this, removed ‘title’ from the fields lists.

The big change is that we now override get_context_data, and insert our form object, named ‘form’, into the context.

Which Way Do We Alter the Context?

I’ve seen two ways to alter the context in get_context_data(). I’m not sure which is more correct, but the one I decided to use conformed to the Django docs:

# This creates a new context, adds our object, and then passes it to the
# parent's get_context_data() method. The risk is that our object will 
# be destroyed.
class CommentList(ListView):
    model = Comment

    # adding a form to a listview
    def get_context_data(self, **kwargs):
        form = CommentForm
        context = {
            'form': form
        return super(CommentList, self).get_context_data(**context)

# This gets the context from the parent's get_context_data() method,
# then adds our form to it. This seems better.
class CommentList(ListView):
    model = Comment

    # adding a form to a listview
    def get_context_data(self, **kwargs):
        form = CommentForm
        context = super(CommentList, self).get_context_data(**kwargs)
        context['form'] = form
        return context

New Templates and UX

The template for the comment_list was also altered: comment_list.html is now:

{% extends "base.html" %}

{% block content %}
    {% for comment in object_list %}
    <div class="comment">
        <p>{{ comment.text }}</p>
            <a href="{% url 'comment_edit' %}">edit</a>
            <a href="{% url 'comment_delete' %}">delete</a></p>
    {% endfor %}

    <form method="post" action="{% url "comment_create" %}">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" />
{% endblock %}

There are a number of UI changes, including the use of class=”comment” and a DIV to wrap each comment, but the main event is at the bottom. There’s a form to enter a new comment.

The first thing to notice is that we can now use {{form.as_p}} in the template, because we added the form to our context.

The second thing to notice is action=”{% url “comment_create” %}”, which will cause our form to post to a different view, named comment_create.

There’s a little twist here – we’re using a hand-coded form, CommentForm, to post the data. The comment_create view then handles the POST with a generic view that uses an automatically created ModelForm object for the Comment model.

I did this because I couldn’t find an easy way to extract the ModelForm from the Model. Even if I could, I would have wanted to alter the text field so it used a Textarea widget.

comment_detail.html, comment_form.html and comment_confirm_delete.html were also edited, to have more links, and generally create a better UX.

The base.html was also altered, to include the static files.

    {% load static from staticfiles %}
        <link rel="stylesheet" href="{% static "main.css" %}">
        <link rel="stylesheet" href="{% static "comment/comment.css" %}">
        Global base template.
        {% block content %}
        {% endblock %}

The main thing to notice is that we use a new tag {% static %} that is similar to {% url %}. It invokes the static-file finding code to seek out and return the files that match.

The main.css file is a “CSS reset” file. I use only a couple resets:

* { box-sizing: border-box; }
body {
    font-family: sans-serif;
    margin: 0;
    padding: 0;

The first line changes the box model for everything so that box dimensions work like IE. It’s nonstandard, but it’s easier than the real CSS standards.

The other lines make it look a little more “web 1.5”, a la Craigslist.


Some code problems will be fixed and uploaded later this week.

The most glaring problem is that this form lacks a user login – you still choose the username from the dropdown. That’s obviously wrong. We need to add a login.

Another missing feature is that there’s no photo upload. Gawker and other sites copied this feature from anime boards and other fansites that allow photos in comments.

Future tutorials will add these features.


Random Notes


I heard on a podcast that the term “static” is wrong, and it should be “assets”. Every trade has its own jargon.

Attachment Size
django-comment-form.png 7.24 KB
minimal2.tgz 12.56 KB