Reply to comment

Django 1.8 Tutorial - 1. A Minimal Application Made Using Generic Class Based Views

I'm not a Django expert. I'm a Django novice writing this document to help other novices. There are probably errors, and I welcome corrections.

This is an intermediate level document for people who know how to program, are fairly comfortable with Python, have done one or two Django tutorials, and know the Django file layout, but haven't really "gotten" Django or the generic View classes.

We will create a small web application with all the CRUD operations in around 120 lines. The tutorial emphasizes using the most generic names.

Sources are attached, below.

Generic View Classes, the docs, and how to read them

The generic View classes are the best thing in Django. They allow you to create views with minimal work - these classes take care of all the usual features: displaying lists of objects, object details, forms to edit objects, forms to update objects, and interactions to delete objects.

They are called: ListView, DetailView, CreateView, UpdateView, and DeleteView.

The docs are confusing. Object oriented design uses inheritance hierarchies. A method that is available in a class may be defined in the class, or may be defined higher up in the hierarhcy.

In Django's case, most of the generic view class behavior is defined in mixin classes. The documentation also follows the inheritance hierarchy, so you don't have all the docs for a given class in the class - it's in the docs for the parent class or mixin class that provides a method or behavior.

To read the docs, you need to dig around the docs, particularly in the docs for the mixins.

So, when you're puzzling over what a View does, drill down into the Mixins. They are all listed in the section titled "Ancestors (MRO)." "MRO" means "method resolution order." This lists the order in which the methods inherited from mixins and the parent class are resolved.

(What's a mixin? It's like a library of code that's included into a class. Django has view classes, and they pull in code from various mixins, each one providing one or more methods or functions. These mixins are re-used across the views.)

Note: There's a simplified documentation reader named CCBV. It will make sense after you've read the regular docs, and written some code.

A Sample Model: Comments
For this tutorial, we're going to start making a simple comment system. This first try won't behave like a real comment system, but we'll be able to create, edit, and delete something resembling comments. To start it:

./manage.py startapp comment

Then, you add 'comment' to the INSTALLED_APPS in settings.py.

There is a single model, Comment. It's related to the default Django user auth system:

# models.py
from django.db import models
from django.contrib import auth
from django.core.urlresolvers import reverse

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

    def get_absolute_url(self):
       return reverse('comment_detail', args=[str(self.id)])

get_absolute_url will be explained later, but it allows some views to automatically send you to the detail view of the object after creating or updating the object.

Enable the Default Admin
After creating the model, enable it in the admin, so we can add some data. I won't get into this in detail - there are a lot of good tutorials out there.

# admin.py
from django.contrib import admin
from models import Comment

class CommentAdmin(admin.ModelAdmin):
    pass
admin.site.register(Comment, CommentAdmin)

Then, set up the superuser, log into the admin, and add a comment.

Generic View Classes Eliminate Forms

The CreateView and UpdateView classes allow you to create editing views.

What makes them nice is that they don't require Form classes.

What is a Form class? In Django, Form classes render forms and perform validation. They are one of the nicer Django features, because they relieve you from writing HTML forms, which are typically the most convoluted and often error-prone parts of web applications. The CreateView and UpdateView automatically create a Form object by inspecting the Model object.

In the project's urls.py, include the app's urls:

# urls.py
from django.conf.urls import include, url
from django.contrib import admin
import comment.urls

urlpatterns = [
    url(r'^c/', include(comment.urls)),
    url(r'^admin/', include(admin.site.urls)),
]

Then create the file urls.py for the app

touch comment/urls.py

This is the complete urls.py. It'll be explained in detail later:

from django.conf.urls import url
import views

urlpatterns = [
    url(r'^$',
        views.CommentList.as_view(),
        name='comment_list'),
    url(r'^(?P<pk>[0-9]+)/$',
        views.CommentDetail.as_view(),
        name='comment_detail'),
    url(r'^create/$',
        views.CommentCreate.as_view(),
        name='comment_create'),
    url(r'^(?P<pk>[0-9]+)/update/$',
        views.CommentUpdate.as_view(),
        name='comment_edit'),
    url(r'^(?P<pk>[0-9]+)/delete/$',
        views.CommentDelete.as_view(),
        name='comment_delete'),
]

Then, create views.py:

from django.views.generic import ListView, DetailView
from django.views.generic import CreateView, UpdateView, DeleteView
from django.core.urlresolvers import reverse_lazy
from comment.models import Comment

class CommentList(ListView):
    model = Comment

class CommentDetail(DetailView):
    model = Comment

class CommentCreate(CreateView):
    model = Comment
    fields = ['author', 'title', 'text']

class CommentUpdate(UpdateView):
    model = Comment
    fields = ['author', 'title', 'text']

class CommentDelete(DeleteView):
    model = Comment
    success_url = reverse_lazy('comment_list')

These two files are the heart of the application: routes and behaviors, external interface and internal state.

They won't display anything without templates. So, we need to take a little detour to explain how templates are named and found by the template system.

Templates

Templates are tagged up HTML files that display our objects.

To keep your application self-contained, you can put the templates in the app's directory. To enable this feature, in your settings.py, set 'APP_DIRS' to True:

'APP_DIRS': True,

Then create these two directories, where appname is your app's name:
appname/templates/
appname/templates/appname/

For our tutorial, the directories are comment/templates/ and comment/templates/comment/. See the sources for details.

The templates go in both directories. If you use template inheritance (and you should), the base.html file goes in templates/. The rest of the application templates go in templates/appname/.

Default Names for Templates

Though you can set the name of the template via the template_name property, we're going to use the default template names.

The default names are constructed using these patterns:

templates/appname/model_list.html
templates/appname/model_detail.html
templates/appname/model_form.html
templates/appname/model_confirm_delete.html

The model is the lowercased name of the Model. In our case, it's "comment", so the files are named "comment_list.html" etc.

You should also create a file, 'base.html" in the app's templates:

templates/base.html

You should stick with "base.html" because it is a convention. Here's our minimalist base.html:

# templates/base.html
<html>
    <body>
        {% block content %}
        {% endblock %}
    </body>
</html>

Contexts for these Templates

A "context" in Django is a dictionary of names and values that can be inserted into a template. A template cannot read any variables defined in the application; it can only read the context.

Each generic view defines values in the context, giving them generic names. For example, in the list, there's a list named "object_list" that contains the model objects retrieved from the database. In the detail view the object is named "object". The names of the objects can be changed, and additional objects can be defined in the context, but we will use the default names.

Looking at the URLs and CommentList Again

A quick perusal of urls.py and views.py shows that the urls point to the views, so we'll discuss each pair of the url configuration and the View class, and the template that they use.

This URL configuration maps the root url to the CommentList class, and it's named "comment_list".

    url(r'^$',
        views.CommentList.as_view(),
        name='comment_list'),
class CommentList(ListView):
    model = Comment

This view will render a list of comments using the comment_list.html template.

In the ListView, the context contains a list of found comments, and the name is "object_list". Here's our template, templates/comment/comment_list.html:

{% extends "base.html" %}

{% block content %}
    {% for comment in object_list %}
        <p> 
            {{ comment.title }} 
            <a href="{% url 'comment_detail' comment.pk %}">detail...</a>
        </p>
    {% endfor %}

    <a href="{% url 'comment_create' %}">Add...</a>
{% endblock %}

We loop over object_list and dispaly the title and a link to get details about the object.

Incidentally, Django will also define "comment_list" in addition to object_list. They will point to the same thing. This tutorial uses the generic names.

The tags {% url 'comment_create' %} and {% url 'comment_detail' comment.pk %} will be explained later.

Generic or Specific
The Django tutorials and some other ones prefer to use the model-specific names. I tend to favor the generic names, unless there are two objects, or two lists, or other situations where a generic name doesn't work. It's idiomatic, and generic, so you can reuse the same code.

CommentDetail

The CommentDetail extends the DetailView class, and displays the entire object. The URL includes a regex that captures a parameter named "pk", and passes that into the view.

    url(r'^(?P<pk>[0-9]+)/$',
        views.CommentDetail.as_view(),
        name='comment_detail'),
class CommentDetail(DetailView):
    model = Comment

Explaining regexes deserves a longer article, but here's a summary of what the expression in the url does.

The following regex means "capture a string of digits; call it 'pk'." "pk" is the generic name for a primary key. Django sometimes also uses "id", but we will use "pk" because it seems to be used in more places.

^(?P<pk>[0-9]+)/$

^ means "the start of the string". It prevents partial matches.

$ means "the end of the string". It prevents partial matches.

( ... ) means "capture this subpattern".

?P<...> means, "name this pattern" using the name between < and >

[0-9]+ is a regex that matches strings of digits.

The / before $ forces the URL to terminate with a /. (Django can add the / to URLs automatically by setting APPEND_SLASH to true.)

The DetailView expects a single named parameter (a kwarg) named "pk". It then gets that model object, putting it in "object" in the context.

It's then displayed using the comment_detail.html template:

{% extends "base.html" %}

{% block content %}

<h2>{{ object.title }}</h2>
<p><em>by {{ object.author.username }}</em></p>
<p>{{ object.text }}</p>

<a href="{% url 'comment_edit' object.pk %}">Edit</a>
|
<a href="{% url 'comment_delete' object.pk %}">Delete</a>

{% endblock %}

What is that {% url %} ?

In the above two templates, we've seen the use of the {% url %} tag. This tag creates URL from a URL's name. If you look at the urls.py, you'll notice that each call to url() contains a name parameter; that's the URL's name.

The {% url 'comment_list' %} "reverses" the name, and turns it back into a URL. In our app, it's "/c/".

The {% url 'comment_detail' comment.pk %} and {% url 'comment_edit' object.pk %} and {% url 'comment_delete' object.pk %} all have a second parameter that specify an object. The name "pk" is just the default name for the primary key. Likewise, object is the default name of the object.

Suppose that the pk of the object is 1.

{% url 'comment_detail' comment.pk %} resolves back to /c/1/.

{% url 'comment_edit' comment.pk %} resolves back to /c/1/edit/.

{% url 'comment_delete' comment.pk %} resolves back to /c/1/delete/.

These URLs are then embedded in A tags to form links.

The mechanism to reverse the URL uses the regex to reconstruct the URL. If you supply a value for "pk", it then replaces the named expression (?P<pk>[0-9]+) in the URL configuration.

CommentCreate and CommentUpdate

Creating and updating an object are similar, so I'll cover them together. The URLs are similar to CommentList and CommentDetail.

    url(r'^create/$',
        views.CommentCreate.as_view(),
        name='comment_create'),
    url(r'^(?P<pk>[0-9]+)/update/$',
        views.CommentUpdate.as_view(),
        name='comment_edit'),
class CommentCreate(CreateView):
    model = Comment
    fields = ['author', 'title', 'text']

class CommentUpdate(UpdateView):
    model = Comment
    fields = ['author', 'title', 'text']

Both these classes require a property, fields, to define what fields to include in the form.

Generic View Classes Eliminate Forms

CreateView and UpdateView don't require Form classes.

What is a Form class? In Django, Form classes render forms, perform validation, and display errors. They are one of the nicer Django features, because they relieve you from writing HTML forms, which are typically the most convoluted and often error-prone parts of web applications. The CreateView and UpdateView automatically create a Form object by inspecting the Model object.

The form for CommentCreate and CommentUpdate

The Django CreateView and UpdateView share a template, named model_form.html.

In our app, CommentCreate and CommentUpdate both use the comment_form.html template, and insert an object named "form" in the context.

The CommentUpdate view defines "object" in the class, setting it to the comment being edited. The CommentCreate view doesn't define "object" - we're creating a comment from nothing, so, there is no object. This template includes some logic to check for "object", to switch between headings for creating or editing the comment.

The template is templates/comment/comment_form.html:

{% extends "base.html" %}

{% block content %}
    {% if object %}
        <h2>Edit {{ object.title }}</h2>
    {% else %}
        <h2>Create</h2>
    {% endif %}
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" />
    </form>
{% endblock %}

The CommentDelete View
The CommentDelete view shows the form in comment_confirm_delete.html, which confirms deletion. It's just a modification of comment_form.html. The view defines "object" in the context, setting it to the object. The logic in the view is a little odd: if the form is requested via GET, it'll show the template and form. If it's submitted via POST, it'll delete the object.

Thus, the user flow first requests by GET, which displays the object and a confirmation. Then, when the form is POSTed, the object will be deleted. The user is then forwarded back to the list.

The view's code is:

class CommentDelete(DeleteView):
    model = Comment
    success_url = reverse_lazy('comment_list')

The template is:

{% extends "base.html" %}

{% block content %}

<form method="post">
    {% csrf_token %}
    <p>Are you sure you want to delete {{ object.title }}?</p>
    <input type="submit" value="yes" />
    <a href="{% url 'comment_list' %}">no</a>
</form>

{% endblock %}

What is {% csrf_token %}?
CSRF means Cross Site Request Forgery. It's an attack on the website. Search for it. Putting the {% csrf_token %} in your form prevents this attack. If you don't add this tag, Django will complain.

The Comment model's get_absolute_url() and reverse()
get_absolute_url() is called by the CreateView and UpdateView views to redirect the user to a page after an object is successfully created or edited.

    def get_absolute_url(self):
       return reverse('comment_detail', args=[str(self.id)])

The reverse() function works like the {% url %} tag, taking the name of a registered URL, and inserting the PK into the URL.

In this code, which was copied from the Django tutorial, we use positional arguments and self.id instead of self.pk. The following also works:

    def get_absolute_url(self):
       return reverse('comment_detail', args=[str(self.pk)])

You can also use kwargs, like this:

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

When I was learning this, I was looking for a way to use success_url to set the next url, just like we do in other views. However, I was stumped as to how it could determine the PK for a specific object. Well, I guess the answer is, "you can't set it in a class attribute like success_url." At that moment, you don't know the PK. You only know the PK when the request is made, or after the new object is saved.

pk, the Primary Key

Chances are, when you make a database, you call the primary key "id" or "TableNameID" or "tablename_id", depending on your habits of naming tables.

In Django, you can specify your primary key with similar names, but "pk" is preferred. Throughout applications, though, you will see the use of "pk" as an alias for the primary key. You will see the use of pk in the url() in urlpatterns, and also in the url tags in the templates. The generic views default to using "pk", so we should also use "pk" as the primary key.

More on Templates: Overriding Templates

Django allows you to override a template. It does this by searching for templates in different directories, so when your code says 'extend "base.html"', it searches for base.html across different directories. The normal way to override your app's base.html is to put it in the project's 'templates' directory. (This can be changed in the TEMPLATES setting, but other docs seem to do it this way.)

To enable this overriding behavior, you need to change the DIRS value in the TEMPLATES list, in your settings.py:

'DIRS': [os.path.join(BASE_DIR, 'templates')],

That adds templates/ in the project's directory (the one with manage.py in it) to the template search path. Django will first look for the template in the project's templates/, then in the app's templates/.

The reason for the "base.html" convention is simple: if different apps all use "base.html" as the name for the base template, a single "base.html" file in the project's templates can be used by all the apps. Just like using "pk" and "object" and "object_list" and "*_set", it's more generic.

Conclusion
The attached app doesn't really look right. The widgets should be changed, and there's no CSS, and it really doesn't function like a comment system. I'll fix these for the next article.

AttachmentSize
minimal.tgz7.56 KB

Reply

The content of this field is kept private and will not be shown publicly.
  • Lines and paragraphs break automatically.

More information about formatting options

2 + 4 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.