Django by errors - a different Django tutorial

Introduction

In this tutorial, we will focus on how to build a recipe site by taking one step at a time, seeing what error is waiting for us next.

When learning a new programming language or framework, there are so many guides and tutorials on how to do everything right. But what do you do the first time you get an error?

Here, we will be accustomed to as many error messages as possible while building a small app to store recipes.

In this tutorial, you will also learn how to use South for database migrations, and Twitter Bootstrap for a nice default site styling.

About Django

Django is a web application framework written in Python. As Python, it is Free software, licenced under the BSD license, and it has lots of documentation and a large community.

Django fosters rapid development, using an easy-to-learn and easy-to-read language like Python will let you not only make the computer understand what you mean, but also the people who may eventually inherit your scripts.

The components of Django are loosely coupled, so you can use another templating language instead of Django’s own, and you can even change the model layer to use Sqlalchemy instead of the built-in Django ORM (Object relational mapper: the layer that translates your models to database language).

In this tutorial, you will create a project, and then an application in that project. Django is built in a way that you will also be able to use third party applications inside your project, and you will in most cases start with a couple of Django applications. You can then build your apps in your projects in a way that you can mix them around and reuse them in other projects.

One of the most important parts is that you should not write the same code over and over again. So in Django projects you should not see long sections duplicated over and over. The “class based views” we are going to use is one good improvement to this, and the template inheritance system is another way you will not repeat yourself.

Simple steps

In this part, you will learn the most basic parts of Django, to get a feeling of how Django applications are built.

We assume you have a computer with Python installed, as all modern operating systems come with Python installed. To check if python is installed, open up a terminal, write “python” and press enter. If the command was not found, you should install Python.

We will also use “virtualenv”. It should not be too hard to install, just check out their official installation documentation.

Starting a project

Find a place where you want your project, and create a virtual environment to keep your requirements and dependencies separated from the rest of your python system:

$ virtualenv --no-site-packages env

And activate your new virtual environment:

$ source env/bin/activate

You are now “inside” this virtual environment. You can type “deactivate” to get out of it. Each time you open a terminal and want to work on a project in a virtual environment, you need to activate it using the command above. But be sure to be inside when installing Django:

$ pip install django

Now that Django is installed you have a new command inside the “bin” folder in env, which you can use to start a new Django project. I have named mine “recipes-project”:

$ ./env/bin/django-admin.py startproject recipes_project

Go into the project folder, make the manage.py script runnable and start the built-in webserver:

$ cd recipes_project
$ python manage.py runserver

Now go to localhost:8000 in your web browser. You can stop the server by pressing ctrl-c as suggested by the command output.

_images/it_worked.png

The page congratulates you, and tells you about your next steps: you should update your database settings and create an app. The apps are meant to be reusable components that you can tie together when building projects. We will do this after a little break.

File structure before creating an app

Inside the folder that was created with the start-project command, you will see the manage.py file and a folder with the same as the project folder itself.

Contents of project folder:

$ ls
manage.py  recipes_project

This subfolder is where project settings and common configuration is stored.

If you list the contents of the subfolder, you will see a special __init__ file that by being there makes the folder to be a Python package, a settings file, the toplevel url routing file, and a wsgi file for running the code by a server. You will need to notice the location of your settings.py and the urls.py for later:

$ ls recipes_project
__init__.py   settings.py   urls.py   wsgi.py
__init__.pyc  settings.pyc  urls.pyc  wsgi.pyc

The pyc-files are compiled versions of the source files. We do not need to bother too much about them, and if you remove them, they will get recreated when the source files are run again.

Database setup

The welcoming page told us to setup the database. The database settings are part of the settings.py file in the configuration folder. Open up recipes_project/settings.py in your favourite text editor, and change the database settings: Append sqlite3 to the ENGINE field and add a database name to the NAME field, “database.db” is a good name:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'database.db',
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}

The database name will be the name of a local file in your project folder. Sqlite is a single-file database system that is easy to use when developing, but not recommended in a production for large sites.

Creating an app

The welcoming page also wanted you to create an app. Do this using the manage.py command in the project folder:

python manage.py startapp recipes

This will create a new folder structure for the new app besides “manage.py” and the inner “recipes_project”, like this:

$ ls
manage.py  recipes  recipes_project

And the new recipes folder contains this:

$ ls recipes
__init__.py  models.py  tests.py  views.py

Activating the app

Now, you should enable your new app in the settings.py, by adding the name of your app to the INSTALLED_APPS, near the bottom of the file. The section should look something like:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'recipes',
)

The extra comma at the end is optional on the last line, but I recommend it, as it makes it easier to add another line later.

Now, to route traffic to the newly created app, we also need to add a line to the list of url patterns Django will use to match incoming requests. In the project level urls.py, you will see a line like this:

# url(r'^recipes_project/', include('recipes_project.foo.urls')),

The code with “#” in front is “commented out” and will not run. To make it active, remove the “#”. We will also change the line itself so it reads:

url(r'^recipes/', include('recipes.urls')),

Every address that starts with “recipes” will now point to a urls file of your recipes app, where the actual routes to views will happen.

It is useful to keep a terminal always running python manage.py runserver, and use another terminal window or tab for all the other commands you need to run. Remember to run the activate command in your new terminal.

Refresh the browser and see that complains: “No module named urls”

_images/no_module_named_urls.png

The line we just activated tells Django to look for url patterns in a file at “recipes/urls.py”, but that file does not exist yet. Copy the urls.py from the project folder into the app folder, and remove all the commented code and url patterns so that the new file looks like this:

from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
        )

Go to the browser and refresh. Now it says “Page not found (404)” which is a generic error message about a page not being found, but this also tells you what alternatives you have.

_images/page_not_found.png

The page suggests that you should add /recipes/ to the address field of your browser. In the urls.py you have created a rule that sends everything starting with recipes/ to the recipes app. Go ahead, add recipes to the browser location, and see that you get the first “It worked!” page again as there were no errors, but also, no contents.

Models, views and templates

There are different ways to organize code so it will not end up as a pile of spaghetti. Have a look again in the recipes app folder, you’ll see four files ending in ‘.py’. The __init__ is needed for the Python module that the app is to work, models will contain your models, tests will contain your tests, and views is the code that will build up different “pages”:

$ ls recipes
__init__.py   models.py   tests.py  urls.pyc
__init__.pyc  models.pyc  urls.py   views.py

Later we will add templates as well: HTML code that will decide the layout and design of your pages. The templates folder is not created automatically as it is possible to put templates other places as well.

If you are coming from another language or framework, you will eventually see that the templates are stricter than you are used to. You are not allowed to put tons of functionality into the template code A graphical designer should be able to understand and change the templates without knowing Python or Django.

Your first model: Food

That’s enough theory for a while. Now we will add a very simple model to models.py. This is the model for all the types of food we will use in the recipes. It will only have one field we need to know of, the name of the food objects. Django will automatically give it an id field for the primary key. Add the following class to recipes/models.py:

class Food(models.Model):
    name = models.CharField(max_length=20)

This model has to be used by the database. Django has a manage command called syncdb that will setup and all tables needed by Django for us. But wait a minute. Using a third party tool called south we can get database migrations as well.

Set up database migration support

Database migrations let you script the database changes so you can go from one version to another without manually executing alter table or other SQL commands. You can also use this for data migrations, but we will not get into that now. You need a third party app called “South” to do this. There have been discussions about taking all or parts of South into the core of Django

In settings.py, add 'south', to the bottom of the INSTALLED_APPS to use that app as well as your own. When saving the file, the running “runserver” process will stop, telling:

Error: No module named south

You need to install South:

$ pip install south

And restart your server.

To create your first migration belonging to the recipes app/module, use the init subcommand:

$ python manage.py schemamigration recipes --init

This will only create the migration, not do anything to the database, as you can create more migrations and execute them at the same time. It will also prevent the syncdb command from creating your databases without migration support.

To actually run this command, you need to run the management command migrate. This will only take care of your new app (since this is the only one with migrations defined). To do both syncdb and migrate at the same time, run:

$ python manage.py syncdb --migrate

The first time syncdb is run, it will ask you to create a user. We will soon be using the built-in admin interface where you later can create users, but to log in and create users, you need a user, so please answer “yes” and fill in the information. The output will look similar to this:

Superuser created successfully.
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)
Migrating...
Running migrations for recipes:
 - Migrating forwards to 0001_initial.
 > recipes:0001_initial
 - Loading initial data for recipes.
Installed 0 object(s) from 0 fixture(s)

Synced:
 > django.contrib.auth
 > django.contrib.contenttypes
 > django.contrib.sessions
 > django.contrib.sites
 > django.contrib.messages
 > django.contrib.staticfiles
 > south

Migrated:
 - recipes

The output from the syncdb command states that all apps specified in INSTALLED_APPS, except for your recipes, has been set up using the normal syncdb, and that your recipes app has been set up using a migration. Good.

Set up admin interface

Now we will utilize the built-in Django Admin. In urls.py in the project folder, uncomment the lines regarding admin:

from django.conf.urls import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
   # Examples:
   # url(r'^$', 'recipes_project.views.home', name='home'),
   url(r'^recipes/', include('recipes.urls')),

   # Uncomment the admin/doc line below to enable admin documentation:
   # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

   # Uncomment the next line to enable the admin:
   url(r'^admin/', include(admin.site.urls)),
)

We have already set up an url pattern to forward everything starting with recipes/ to the python module recipes.urls, and now everything starting with “admin” will redirect to the admin interface we will soon take a closer look at.

If you refresh your browser at this time, you will get an error about your site being improperly configured.

_images/improperly_configured.png

The error message suggests that you should put django.contrib.admin in the INSTALLED_APPS section of settings.py. It is already there, you just need to uncomment it.

After uncommenting the admin app, have a look in your browser. No matter what address you go to, the server will not find it, and suggests you should try localhost:8000/admin/. Go there and have a look.

_images/admin_login.png

You should now be able to log in and have a look around. You should see some predefined classes from Django like User and Group, but Admin can also take care of your Food model. To get that to work, you need to create a file in the recipes folder called “admin.py”. The file should contain:

from django.contrib import admin
from recipes.models import Food

admin.site.register(Food)

On browser refresh, nothing changes. When adding new models to admin, you need to restart the server. Just stop it (ctrl-c) and restart the runserver command.

You should now be able to see your Food model in the list. Click on it and try to add some food objects, like “Banana” or “Apple”.

_images/no_admin_tables.png

You will now get an error complaining about missing tables. This is because you added the admin inteface after the last run of “syncdb”, so the tables admin needs are not created. Just run the same syncdb command again:

$ python manage.py syncdb --migrate

This time, the output also lists “django.contrib.admin” as a synced app.

Adding a method to your model

When you have successfully created Apple and Banana, you will see that you have two lines of Food object.

_images/food_objects.png

This is not very useful, as it is not possible to distinguish between the lines in the list. In your models.py add a function named __unicode__ inside your Food class (at the same indentation level as the “name”). Make it return self.name, like this:

def __unicode__(self):
    return self.name

When refreshing the list, your table should look more user friendly. The __unicode__ is utilized by Django to write a human readable version of the object. A side effect is that you now have defined a default representation of the object, so you do not need to add .name everywhere.

_images/user_friendly_food_objects.png

Your first view: Food list

Admin does everything nice and tidy, but you don’t want to expose the admin inteface to your users. We have to create a simpler representation to show to our users.

Open up recipes/views.py and paste in this code:

from django.shortcuts import render_to_response
from django.template import RequestContext
from recipes.models import Food

def food_list(request):
    food = Food.objects.all()
    return render_to_response('recipes/food_list.html', {'object_list': food}, context_instance=RequestContext(request))

The food_list method will fetch all Food objects from the database. Hold them in a variable named food, and send this variable to a template named food_list.html, but as a variable named object_list exposed to the template.

Go to your app’s urls.py and add an import statement to the top:

from recipes.views import food_list

And a line to the pattern list to get all food:

url(r'^food/$', food_list, name='food-list'),

Now /recipes/food/ should trigger the newly created food_list function. Go to this address and see what you get.

_images/food_template_does_not_exist.png

You got an error message. It tells you to make a template named “recipes/food_list.html”.

Bootstrapping a template

We will make this template in a folder named “templates/recipes” inside the app folder:

$ mkdir -p templates/recipes

And create a file in the newly created folder called food_list.html containing (copied from http://twitter.github.com/bootstrap/getting-started.html and changed to serve static media from Django’s locations):

<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 101 Template</title>
<!-- Bootstrap -->
<link href="{{ STATIC_URL }}css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<h1>Hello, world!</h1>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="{{ STATIC_URL }}js/bootstrap.min.js"></script>
</body>
</html>

This template needs some files from the Twitter Bootstrap project, so in your app folder, download twitter bootstrap static files, unzip and rename the directory to static:

$ wget http://twitter.github.com/bootstrap/assets/bootstrap.zip
$ unzip bootstrap.zip
$ rm bootstrap.zip
$ mv bootstrap static

Have a look at the file structure there and compare to the explanations at http://twitter.github.com/bootstrap/getting-started.html. It should be alright.

You need to stop and start the server again, as the new templates folder is only picked up at server restart.

Now, refresh the web browser and see the page saying “Hello, world!”.

The real coding begins

We will now change the template. You can compare your work to the full example further down the page.

Add a div tag with class container around the h1 and see how the page changes. Change the template by changing the h1 tag and the title, and after the h1 (but inside the div), display the contents of the object_list template variable we created above, like this:

<ul>
{% for object in object_list %}
<li>{{ object }}</li>
{% endfor %}
</ul>

Refresh your browser and see. We want to see some details about the food we have created, but we do not know the addresses to these pages yet, so we will insert empty links (a href=””) around the {{ object }}. Insert this instead of {{ object }}, (inside the li-tag):

<a href="">{{ object }}</a>

Also add an empty link at the bottom of the page that will later be used for adding more food to our list.

<a href="">Add food</a>
_images/food_list.png

The template should now look similar to this:

<!DOCTYPE html>
<html>
<head>
<title>Food</title>
<!-- Bootstrap -->
<link href="{{ STATIC_URL }}css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <h1>Food</h1>

    <ul>
    {% for object in object_list %}
    <li><a href="">{{ object }}</a></li>
    {% endfor %}
    </ul>

    <a href="">Add food</a>
</div>

<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="{{ STATIC_URL }}js/bootstrap.min.js"></script>
</body>
</html>

A simpler view

The view function we made earlier gives us full control over what happens. But it is long, and making a few of these requires a lot of typing. To make sure you don’t repeat yourself too much, you can use the newer “Class based generic view”s instead.

In views.py, remove the file contents and insert this instead:

from recipes.models import Food
from django.views.generic import ListView

class FoodListView(ListView):
    model = Food

And the urls.py should import the new FoodListView instead of food_list, like this:

from recipes.views import FoodListView

And the pattern should be changed to this:

url(r'^food/$', FoodListView.as_view(), name='food-list'),

Here, instead of calling the view function directly, we are now calling the as_view function on the FoodListView class we just created. Our FoodListView does not define this as_view() function, but inherits it from the ListView model class of the django.views.generic module.

Have a look in the browser. The functionality is the same, the code a bit shorter. You may need the old syntax from time to time, so you should know that an old and more flexible syntax exists.

Your second view: Food details

We want to get up a page for each food object, that may in the future list more details. In the views.py, append DetailView (comma separated) to the django.views import statement at the top, and add another class at the bottom of the file:

class FoodDetailView(DetailView):
    model = Food

Add another pattern to the urls.py of the app, and remember to import FoodDetailView at the top (the same way as you imported DetailView above):

url(r'^food/(?P<pk>\d+)$', FoodDetailView.as_view(), name='food-detail'),

The <pk> part of the pattern says that you want to match the primary key field of the object. The primary key field is for all common cases the hidden auto-incremented numerical id. When this url pattern is matched to an incoming url, we have a match if the incoming url starts with the recipes/ of the app, defined in the project’s urls.py, then the next part has to be food/ as defined here, and it has to end with a number. If we have a match, Django will run the inherited as_view function of the FoodDetailsView model, fetching the database record matching the incoming integer to the Food table’s primary key.

You see that the last parameter is “name”. This is a short name to use when refering to links that may be long and have a lot of parameters. Another good thing about this is that we can change the urls without updating all the places where they are used. This concept is called named urls in Django, and this way, the url patterns are used both for url pattern matching and link url generation.

In the template we created some moments ago, insert the name of the url you need into the address field, so that the line becomes:

<li><a href="{% url food-detail object.id %}">{{ object }}</a></li>

Here we send in object.id (the hidden primary key of the object), so that when it is clicked, it will get used the other way, as described above.

When you have a look at the web browser now, you see by hovering the mouse over the links that they point somewhere. By clicking one of them, you will see we need to make another template. templates/food_detail.html is missing.

_images/food_detail_template_missing.png

In the innermost template folder, copy the template you already have to food_detail.html in the same folder. Change the new template to add a new title, h1 and the contents itself. In the title and h1, you can use {{ object }}. This will make use of the already utilized __unicode__-function.

The contents is not too much fun as we do only have one field in the Food model, and the normally hidden id. Add a few <p>-tags with the object id and name, and a link back to the list, using the named url, like this:

<p><a href="{% url food-list %}">Back to food list</a></p>

<p>{{ object.id }}</p>
<p>{{ object.name }}</p>

You can be happy if the detailed page looks something like this when you refresh the browser:

_images/food_detail.png

Don’t repeat yourself: Use a common base

When you look at the two templates, you see that there is a lot of common code in them. Adding even more templates, the contents will become unorganized and at last not look consistent. Not good. Create a new template one folder level up, relative to the two you have, called “base.html” with the common code, like this:

<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Generic title{% endblock %}</title>
<!-- Bootstrap -->
<link href="{{ STATIC_URL }}css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
{% block content %}
<h1>Generic title</h1>

Nothing interesting yet

{% endblock %}
</div>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="{{ STATIC_URL }}js/bootstrap.min.js"></script>
</body>
</html>

You see some placeholder text in there, inside some blocks {% block content %}. Blocks are made to be overridden in the templates extending them, so we will only see this placeholder text if we forget to override the blocks in the other templates.

Now remove the common code from the other two templates and add a line at the top to tell them to extend the new base template. Then override the two blocks, title and content in both templates. The list template now looks like this:

{% extends "base.html" %}

{% block title %}Food list{% endblock %}

{% block content %}
<h1>Food list</h1>
<ul>
{% for object in object_list %}
<li><a href="{% url food-detail object.id %}">{{ object }}</a></li>
{% endfor %}
</ul>

<a href="">Add food</a>
{% endblock %}

Now, the browser should look exactly the same for the two views. If you see the generic text of the base, then you do not override the blocks using the same names.

Create more objects

Append CreateView to the django.views import at the top of views.py, and create a new view like:

class FoodCreateView(CreateView):
    model = Food

In the urls.py, add the new FoodCreateView to the import at the top, and add a new url pattern:

url(r'^food/create$', FoodCreateView.as_view(), name='food-create'),

It is good practice to have the more specific patterns above the patterns that tries to match url input. If the food details url was set up to take a word instead of a number as a parameter, it would also match “create” and would be trying to fetch a food object named “create”.

Now you can update the create link in the list template to use the new and named food-create, like this:

<a href="{% url food-create %}">Add food</a>

Clicking the new link will also give an error about a missing template.

_images/food_create_view_missing.png

The error message tells us that “recipes/food_form.html” is missing. Create it and make it look similar to the other two templates, but with a form added to it:

{% extends "base.html" %}

{% block title %}Add food{% endblock %}

{% block content %}
<h1>Add food</h1>

<form>
    {{ form }}
    <button type="submit">Save</button>
</form>
{% endblock %}

We haven’t added any action or method parameters to the form at this time. The {{ form }} tag will let Django show the fields that represent the models. And we also have a standard submit button. Have a look at the form in the browser.

_images/food_create_boring.png

OK, but we can do better…

Primary action button

To make it slightly nicer, add a class="btn btn-primary" to the submit button. Looks better? This is because of the styling we get from Twitter Bootstrap.

_images/food_create_blue_button.png

A more crispy form

We will also make the form layout a bit nicer with the third party Crispy Forms module. This will help us by adding useful CSS classes that will be styled by the bootstrap css rules. To INSTALLED_APPS in setup.py, add crispy_forms and install django-crispy-forms with pip:

pip install django-crispy-forms

Below the extends line in the food_form template, add:

{% load crispy_forms_tags %}

And add the crispy filter to the form variable, like this:

<form>
    {{ form|crispy }}
    <button class="btn btn-primary" type="submit">Save</button>
</form>
_images/food_create_crispy.png

It now looks good, but it won’t work.

Making the form post

Now, add a fruit name and click “Save”. The url changes, but you are still on the same page. Our Django view will answer differently on GET and POST requests, but we did not tell the form to use the http POST method. Change the form definition to use the POST method:

<form method="POST">

If we try again, we will see another error, complaining about “Forbidden: CSRF verification failed. Request aborted.”. Cross site request forgery is a well established mechanism to used to decide that a request originates from the same site. This is done by using the randomly generated SECRET variable in settings.py to generate a combination of characters that will be attached as hidden fields to all forms, and then be validated on the servers when the form is posted. All you have to do is to add a {% csrf_token %} to your form. Add this e.g. at the same line as the form definition tag, like this:

<form method="POST">{% csrf_token %}

Now, try to save again. Another error! So much errors, so much to learn!

_images/no_url_to_redirect_to.png

This time Django complains about not knowing where to send you after the form has been parsed and your object saved. You would need to define either a success_url in the view, to tell it where to go, or you can let Django go back to the detailed view for the object. This is the default behaviour, as long as you have a get_absolute_url method defined in your model. Head over to models.py and add a method at the bottom of your Food class (on the same indentation level as __unicode__):

@models.permalink
def get_absolute_url(self):
    return ('food-detail', [self.id])

The @models.permalink gives a short and easier way to write a url than when calling reverse yourself.

Now, go back and add a fruit and click save. Nice? If you now have two fruits with the same name, that is because your fruit got added even though your success link were missing.

_images/two_of_the_same.png

To be sure you will never register the same fruit twice, you can add unique=True within the definition of name in your model class, so the changed definition get something like:

name = models.CharField(max_length=20, unique=True)

If you now try to add a food object that has the same name as one previously created, you should see an error message like this:

_images/food_with_this_name_already_exists.png

Now you know how to add a model and some views to list, see details or add new objects.

More models

To be able to create recipes, we need at least two more models. A recipe model is obvious, where we can add ingredients and a description of how to use the ingredients. But how do we connect the recipes to the food objects?

There is a way to connect two models together in a very generic way. This is called a “many to many relation”. In this case that would be too simple, as we need to add properties to the intermediate table. With ManyToMany we would be able to say what ingredients we need, but now how much of what ingredient we need in the recipe.

We need to say what Food object we will use, how much of it, and to what ingredient we want it added. When saying how much, we need to know the measurement, as “1 salt” is not very useful.

We will first define the Recipe model. It will have a title, a description of unknown length, and a __unicode__ method as we have already seen. But wouldn’t it be nice to have a nice looking url? From the news paper agencies (where Django was first created), we have gotten slugs: readable parts of a url that will be used to identify an object. We will add a slug field that will hold a nice slugified version of the object’s title:

class Recipe(models.Model):
    title = models.CharField(max_length=80)
    slug = models.SlugField(max_length=80)
    description = models.TextField()

    def __unicode__(self):
        return self.title

To connect the Recipe to the Food, we create a table to hold the references as well as the measurement fields:

class Ingredient(models.Model):
    recipe = models.ForeignKey(Recipe)
    food = models.ForeignKey(Food)
    amount = models.DecimalField(decimal_places=2, max_digits=4)
    measurement = models.SmallIntegerField(choices=MEASUREMENT_CHOICES)

We have ForeignKey fields that connects the Ingredient to a Food object and a Recipe object. The amount is defined as a DecimalField and the measurement as a SmallIntegerField. We could have created a table for all the different measurements available, but we want to see how to make predefined choices. The measurements will be saved as a number, but should be treated as a choice of strings all the way through the application. In the above model definition, we refer to MEASUREMENT_CHOICES which are not defined. Define some choices above the Ingredient model definition where it will be referred to:

MEASUREMENT_CHOICES = (
    (1, "piece"),
    (2, "liter"),
    (3, "cup"),
    (4, "tablespoon"),
    (5, "teaspoon"),
)

Migrations, simple

Now that we have defined new models, we should create and run a new migration as well. To create a new migration, go up to the project level directory where manage.py is and run:

python manage.py schemamigration --auto recipes

And run it with:

python manage.py syncdb --migrate

You may get into trouble here if you still have food objects with the same name. Head over to http://localhost:8000/admin/recipes/food/ and delete the duplicates. Then, try to run the syncdb command above again. If you now get an error about a table recipes_recipe already existing, you may need to run an SQL command manually to fix it, as stated by the top of the error message:

DROP TABLE "recipes_recipe"; DROP TABLE "recipes_ingredient";

This is because sqlite3 is not a very good database backend, but as it is easy to develop with, and you do not have very important data in our development database, it is really no problem. Where to run that SQL command:

python manage.py dbshell

Should give you access to the configured database shell. Exit by pressing ctrl-d. NOTE that you should only do that if you got this error.

Extending the admin inteface

Register the two new models with the admin interface, in recipes/admin.py. (Do not forget to update the import statement):

admin.site.register(Recipe)
admin.site.register(Ingredient)

In the admin interface (at /admin), try to add a new recipe, e.g. Pancakes. Insert “My Pancakes” as the title and “my-pancakes” as the slug. Try to save without filling in the “description” field. Click Save. Form validations will not let you save this without filling in a description. Or telling the model that an empty description is OK, by adding blank=True to the description field, like:

description = models.TextField(blank=True)

That worked. Before adding ingredient objects, go back and add some more food objects, like “egg”, “milk”, “salt” and “wheat flour”.

And then, add a new ingredient object. Choose “My Pancakes”, “Milk”, “0.5” and “liter” and save.

We get redirected back to the Ingredient list, and see that we need to add a __unicode__ method to the ingredient class. Python has several ways to format a string to look nice(REF). The first attempt is to add the method like this:

def __unicode__(self):
    return "%f %s %s (%s)" % (self.amount, self.measurement, self.food, self.recipe)

Here, we output a number which may contain decimals for the amount, a string for the measurement and a string in parentheses for the recipe it belongs to.

When refreshing the ingredient list page, you see that the %f gives a lot of unneeded decimals. Change this to %.2g to allow at most two decimals.

You also see that the line does not print out the measurement, only the numerical id. So change the self.measurement to self.get_measurement_display() to use a method that is dynamically available to fields with choices. (In the documentation this is called get_FIELD_display()).

Now we have been sort of exploiting the string representation of the object to make it look nice in the admin interface. But instead of using the object’s string representation in a single cell in the table, you can define how to represent the object in the admin interface. Replace the Ingredient line in admin.py with this:

class IngredientAdmin(admin.ModelAdmin):
    list_display = ('food', 'amount', 'measurement', 'recipe')

admin.site.register(Ingredient, IngredientAdmin)

Here, you also see that the measurement is listed using multiple columns.

New views

Now, everything looks nice in the admin interface, but we still do not want to expose it to the users. We need to get similar functionality in our own views.

We want to list all recipes, so you should add a RecipeListView and a RecipeDetailView to views.py. Import the Recipe model in the first line, and add two new views:

class RecipeListView(ListView):
    model = Recipe

class RecipeDetailView(DetailView):
    model = Recipe

Create two new url patterns like this to the urls.py, and remember to do the correct import at the top:

url(r'^$', RecipeListView.as_view(), name='recipe-list'),
url(r'^(?P<slug>[-\w]+)$', RecipeDetailView.as_view(), name='recipe-detail'),

The first will match the address “/recipes/”. The second will match “/recipes/” plus “a string containing numbers, letters, hyphens and underscores”. This is used to match the slug field we described earlier. The P<slug> actually saves the value to a parameter named “slug”, which is treated almost like an id internally by Django.

Now copy the template food_list.html to recipe_list.html in the same folder, and modify the new recipe list to be useful to list recipes. Also get the list to link to the recipe-detail url that you just created:

{% url recipe-detail object.slug %}

While you are at it, copy food_detail.html to recipe_detail.html and modify that as well. The contents could be something like:

<h1>{{ object.title }}</h1>

<p><a href="{% url recipe-list %}">Back to recipe list</a></p>

<h2>Ingredients</h2>
<ul>
{% for ingredient in object.ingredient_set.all %}
<li>{{ ingredient}}</li>
{% endfor %}
</ul>

<h2>Description</h2>
<p>{{ object.description }}</p>

Here you see how we can list out the ingredients of the recipe.

You should now be able to navigate between the list and the detailed recipe(s). In the recipe_detail.html you just created, change the last line to add |default:"No description" to print out a default value when the description has not been added. In case you wonder, this is how it should look:

<p>{{ object.description|default:"No description" }}</p>

Add recipes

Now, we will add a new view by doing it the other way around. Add or update the linke at the bottom of the recipe_list.html. Like this:

<a href="{% url recipe-create %}" class="btn btn-primary">Add new</a>

Here, we point to a url pattern called recipe-create, and if you try to view the recipe list now, you will get an error message telling you this, you are using a link that is not defined.

_images/no-reverse-match-recipe-create.png

So head over to urls.py and add recipe-create before the recipe-detail url (if you put it after, the recipe-detail will be reached first, and you will try to fetch a recipe called “create”):

url(r'^new/$', RecipeCreateView.as_view(), name='recipe-create'),

If you try to view the recipe-list in the browser now, you will see an error message telling you that RecipeCreateView is not defined.

_images/recipe-create-view-not-defined.png

Add the missing import line, try again, and you will get an error message telling you that it cannot find RecipeCreteView in views.py.

_images/cannot-import-name-recipecreateview.png

So, go ahead and create that simple function:

class RecipeCreateView(CreateView):
    model = Recipe

Try it in your browser. Yes, we now have a nice button.

_images/recipe-list.png

If you try clicking that new button, you will once again see the error about a missing template. Even if this is a new template, the contents should look very familiar. You can copy food_form.html to recipe_form.html and do just a few modifications to get what you want:

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Add recipe{% endblock %}

{% block content %}

<h1>Add recipe</h1>

<form method="post">
    {% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary">Save</button>
</form>

{% endblock %}

Now, you should see something useful in your browser. Try to create a simple recipe, were you do not use too much time, as I now warn you that this will end in an error. Yes, once again, Django complains about a missing success_url - it does not know where to send us after the object is created.

_images/improperly_configured_recipes_create.png

You have already done this. Create a method in the Recipe model named get_aboslute_url that will return the recipe-detail url:

@models.permalink
def get_absolute_url(self):
    return ('recipe-detail', [self.slug])

You see how we use include the slug when creating this url, as we need that to access the human readable url.

Try to add another recipe, and see that everything is now working.

Editing an object

The way to edit an object is not too different from creating a new object. It is in fact so similar that Django by default reuses the same template. As we will see, one of the differences is how we need to identify the object we are going to edit.

To the recipe-detail template, add a link to a still undefined url recipe-update:

<p><a href="{% url recipe-update object.slug %}">Edit description</a></p>

The url will contain the slug, like the create view:

url(r'^(?P<slug>[-\w]+)/update$', RecipeUpdateView.as_view(), name='recipe-updat e'),

The view will not be very different from before, but you need to remember to import UpdateView and code the view itself:

class RecipeUpdateView(UpdateView):
    model = Recipe

Now this should work without adding another template, as the recipe_form.html will be used by both the create view and the update view. When you try to edit the description, you will see that the template still says “Add recipe”.

Using a non-default template

You could have chosen other texts that would have fit both a create form and an update form, but we want to show how to use a non-default template. Copy the recipe_form.html to recipe_update_form.html, change its contents so it says “Change recipe” and set the template_name variable of the view to point to the new template instead of silently pointing to the default:

class RecipeUpdateView(UpdateView):
    model = Recipe
    template_name = "recipes/recipe_update_form.html"

Adding ingredients

The last thing to do is to combine all of this to add, show and delete ingredients. Start by adding a link to the recipe-list template where your users can click to add ingredients:

<p><a href="{% url ingredient-create object.slug %}">Add ingredient</a></p>

You see that we need to send in the slug of the object so that we do not need our users to choose this from a menu later. This slug is of course also part of the needed url pattern:

url(r'^(?P<slug>[-\w]+)/add_ingredient/$', IngredientCreateView.as_view(), name='ingredient-create'),

We first define the view as simple as possible:

class IngredientCreateView(CreateView):
    model = Ingredient

This will now work, except for the missing template, ingredient_form.html:

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Add ingredient{% endblock %}

{% block content %}

<h1>Add ingredient</h1>

<form method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button type="submit" class="btn btn-primary">Save</button>
</form>

{% endblock %}

When you look at the form in your browser, you see that you can make it a little bit simpler to use by taking away the “Recipe” form field. First, add a method to the IngredientCreateView that will select initial values in our form:

def get_initial(self, *args, **kwargs):
    recipe = Recipe.objects.get(slug=self.kwargs['slug'])
    return {'recipe': recipe}

This will use the slug to fetch the corresponding Recipe object, and use that to fill in the initial value of the recipe form field. Try it out and see that it works.

The next step is to hide the field from the user, as they should no longer need to do anything to it. To hide the field, you need to define your own form. We do this by creating a new file in the same folder as views.py called forms.py. In this file, we define a new ModelForm (REF), a form that will be based on the Ingredient model, and we override the form widget used to show the recipe field:

from django.forms import ModelForm, HiddenInput
from recipes.models import Ingredient

class IngredientForm(ModelForm):

    class Meta:
        model = Ingredient
        widgets = {'recipe': HiddenInput()}

Now, have a look. Isn’t it easier? Try to add some ingredients. Oh noes! Another error! This time, we will actually define a success url, as we do not want to show any details about “1 tablespoon of salt”. We want to redirect back to the recipe details instead. To the same view, add a method called get_success_url that contains:

def get_success_url(self):
    return reverse('recipe-detail', args=[self.kwargs['slug']])

Deleting objects

You have probably done your fair share of testing now, and have accumulated a large amount of testdata. Some ingredients have been created that does not belong to some recipes, so we need to delete them.

First, add a link to each ingredient row in the recipe detail template. It could say “delete” or be a little “x”, but it should point to the url you name “ingredient-delete”, and it should take in the object’s slug and the ingredient’s id:

<li>{{ ingredient }} <a href="{% url ingredient-delete object.slug ingredient.id %}">x</a></li>

Now, create the url pattern this points to:

url(r'^(?P<slug>[-\w]+)/remove_ingredient/(?P<pk>\d+)/$', IngredientDeleteView.as_view(), name='ingredient-delete'),

This is probably the longest of them all as we use both the slug and the ingredient’s id field. You maybe wonder if we really need to pick up the slug again, since the ingredient’s id should be unique alone, but it is a nice looking url, and it will save us from some work later.

So, happily knowing what is going on, you bring up your browser and try to delete one of the silly test ingredients, but what? An error? Missing an ingredient_confirm_delete.html was maybe a bit unexpected.

Delete confirmation

The default delete view is doing the same thing as the create and update views by showing a form on a GET request and processing the form on the form on a POST request.

There are several ways to circumvent the confirm_MODEL_delete.html templates, by using a button in a small form, using javascript to send a POST request instead of a get on the link clicking, redirecting from the GET to the POST… but I think a delete confirmation page is a good habit, especially when listing out related objects that would also be deleted. The ingredient_confirm_delete could look something like:

{% extends "base.html" %}

{% block title %}Delete ingredient{% endblock %}

{% block content %}
<h1>Delete ingredient</h1>

<h2>Really delete {{ object }} from {{ object.recipe }}?</h2>

<p>It will be permanently lost</p>

<form method="post">{% csrf_token %}
    <button type="submit">Delete</button>
</form>

{% endblock %}

# FIXME: se om det er noe vits med form-output

The important thing is the delete button. Skipping the csrf_token will give back the error about cross site scripting attacks again.

You should really add a cancel button to the form as well to help the users, bringing them back to the detail page without changing anything:

<a href="{% url recipe-detail object.recipe.slug %}">Cancel</a>

Now this is now a small form with a button and a small link. If you add some css classes defined in the Twitter Bootstrap css, it can be a lot nicer. Add class="btn" to the cancel link to style it like a button, and class="btn btn-primary" to the delete button to make it look like a default action button.

Yes, this is nice an shiny, but the form is still not working. If you try it, you’ll see that we are missing a success-url. This time, we will just copy the get_success_url we made in IngredientCreateView to IngredientDeleteView to get the same redirect back to the recipe-detail:

def get_success_url(self):
    return reverse('recipe-detail', args=[self.kwargs['slug']])

Now, this looks better, and redirects us to the recipe we deleted the ingredient from. Just to show off, we could replace the delete link on the recipe detail view with an icon from Twitter Bootstrap, by adding an <i>-tag with a class representing the icon we want to use (“icon-remove”) from http://twitter.github.com/bootstrap/base-css.html#icons:

<li>{{ ingredient}} <a href="{% url ingredient-delete object.slug ingredient.id %}"><i class="icon-remove"></i></a></li>

Easier editing with Markdown

Try to edit the description of a recipe and save it. The description of a recipe will probably consist of several steps on a way to the finished meal, and you would probably want to put these steps in several paragraphs or a list. As you probably guess, you would need to type html to get this nice looking.

There is a filter called “markdown” filter that will take a more simpler made text and convert it to html for you (REF). To the description field in the recipe-detail template, add |markdown between description and |default, like this:

<p>{{ object.description|markdown|default:"No description" }}</p>

You shouldn’t be surprised that this will not work. The error message should tell you that Django does not understand “markdown”. You need to load a module where “markdown” is defined. On line two of the file, load the markup filters:

{% load markup %}

This still does not work, because you also need to have a markdown library installed which this filter will contact to parse the text. Head over to a terminal where your virtualenv is activated, and install markdown using Python package installer, Pip:

pip install markdown

You will also need to tell Django to actually load this file in settings.py. In the INSTALLED_APPS section, add:

'django.contrib.markup',

You do not have an easy way to go between the recipe section and the food section of your website. What about using a fancy top menu from Twitter Bootstrap http://twitter.github.com/bootstrap/components.html#navbar? In “base.html” template (one level up from the other templates), add a this inside the “container” div, before the “content” block:

<div class="navbar">
    <div class="navbar-inner">
        <a class="brand" href="{% url recipe-list %}">Djecipes</a>
        <ul class="nav">
            <li><a href="{% url recipe-list %}">Recipes</a></li>
            <li><a href="{% url food-list %}">Food</a></li>
        </ul>
    </div>
</div>

Future sections?

  • debugging with ipython, pdb, web error
  • unit testing
  • authentication

Indices and tables