Python Web frameworks, Develop for the Web with Django and Python



The Django project is a custom-built framework that originated with an online newspaper Website and was released as open source in July 2005. The core components of the Django framework are:
  • Object-relational mapping for creating models
  • Polished administrator interface designed for end users
  • Elegant URL design
  • Designer-friendly template language
  • Caching system
This is the first article in a two-part series on Python Web frameworks. The secord article will introduce you to the TurboGears framework.
To use and understand the code in this article, you need to have Python installed and know how to use it at a beginner level. Check to see if you have Python, and what version, by typing python -V. Django requires, at a minimum, version 2.3.5, which is available at the Python Web site (see the Resources section later in this article for a link). You should also be at least passingly familiar with the MVC architecture.
This article uses the development version of Django, to take advantage of the recent improvements to the Django framework. I recommend that you use this version until the 0.95 release. Check the Django Web site for the latest release (again, seeResources for a link).
Download and install Django as follows:

Listing 1. Downloading and installing Django
~/downloads# svn co http://code.djangoproject.com/svn/django/trunk/ django_src
~/downloads# cd django_src
~/downloads# python setup.py install
      

After installing Django, you should have the admin tool, django-admin.py, available on your path. Listing 2 shows some of the commands available to the admin tool:

Listing 2. Using the Django administration tool
~/dev$ django-admin.py
usage: django-admin.py action [options]
actions:
  adminindex [modelmodule ...]
    Prints the admin-index template snippet for the given model
    module name(s).

  ... snip ...

  startapp [appname]
    Creates a Django app directory structure for the given app name
    in the current directory.

  startproject [projectname]
    Creates a Django project directory structure for the given
    project name in the current directory.

  validate
    Validates all installed models.

options:
  -h, --help  show this help message and exit
  --settings=SETTINGS Python path to settings module, e.g.
   "myproject.settings.main". If this isn't
   provided, the DJANGO_SETTINGS_MODULE
   environment variable will be used.
  --pythonpath=PYTHONPATH
   Lets you manually add a directory the Python
   path, e.g. "/home/djangoprojects/myproject".
      

To begin a Django project, use the django-admin startproject command, like so:

Listing 3. Starting a project
~/dev$ django-admin.py startproject djproject
      

The above command creates a directory called djproject that contains the basic configuration files needed to run a Django project:

Listing 4. Contents of the djproject directory
__init__.py
manage.py
settings.py
urls.py
      

For this project, you will build a job-board application called "jobs." To create an application, use the manage.py script, which is a project-specific django-admin.py script where the settings.py file is automatically supplied:

Listing 5. Using manage.py startapp
~/dev$ cd djproject
~/dev/djproject$ python manage.py startapp jobs
      

This creates a barebone application with one Python module for your models and another for your views. The jobs directory will contain the following files:

Listing 6. Contents of the jobs application directory
__init__.py
models.py
views.py
      

The location of the application inside the project is purely a convention created for new Django developers, not a requirement. Once you start mixing and matching applications across several projects, you can put applications in their own module namespace and tie them together using settings and master URL files. For now, follow the steps as shown.
To make Django aware of a new application, you'll need to add an entry to the INSTALLED_APPS field in the settings.py file. For this job board application, the string djproject.jobs must be added:

Listing 7. Adding an entry to settings.py
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'djproject.jobs',
)
      

Django comes with its own object-relational mapper (ORM) library that supports dynamic database access through a Python object interface. The Python interface is very usable and powerful, but you are also free to drop down and use SQL directly, if needed.
The ORM currently provides support for PostgreSQL, MySQL, SQLite, and Microsoft® SQL databases.
This example uses SQLite as the database backend. SQLite is a lightweight database that requires no configuration and resides on disk as a simple file. To use SQLite, simply install the pysqlite library using setuptools (see Resources for more information on setuptools and the easy_install tool in particular, which you need to install separately):
easy_install pysqlite
Before working on the model, configure the database in the settings file. SQLite requires only that the database engine and name be specified.

Listing 8. Configuring the database in settings.py
DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = '/path/to/dev/djproject/database.db'
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''
      

This job board application will have two types of objects, Locations and Jobs. A Location contains city, state (optional), and country fields. A Job has a location, title, description, and publish date.

Listing 9. The jobs/models.py module
from django.db import models

class Location(models.Model):
    city = models.CharField(maxlength=50)
    state = models.CharField(maxlength=50, null=True, blank=True)
    country = models.CharField(maxlength=50)

    def __str__(self):
        if self.state:
            return "%s, %s, %s" % (self.city, self.state, self.country)
        else:
            return "%s, %s" % (self.city, self.country)

class Job(models.Model):
    pub_date = models.DateField()
    job_title = models.CharField(maxlength=50)
    job_description = models.TextField()
    location = models.ForeignKey(Location)

    def __str__(self):
        return "%s (%s)" % (self.job_title, self.location)
      

The __str__ method is a special class method in Python that returns the string representation of an object. Django uses this method extensively when displaying objects in the Admin tool.
To see the database schema for the model, run manage.py's sql command. The schema won't be enacted yet.

Listing 10. Viewing the database schema using the manage.py sql command
~/dev/djproject$ python manage.py sql jobs

BEGIN;
CREATE TABLE "jobs_job" (
    "id" integer NOT NULL PRIMARY KEY,
    "pub_date" date NOT NULL,
    "job_title" varchar(50) NOT NULL,
    "job_description" text NOT NULL,
    "location_id" integer NOT NULL
);
CREATE TABLE "jobs_location" (
    "id" integer NOT NULL PRIMARY KEY,
    "city" varchar(50) NOT NULL,
    "state" varchar(50) NULL,
    "country" varchar(50) NOT NULL
);
COMMIT;
      

To initialize and install the model, run the synchronize database command, syncdb:
~/dev/djproject$ python manage.py syncdb
Note that the syncdb command asks you to create a superuser account. This is because the django.contrib.auth application, which provides basic user authentication functionality, is supplied by default in your INSTALLED_APPS settings. The superuser name and password will be used for logging into the admin tool described in the next section. Remember that this is the Django superuser, not your system's.
Django models access the database through the default Manager class called objects. For example, to print a list of all Jobs, you would use the all method of the objects manager:

Listing 11. Printing all jobs
>>> from jobs.models import Job
>>> for job in Job.objects.all():
...     print job
      

The Manager class also has filtering methods called filter and exclude. Filtering gets all the objects that meet a condition, while excluding gives all the objects that do not. The queries below should give the same results ("gte" means "greater than or equal," and "lt" means "less than").

Listing 12. Excluding and filtering jobs
>>> from jobs.models import Job
>>> from datetime import datetime
>>> q1 = Job.objects.filter(pub_date__gte=datetime(2006, 1, 1))
>>> q2 = Job.objects.exclude(pub_date__lt=datetime(2006, 1, 1))
      

The filter and exclude methods return QuerySet objects that can be chained together and can even perform joins. The q4query below will find jobs posted since January 1st, 2006, in Cleveland, Ohio:

Listing 13. More excluding and filtering jobs
>>> from jobs.models import Job
>>> from datetime import datetime
>>> q3 = Job.objects.filter(pub_date__gte=datetime(2006, 1, 1))
>>> q4 = q3.filter(location__city__exact="Cleveland",
...                location__state__exact="Ohio")
      

It's very nice that QuerySets are lazy. This means that they do not execute against the database until they are evaluated, and thus run much faster than immediate queries.
This laziness is handy with Python's slicing functionality. Rather than request all the records and then slice the records needed, the code below uses an OFFSET of 5 and a LIMIT of 10 in the actual SQL query, greatly improving performance.

Listing 14. Python slice
>>> from jobs.models import Job
>>> for job in Job.objects.all()[5:15]
...     print job
      

Note: Use the count method to find out how many records are in a QuerySet. The Python len method does a full evaluation and then counts the rows returned as records, while the count method does an actual SQL COUNT, which is much faster. Your database administrator will thank you.

Listing 15. Counting records
>>> from jobs.models import Job
>>> print "Count = ", Job.objects.count()       # GOOD!
>>> print "Count = ", len(Job.objects.all())    # BAD!
      

For more information, see the Resources section for a link to the Django "Database API reference."
One of the biggest selling points of Django is its well-polished admin interface. This tool was created with end users in mind. It gives your projects a great data entry tool.
The admin tool is an application that comes with Django. It must be installed, like the jobs application, before you can use it. The first step is to add the application's module (django.contrib.admin) to the INSTALLED_APPS setting:

Listing 16. Modifying settings.py
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'djproject.jobs',
    'django.contrib.admin',
)
      

To make the admin tool available from the /admin URL, simply uncomment the line provided in your project's urls.py file. The next section goes into URL configuration in greater detail.

Listing 17. Making the admin tool available via urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^admin/', include('django.contrib.admin.urls.admin')),
)
      

The admin application has its own database model and needs to be installed. Use the syncdb command again to accomplish this:
python manage.py syncdb
To view the admin tool, you can use the test server that comes with Django.

Listing 18. Using the test server to view the admin tool
~/dev/djproject$ python manage.py runserver
Validating models...
0 errors found.

Django version 0.95 (post-magic-removal), using settings 'djproject.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C (Unix) or CTRL-BREAK (Windows).
      

You can now navigate to the admin tool at http://localhost:8000/admin and log in using the superuser account you created before. You will notice that none of your models are available for use.
To make a class accessible through the admin tool, create an Admin subclass to it. You can then customize how each class can be administered by adding class attributes to this subclass. Listing 19 shows how to add the Location class to the admin tool.

Listing 19. Adding the Location class using the admin tool
class Location(meta.Model):
    ...
    class Admin:
        list_display = ("city", "state", "country")
      

You can now create, update, and delete the Location records through the admin interface.

Figure 1. Editing locations with the admin tool
Editing locations with   the Admin tool 
You can list and sort Records by city, state, and country as specified by the list_display class attribute.

Figure 2. Listing locations with the admin tool
Listing locations with   the Admin tool 
The admin tool has numerous options for managing each type of model class. Listing 20 shows several examples applied to the Job class:

Listing 20. Options for managing model classes
class Job(meta.Model):
    ...
    class Admin:
 list_display = ("job_title", "location", "pub_date")
 ordering = ["-pub_date"]
 search_fields = ("job_title", "job_description")
 list_filter = ("location",)
      

According to the above settings, a job's title, location, and published data will be used when listing job records. The jobs will be ordered by when they were published, starting with the most recent (a minus sign indicates descending order). Users can find jobs by title and description, and administrators can filter records based on location.

Figure 3. Listing jobs with the admin tool
Listing jobs with   the admin tool 
The Django URL dispatch system uses regular-expression configuration modules that map URL string patterns to Python methods called views. This system allows URLs to be completely decoupled from the underlying code, allowing for maximum control and flexibility.
A urls.py module is created and defined as the default starting point for URL configuration (via the ROOT_URLCONF value in the settings.py module). The only requirement for a URL configuration file is that it must contain an object that defines the patterns called urlpatterns.
The job board application will start with an index and detail view that are accessed through these URL mappings:
  • /jobs index view: Displays the latest 10 jobs
  • /jobs/1 detail view: Displays jobs with an ID of 1
Both views (index and detail) will be implemented in a module called views.py in the jobs application. Implementing this configuration in the project's urls.py file would look like this:

Listing 21. Implementing the configuration of views in djproject/urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^admin/', include('django.contrib.admin.urls.admin')),

    (r'^jobs/$', 'djproject.jobs.views.index'),
    (r'^jobs/(?P<job_id>\d+)/$', 'djproject.jobs.views.detail'),
)
      

Note the <job_id> piece. It's important later.
Best practice is to pull out application-specific URL patterns and place them in the application itself. This decouples the application from the project and allows for greater reuse. An application-level URL config file for jobs would look like this:

Listing 22. Application-level URL configuration file, urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^$', 'djproject.jobs.views.index'),
    (r'^(?P<job_id>\d+)/$', 'djproject.jobs.views.detail'),
)
      

Since the view methods now all come from the same module, the first argument can be used to specify djproject.jobs.views as the module's root name, and Django will use it to look for the methods index and detail:

Listing 23. jobs/urls.py: Looking for index and detail
from django.conf.urls.defaults import *

urlpatterns = patterns('djproject.jobs.views',
    (r'^$', 'index'),
    (r'^(?P<object_id>\d+)/$', 'detail'),
)
      

Tying the above jobs URLs back into the project as a whole is done using the include function. The application level URLs are tied back below the /jobs section:

Listing 24. djproject/urls.py: Tying URLs back into the project
from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^admin/', include('django.contrib.admin.urls.admin')),
    (r'^jobs/', include('djproject.jobs.urls')),
)
      

If you try to access the index page (http://localhost:8000/jobs) at this point using your test server, you will get an error, since the view being called (djproject.jobs.views.index) does not exist yet.
A view is a simple Python method that accepts a request object and is responsible for:
  • Any business logic (directly or indirectly)
  • A context dictionary with data for the template
  • Rendering the template with a context
  • The response object that passes the rendered results back to the framework
In Django, the Python method called when a URL is requested is called a view, and the page loaded and rendered by the view is called a template. Because of this, the Django team refers to Django as an MVT (model-view-template) framework. TurboGears, on the other hand, calls its methods controllers and their rendered templates views so that they can fit squarely into the MVC acronym. The difference is largely semantic, as they accomplish the same things.
The simplest possible view returns an HttpResponse object initialized with a string. Create the following method and make a/jobs HTTP request to ensure your urls.py and views.py files are set up correctly.

Listing 25. jobs/views.py (v1)
from django.http import HttpResponse

def index(request):
    return HttpResponse("Job Index View")
      

The following code gets the latest 10 jobs, renders them through a template, and returns a response. It will not work without the template file from the next section.

Listing 26. jobs/views.py (v2)
from django.template import Context, loader
from django.http import HttpResponse
from jobs.models import Job

def index(request):
    object_list = Job.objects.order_by('-pub_date')[:10]
    t = loader.get_template('jobs/job_list.html')
    c = Context({
        'object_list': object_list,
    })
    return HttpResponse(t.render(c))
      

In the above code, the template is named by the jobs/job_list.html string. The template is rendered with a context of the job list named object_list. The rendered template string is then passed into an HTTPResponse constructor, which is sent back to the request client via the framework.
The steps of loading a template, creating a context, and returning a new response object are replaced below with the convenience method named render_to_response. Also new is the detail view method that uses a convenience method calledget_object_or_404 to retrieve a Job object using the arguments supplied. If the object is not found, a 404 exception is thrown. These two methods remove a lot of boilerplate code in most Web applications.

Listing 27. jobs/views.py (v3)
from django.shortcuts import get_object_or_404, render_to_response
from jobs.models import Job

def index(request):
    object_list = Job.objects.order_by('-pub_date')[:10]
    return render_to_response('jobs/job_list.html',
                              {'object_list': object_list})

def detail(request, object_id):
    job = get_object_or_404(Job, pk=object_id)
    return render_to_response('jobs/job_detail.html',
                              {'object': job})
      

Note that detail takes object_id as an argument. This is the number mentioned earlier after the /jobs/ URL path in the jobs urls.py file. It is passed further to the get_object_or_404 method as the primary key (pk).
The above views will still fail because the templates that they load and render (jobs/job_list.html and jobs/job_detail.html) do not exist yet.
Django provides a simple templating language designed for fast rendering and ease of use. Django templates are created with plain text embedded with {{ variables }} and {% tags %}. Variables are evaluated and replaced with the value they represent. Tags are used for basic control logic. Templates can be used to generate any text-based format including HTML, XML, CSV, and plain text.
The first step is to define where the templates are located. For simplicity's sake, create a templates directory under djproject and add its path to the TEMPLATE_DIRS settings.py entry:

Listing 28. Creating a templates directory in settings.py
TEMPLATE_DIRS = (
    '/path/to/devdir/djproject/templates/',
)
      

Django templates support a concept called template inheritance, which allows site designers to create a uniform look and feel without repeating content in every template. You can use inheritance by defining a skeleton, or base, document with block tags. These block tags are filled by page templates with content. This example shows an HTML skeleton with blocks called title,extrahead, and content:

Listing 29. Skeleton document, templates/base.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Company Site: {% block title %}Page{% endblock %}</title>
    {% block extrahead %}{% endblock %}
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>
      

To keep the application decoupled from the project, use an intermediary base file as the base for all the Job application page files. For this example, put the application CSS in the base file for simplicity. In a real application, with a properly configured Web server, extract this CSS and put it in a static file served by the Web server.

Listing 30. Intermediary base file, templates/jobs/base.html
{% extends "base.html" %}

{% block extrahead %}
    <style>
        body {
            font-style: arial;
        }
        h1 {
            text-align: center;
        }
        .job .title {
            font-size: 120%;
            font-weight: bold;
        }
        .job .posted {
            font-style: italic;
        }
    </style>
{% endblock %}
      

By default, the Django test server does not serve static files because that is the Web server's job. If, during development, you would like Django to serve images, style sheets, etc., then see the link in Resources on how to turn that feature on.
Now, create two page templates to be loaded and rendered by the views. The jobs/job_list.html template simply iterates through the object_list it gets through the context by the index view, and displays a link to each record's detail page.

Listing 31. The templates/jobs/job_list.html template
{% extends "jobs/base.html" %}

{% block title %}Job List{% endblock %}

{% block content %}
    <h1>Job List</h1>
    <ul>
    {% for job in object_list %}
        <li><a href="{{ job.id }}">{{ job.job_title }}</a></li>
    {% endfor %}
    </ul>
{% endblock %}
      

The jobs/job_detail.html page shows one record, called job:

Listing 32. The templates/jobs/job_detail.html page
{% extends "jobs/base.html" %}

{% block title %}Job Detail{% endblock %}

{% block content %}
    <h1>Job Detail</h1>

    <div class="job">
        <div class="title">
            {{ job.job_title }}
            -
            {{ job.location }}
        </div>
        <div class="posted">
            Posted: {{ job.pub_date|date:"d-M-Y" }}
        </div>
        <div class="description">
            {{ job.job_description }}
        </div>
    </div>
{% endblock %}
      

The Django template language has been designed with limited functional capabilities. This limitation keeps templates simple for non-programmers and keeps programmers from putting business logic where it doesn't belong, the presentation layer. See the link to the template language documentation in Resources.
Django comes with four sets of generic views that let developers create applications that follow typical patterns:
  • List/detail pages (like the above example)
  • Date-based breakdown of records (useful for news or blog sites)
  • Creation, update, and deletion (CRUD) of objects
  • Simple direct template rendering or simple HTTP redirect
Instead of creating boilerplate view methods, all of the business logic is in the urls.py file and is handled by the generic view methods supplied by Django.

Listing 33. Generic views in jobs/urls.py
from django.conf.urls.defaults import *
from jobs.models import Job

info_dict = {
    'queryset': Job.objects.all(),
}

urlpatterns = patterns('django.views.generic.list_detail',
    (r'^$', 'object_list', info_dict),
    (r'^(?P<object_id>\d+)/$', 'object_detail', info_dict),
)
      

Three major changes to this urls.py file are:
  • An info_dict map object passes along a query set for the Jobs to be accessed.
  • It uses django.views.generic.list_detail instead of djproject.jobs.views.
  • The actual views called are object_list and object_detail.
This project follows some requirements to make the transition to generic views work automatically:
  • The generic detail view expects an argument named object_id.
  • The templates follow the naming pattern: app_label/model_name_list.html (jobs/job_list.html)app_label/model_name_detail.html (jobs/job_detail.html)
  • The list template handles a list named object_list.
  • The detail template handles an object named object.
More options can be passed through the info_dict, including a paginate_by value that specifies the number of objects per page.

Article written by, Ian Maurer (ian@itmaurer.com), Senior Consultant, Brulant, Inc.

Comments

Popular Posts