Intro to Django — Part 4

Django 1.3 edition

Steven F. Lott

slott56@gmail.com

http://slott-softwarearchitect.blogspot.com/

Topics

Code Kata

Review: Model, View

Presentation

Dynamic Content and Templates

Generic List/Detail Views

Code Kata

Simple Inventory of ingredients, recipes and locations.

What?

Use Case

Model

model.png

Model Code

http://docs.djangoproject.com/en/1.3/ref/models/fields/

from django.db import models

class Ingredient( models.Model ):
    name = models.CharField( max_length=64 )
    safety = models.CharField( max_length=64 )

UNITS = ( ("ea.","Each"), ("lb.","Pounds"), ("qt.","Quarts") )

class OnHand( models.Model ):
    ingredient = models.ForeignKey( Ingredient )
    count = models.FloatField( )
    units = models.CharField( max_length=64, choices=UNITS )

View

Several standard pieces to a handling a request.

Common Aspects:

All those interlocking pieces and parts...

Presentation

Three parts:

Template File

Best to use a "base" template for the overall page structure.

{% extends "base.html" %}

Often a good idea to keep the "base" outside any specific application.

Example base.html

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head><title>{{title}}</title></head>
<body>
<div id="body">
{%block body%}
    <p>Inventory<p>
{%endblock%}
</div>
</body>
</html>

Other Stuff

Your .CSS, .JS and .PNG's are just static files.

They're referenced in your HTML.

They reside on your Apache server.

They're downloaded by Apache outside Django.

The STATIC_URL setting must match your Apache configuration.

The {% Tags

Django template tags.

http://docs.djangoproject.com/en/1.3/ref/templates/builtins/

Lots and lots of features for decisions, iteration, and some reformatting.

This is not the wild-west (i.e. JSP or PHP)

This is only presentation.

The {%block%} Tag

A "place-holder" block of HTML.

A template that {%extends%} this may replace the block.

If a block isn't replaced, the default value will be rendered.

Keep it Simple.

"Real" processing happened in the view function.

Example home.html

{% extends "base.html"%}
{% block body %}
<p>Inventory<p>
<p><a href="{%url tutorial.inventory.views.ingredient%}">Ingredients</a></p>
{% endblock %}

All global layout changes in base.html

Any local changes in home.html

The {%url%} Tag

{%url tutorial.inventory.views.ingredient%}

Seriously cool.

This maps the view function name (ingredient) backwards through urls.py to create the URL path.

Creates the correct path to invoke the given view function.

Never "hard-code" a URL path.

Don't Repeat Yourself.

Rendering A Template

def home( request ):
    return render( request, "home.html" )

Note

Demonstrate

Run the developer server.

./manage.py runserver

Runs a Django service on http://127.0.0.1:8000/

HTTP Request → url pattern match → view function → HTTP Response.

Branding

You'll want to change the title and the branding.

These are template blocks that you can override.

The words Site administration is hard-wired into admin.sites module AdminSites.index().

Dynamic Content and Templates

Parse the URL path and capture data values.

/inventory/ingredient/ingredient/

Three steps:

  1. Parsing Rules in urls.py
  2. View Function Parameters in views.py
  3. More Data in Templates

Parsing Rules

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

urlpatterns = patterns('tutorial.inventory.views',
    url(r'^$', 'home' ),
    url(r'^ingredient/(?P<name>.*?)/$', 'ingredient' ),
    url(r'^ingredient/$', 'ingredient' ),
    #url(r'^location/$', 'location' ),
    #url(r'^onhand/$', 'on_hand' ),
)

/(?P<name>.*?)/ part of the path captured as name; a named argument to the view function.

View Function Parameters

def ingredient( request, name=None ):
    if name:
        data = Ingredient.objects.filter(
            name__icontains=name )
        return render( request, "ingredient_detail.html",
            {'objects':data, 'name':name}, )
    else:
        data = Ingredient.objects.all()
        return render( request, "ingredient_list.html",
            {'objects':data, 'name':'All'}, )

More Data in Templates

{% extends "base.html"%}
{% block body %}
<p>Ingredients {{name}}<p>
<table>
    <tr><th>Name</th><th>Kind</th></tr>
    {%for ing in objects%}
    <tr><td>{{ing.name}}</td>
      <td>{%if ing.perishable %}Perishable{%endif%}</td>
    </tr>
    {%endfor%}
</table>
{% endblock %}

The {{variable}} Notation

Looks up the variable in the page context.

There's a obj.attr notation to get attributes of an object.

The ORM manages attribute references as SQL SELECT columns.

Also, you can use Python @property decorator to evaluate simple (no argument) methods of a model class.

ingredient_detail.html Template

{%for ing in objects%}
<h2>{{ing.name}}</h2>
{%if ing.perishable %}<p>Perishable</p>{%endif%}
<table>
    <tr><th>Location</th><th>Qty</th><th>Unit</th></tr>
    {%for oh in ing.onhand_set.all%}
    <tr><td>{{oh.location}}</td>
      <td>{{oh.count}}</td>
      <td>{{oh.units}}</td></tr>
    {%endfor%}
</table>
{%endfor%}

Object References and Navigation

objects is an iterator over Ingredient instances.

ing is an Ingredient instance.

for oh in ing.onhand_set.all is an iterator over all OnHand instances.

oh is an an OnHand instance

oh.location is a Location instance

Lazy fetches from the database. Works with Paginator.

Role-Specific Navigation

Steps:

  1. Define a Profile to carry additional properties and attributes of each User.
  2. Update Settings to use the Profile.
  3. Customize Links based on the Profile.

Define A Profile

inventory/models.py:

from django.contrib.auth.models import User
class Profile( models.Model ):
    user= models.OneToOneField( User )
    @property
    def is_galley( self ):
        return self.ingroup( 'galley' )
    def ingroup( self, name ):
        return name in (
            g.name.lower()
                for g in self.user.groups.all())

Update Settings

Must tweak settings to use our profile

settings.py:

AUTH_PROFILE_MODULE = 'inventory.Profile'

Profile Objects for Users

>>> from django.contrib.auth.models import User
>>> User.objects.all()
[<User: slott>, <User: cookie>]
>>> slott, cookie = _
>>> slott.get_profile()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/Django-1.3-py2.6.egg/django/contrib/auth/models.py", line 383, in get_profile
    self._profile_cache = model._default_manager.using(self._state.db).get(user__id__exact=self.id)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/Django-1.3-py2.6.egg/django/db/models/query.py", line 349, in get
    % self.model._meta.object_name)
DoesNotExist: Profile matching query does not exist.

Create Profile Objects

>>> from inventory.models import Profile
>>> Profile.objects.create( user=slott )
<Profile: >
>>> _.save()
>>> Profile.objects.create( user=cookie )
<Profile: galley>
>>> _.save()

Generic List/Detail Views

I'm already tired of repeating myself.

Many view functions are very simple-minded.

Why repeat this boilerplate?

Generic Views.

Simple Generic View urls.py

from tutorial.inventory.models import Location
from django.views.generic import ListView

url(r'^location/$',
    ListView.as_view(
        model=Location,
        template_name='location_list.html')
    ),

No views.py code at all. Just a template.

Typical Generic View urls.py

from tutorial.inventory.models import Location
from tutorial.inventory.views import Location_Filter
from django.views.generic import ListView

url(r'^location/(?P<name>.*?)/$',
    Location_Filter.as_view(
        template_name='location_detail.html'),
    name='location' ),
url(r'^location/$',
    ListView.as_view(
        model=Location,
        template_name='location_list.html')
    ),

Typical Generic Views views.py

Subclass the built-in ListView to provide a get_queryset method.

from django.views.generic import ListView

class Location_Filter( ListView ):
    model= Location
    def get_queryset( self, **args ):
        return self.model.objects.filter(
            name__icontains=self.kwargs['name'] ).all()

Now that we have a named location...

{%url location name=oh.location.name%}

Tidy.

We provide a URL name for a generic view.

With a tiny bit of planning (and a naming convention) we can assure that our data is throughly linked.

Without hard-coding a single URL.

Conclusion

We're not even close to done with the Django feature set.