C H A P T E R   9

image

Form Processing in the Code-Sharing Application

All of your Django applications so far—with the exception of the comments system for the weblog—have been focused exclusively on systems in which trusted members of a site's staff enter content through Django's administrative interface, rather than on interactive features that let ordinary users submit content to be displayed. For this new application, though, you're going to need a way to allow users to submit their snippets of code. You'll also want to make sure that their submissions are in a format that works with the data models you've set up.

Fortunately, Django is going to make this fairly easy through the use of a simple but powerful system for displaying and processing web-based forms. In this chapter, you'll get a thorough look at Django's form-handling system and use it to build the forms that people will use to submit and edit their code samples.

A Brief Tour of Django's Form System

Django's form-handling code, which lives in the module django.forms, provides three key components that, taken together, cover every aspect of constructing, displaying, and processing a form:

  • A set of field classes, similar to the types of fields available for Django data models, which represent a particular type of data and know how to validate that data
  • A set of widget classes, which know how to render various types of HTML form controls (text inputs, check boxes, and so on) and read out the corresponding data from an HTTP form submission
  • A Form class that ties these concepts together and provides a unified interface for defining the data to be collected and high-level rules for validating it

A Simple Example

To get a feel for how this works, let's take a look at a simple but common requirement: user signups.

Basic signups will require a registration form that collects three pieces of data:

  • A username
  • An e-mail address to associate with the new account
  • A password the user will use to log in

Additionally, you'll want to do a little bit of custom validation work:

  • You'll want to make sure that the username isn't already in use because you can't have two users with the same username.
  • It's always a good idea to show two password fields and have the user type the same password twice. This will catch typos and provide a little extra safety to make sure new users get the password they're expecting.

Logically, this works out to an HTML <form> element with four fields: one each for the username and e-mail address, and two to handle the repeated password. Here's how you might start building the form:

from django import forms


class SignupForm(forms.Form):
    username = forms.CharField(max_length=30)
    email = forms.EmailField()
    password1 = forms.CharField(max_length=30)
    password2 = forms.CharFIeld(max_length=30)

Aside from the use of classes from django.forms instead of django.db.models, this starts out looking similar to the way you define model classes in Django: simply subclass the appropriate base class and add the appropriate fields.

But the code is not quite perfect. HTML provides a special form input type for handling passwords—<input type="password">—which would be a more appropriate way to render the password fields. You can implement this input type by changing those two fields slightly:

password1 = forms.CharField(max_length=30,
                            widget=forms.PasswordInput())
password2 = forms.CharField(max_length=30,
                            widget=forms.PasswordInput())

The PasswordInput widget will render itself as an <input type="password">, which is exactly what you want. This also shows off one major strength of the way Django's form system separates the validation of data, which is handled by the field, from the presentation of the form, which is handled by the widgets. It's fairly common to run into situations where you have a single underlying validation rule that needs to work with multiple fields that all become different types of HTML inputs. This separation makes it easy: you can reuse a single field type and just change the widget.

While you're at it, let's make one more change:

password1 = forms.CharField(max_length=30,
                            widget=forms.PasswordInput(render_value=False))
password2 = forms.CharField(max_length=30,
                            widget=forms.PasswordInput(render_value=False))

The render_value argument to the PasswordInput tells it that even if it has some data, it shouldn't show it. An error a user makes while entering the password should completely clear the field to make sure the user types it in correctly the next time.

Validating the Username

The fields you've specified so far all have some implicit validation rules associated with them. The username field and the two password fields both have maximum lengths specified, and the EmailField will confirm that its input looks like an e-mail address (by applying a regular expression). But you also need to make sure that the username isn't already in use, so you'll need to define some custom validation for the username field.

You can do this by defining a method on the form called clean_username(). During the validation process, Django's form system automatically looks for any method whose name starts with clean_ and ends in the name of a form on the field, then calls it after the field's built-in validation rules have been applied.

Here's what the clean_username() method looks like (assuming that the Django user model has already been imported using from django.contrib.auth.models import User):

def clean_username(self):
    try:
        User.objects.get(username=self.cleaned_data['username'])
    except User.DoesNotExist:
        return self.cleaned_data['username']
    raise forms.ValidationError("This username is already in use.image
    Please choose another.")

This code packs a lot into a few lines. First of all, this method is called only if the username field has already met its built-in requirement of containing fewer than 30 characters of text. In that case, the value submitted for the username field is in self.cleaned_data['username']. The attribute cleaned_data is a dictionary of any submitted data that's made it through validation so far.

You query for a user whose username exactly matches the value submitted to the username field. If there is no such user, Django will raise the exception User.DoesNotExist. This exception tells you that the username isn't in use, so you know the value for the username field is valid. In this case, you simply return that value.

If there is a user with the submitted username, you raise the exception ValidationError. Django's form-handling code will catch this exception and turn it into an error message that you can display. (You'll see how to do this in a moment, when you look at the template that shows this form.)

Validating the Password

Validating the password is a bit trickier because it involves looking at two fields at once and making sure they match. You could do this by defining a method for one of the fields and having it look at the other:

def clean_password2(self):
    if self.cleaned_data['password1'] != self.cleaned_data['password2']:
        raise forms.ValidationError("You must type the same password each time")
    return self.cleaned_data['password2']

But there's a better way to do this. Django lets you define a validation method—simply called clean()—which applies to the form as a whole. Here's how you could write it:

def clean(self):
    if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
        if self.cleaned_data['password1'] != self.cleaned_data['password2']:
            raise forms.ValidationError("You must type the same password each time")
    return self.cleaned_data

Note that in this case, you manually check whether there are values in cleaned_data for the two password fields. If there were any errors raised during individual field validation, cleaned_data will be empty. So you need to check this before referring to anything you expect to find in it.

Creating the New User

At this point, you could stop writing form code and move on to a view that processes the form. You could write the view so that it creates and saves the new User object. But if you ever needed to reuse this form in other views, you'd have to write out that code again and again. So it's better to write a method on the form itself that knows what to do with the valid data. Because the method is saving a new User object to the database, let's call it save().

In the save() method, you need to create a User object from the username, e-mail, and password submitted to your form. Assuming you've already imported the User model, you can do it like this:

def save(self):
    new_user = User.objects.create_user(username=self.cleaned_data['username'],
                                        email=self.cleaned_data['email'],
                                        password=self.cleaned_data['password1'])
    return new_user

And here's the finished form:

from django.contrib.auth.models import User
from django import forms


class SignupForm(forms.Form):
    username = forms.CharField(max_length=30)
    email = forms.EmailField()
    password1 = forms.CharField(max_length=30,
    widget=forms.PasswordInput(render_value=False))
    password2 = forms.CharField(max_length=30,
    widget=forms.PasswordInput(render_value=False))

    def clean_username(self):
        try:
            User.objects.get(username=self.cleaned_data['username'])
        except User.DoesNotExist:
            return self.cleaned_data['username']
        raise forms.ValidationError("This username is already in use.image
        Please choose another.")

    def clean(self):
        if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
            if self.cleaned_data['password1'] != self.cleaned_data['password2']:
                raise forms.ValidationError("You must type the sameimage
 password each time")
       return self.cleaned_data

   def save(self):
       new_user = User.objects.create_user(username=self.cleaned_data['username'],
                                           email=self.cleaned_data['email'],
                                           password=self.cleaned_data['password1'])
       return new_user

How Form Validation Works

The method you'll use in views to determine whether or not submitted data is valid is called is_valid(), and it's defined on the base Form class that all Django forms derive from. Inside the Form class, is_valid() touches off the form's validation routines, in a specific order, by calling full_clean() (another method defined in the base Form class in django.forms; see Figure 9-1).

image

Figure 9-1. The order in which validation methods are applied to a Django form

The order of validation goes like this:

  1. First, full_clean() loops through the fields on the form. Each field class has a method named clean(), which implements that field's built-in validation rules, and each of these methods will either raise a ValidationError or return a value. If a ValidationError is raised, no further validation is done for that field (because the data is already known to be invalid). If a value is returned, it goes into the form's cleaned_data dictionary.
  2. If a field's built-in clean() method didn't raise a ValidationError, then any available custom validation method—a method whose name starts with clean_ and ends with the name of the field—is called. Again, these methods can either raise a ValidationError or return a value; if they return a value, it goes into cleaned_data.
  3. Finally, the form's clean() method is called. It can also raise a ValidationError, albeit one that's not associated with any specific field. If clean() finds no new errors, it should return a complete dictionary of data for the form, usually by doing returnself.cleaned_data.
  4. If no validation errors were raised, the form's cleaned_data dictionary will be fully populated with the valid data. If there were validation errors, however, cleaned_data will not exist, and a dictionary of errors (self.errors) will be filled with validation errors. Each field knows how to retrieve its own errors from this dictionary, which is why you can do things like {{ form.username.errors }} in a template.
  5. Finally, is_valid() returns either False if there were validation errors or True if there weren't.

Understanding this process is key to getting the most out of Django's form-handling system. It might seem a bit complex at first, but the ability to attach validation rules to a form in multiple places results in a huge amount of flexibility and makes it easier to write reusable code. For example, if you find yourself needing to use a particular type of validation over and over again, you'll notice that writing a custom method on each form gets tedious. You'll probably be better off writing your own field class, defining a custom clean() method on it, and then reusing that field.

Similarly, distinguishing field-specific methods from the “form-level” clean() method opens up a lot of useful tricks for validating multiple fields together. You wouldn't necessarily need these tricks when working with a single field only.

Processing the Form

Now, let's take a look at a view you might use to display and process this form:

from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response

def signup(request):
    if request.method == 'POST':
        form = SignupForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            return HttpResponseRedirect("/accounts/login/")
    else:
        form = SignupForm()
    return render_to_response('signup.html',
                              { 'form': form })

Let's break this down step by step:

  1. First you check the method of the incoming HTTP request. Usually, this will be GET or POST. (There are other HTTP methods, but they're not as commonly used, and web browsers typically support only GET and POST for form submissions.)
  2. If, and only if, the request method is POST, you instantiate a SignupForm and pass it request.POST as its data. Back in Chapter 3, when you wrote a simple search function, you saw that request.GET is a dictionary of data sent with a GET request; similarly, request.POST is the dictionary of data (in this case, the form submission) sent along with a POST request.
  3. You check whether the submitted data is valid by calling the form's is_valid() method. Under the hood, this matches up the submitted data with the fields on the form and checks against each field's validation rules. If the data passes validation, is_valid() will return True, and the form's cleaned_data dictionary will be populated with the correct values. Otherwise, is_valid() will return False, and the cleaned_data dictionary will not exist.
  4. If the data was valid, you call the form's save() method, which you previously defined. Then you return an HTTP redirect—using django.http.HttpResponseRedirect—to a new page, which, presumably, would be wired up to a view to let the new user log in. Whenever you accept data from an HTTP POST, you should always redirect after successful processing. By taking the user to a new page, you avoid a common pitfall where refreshing or clicking the Back button in a web browser accidentally resubmits a form.
  5. If the request method was anything other than POST, you instantiate a SignupForm without any data. Technically speaking, this is called an unbound form (one that has no data to work with), as opposed to a bound form, which does have some data to validate.
  6. You render a template, passing the form as a variable into it, and return a response. Note that because of the way this view is written, you'll never get to this step if the user submitted valid data. In that case, the if statements farther up would already have ensured that a redirect was returned. Also, note that this step is the same regardless of whether there was invalid data or no data at all—the SignupForm object doesn't have to be treated specially according to the different cases.

Finally, let's take a look at how you might display this form in the signup.html template used by this view:

<html>
  <head>
    <title>Sign up for an account</title>
  </head>
  <body>
    <h1>Sign up for an account</h1>
    <p>Use the form below to register for your new account; all
       fields are required.</p>
    <form method="post" action="">
      {% if form.non_field_errors %}
      <p><span class="error">
      {{ form.non_field_errors|join:", " }}
      </span></p>
      {% endif %}
      <p>{% if form.username.errors %}
      <span class="error">{{ form.username.errors|join:", " }}</span>
      {% endif %}</p>
      <p><label for="id_username">Username:</label>
         {{ form.username }}</p>
      <p>{% if form.email.errors %}
      <span class="error">
      {{ form.email.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_name">Your e-mail address:</label>
         {{ form.email }}</p>
<p>{% if form.password1.errors %}
      <span class="error">
      {{ form.passsword1.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_password1">Password:</label>
         {{ form.password1 }}</p>
      <p>{% if form.password2.errors %}
      <span class="error">
      {{ form.passsword2.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_password2">Password (again, to catch
          typos): </label>
         {{ form.password2 }}</p>
      <p><input type="submit" value="Submit"></p>
    </form>
  </body>
</html>

Most of the HTML here is pretty simple: a standard <form> tag with <label> tags for each field and a button to submit. But notice how you actually show the fields. Each one is accessed as an attribute of the {{ form }} variable. You can check each one to see if it had any errors and display the error messages (which will be in a list, even if there's only one message—hence you use the join template filter, which can join a list of items using a specified string as a separator).

Note, though, that at the top of the form you use {{ form.non_field_errors }}. This is because the error raised from the clean() method doesn't “belong” to any one field (because it comes from comparing two fields to each other). Whenever you have a potential validation error from the clean() method, you'll need to check for non_field_errors and display it if present.

Writing a Form for Adding Code Snippets

Now that the user-signup example has given you a pretty good idea of how to write a form to accept submitted data, you can write one for adding instances of your Snippet model. You'll simply set up fields for the information you want users to fill in, and then give it a save() method, which creates and saves the new snippet.

But there's one new thing you have to handle here. The author field on your Snippet model has to be filled in, and it has to be filled in correctly, but you don't want to show it to your users and let them choose a value. If you did that, any user could effectively pretend to be any other by filling in someone else's name on a snippet. So you need some way to fill in that field without making it a public part of the form.

Luckily, this is easy to do: a form is just a Python class. So you can add your own custom __init__() method to it and trust that the view function that processes the form will pass in the identity of the correct, authenticated user, which you can store and refer back to when it's time to save the snippet. So let's get started writing AddSnippetForm.

Go into the cab directory and create a file called forms.py. In it you can start writing your form as follows:

from django import forms
from cab.models import Snippet


class AddSnippetForm(forms.Form):
    def __init__(self, author, *args, **kwargs):
        super(AddSnippetForm, self).__init__(*args, **kwargs):
        self.author = author

Aside from accepting an extra argument—author, which you store for later use—you're doing two important things here:

  • In addition to the author argument, you specify that the __init__() method accepts *args and **kwargs. This is a Python shorthand for specifying that it will accept any combination of positional and keyword arguments.
  • You use super() to call the parent class's __init__() method, passing the other arguments that your custom __init__() accepted. This ensures that the __init__() from the base Form class gets called and sets up everything else on your form properly.

Using this technique—accepting *args and **kwargs and passing them on to the parent method—is a useful shorthand when the method you're overriding accepts a lot of arguments, especially if a lot of them are optional. The __init__() method of the base Form class actually accepts up to seven arguments, all of them optional, so this is a handy trick.

Now you can add the fields you care about:

title = forms.CharField(max_length=255)
description = forms.CharField(widget=forms.Textarea())
code = forms.CharField(widget=forms.Textarea())
tags = forms.CharField(max_length=255)

Note that once again you're relying on the fact that you can change the widget used by a field to alter its presentation. Where Django's model system uses two different fields—CharField and TextField—to represent different sizes of text-based fields (and has to, because they work out to different data types in the underlying database columns), the form system only has a CharField. To turn it into a <textarea> in the eventual HTML, you simply change its widget to a Textarea, in much the same way that you used the PasswordInput widget in the example user-signup form.

And that takes care of everything except the language, which is suddenly looking a little bit tricky. What you'd like to do is show a drop-down list (an HTML <select> element) of the available languages and validate that the user picked one of them. But none of the field types you've seen so far can handle that, so you'll need to turn to something new.

One way you could handle this is with a field type called ChoiceField. It takes a list of choices (in the same format as a model field that accepts choices—you've seen that already in the status field on the weblog's Entry model, for example) and ensures that the submitted value is one of them. But setting that up properly so that the form queries for the set of languages each time it's used (in case an administrator has added new languages to the system) would require some more hacking in the __init__() method. And representing a model relationship like this is an awfully common situation, so you'd expect Django to provide an easy way to handle this.

As it turns out, Django does provide an easy solution: a special field type called ModelChoiceField. Where a normal ChoiceField would simply take a list of choices, a ModelChoiceField takes a Django QuerySet and dynamically generates its choices from the result of the query (executed freshly each time). To use it, you'll need to change the model import at the top of the file to also bring in the Language model:

from cab.models import Snippet, Language

And then you can simply write:

language = forms.ModelChoiceField(queryset=Language.objects.all())

For this form, you don't need any special validation beyond what the fields themselves give you, so you can just write the save() method and be done:

def save(self):
    snippet = Snippet(title=self.cleaned_data['title'],
                      description=self.cleaned_data['description'],
                      code=self.cleaned_data['code'],
                      tags=self.cleaned_data['tags'],
                                  author=self.author,
                      language=self.cleaned_data['language'])
    snippet.save()
    return snippet

Because creating an object and saving it all in one step is a common pattern in Django, you can actually shorten that a bit. The default manager class Django provides will include a method called create(), which creates, saves, and returns a new object. Using that, your save() method is a couple lines shorter:

def save(self):
    return Snippet.objects.create(title=self.cleaned_data['title'],
                                  description=self.cleaned_data['description'],
                                  code=self.cleaned_data['code'],
                                  tags=self.cleaned_data['tags'],
                                                  author=self.author,
                                  language=self.cleaned_data['language'])

And now your AddSnippetForm is complete:

from django import forms
from cab.models import Snippet, Language


class AddSnippetForm(forms.Form):
    def __init__(self, author, *args, **kwargs):
        super(AddSnippetForm, self).__init__(*args, **kwargs):
        self.author = author
title = forms.CharField(max_length=255)
    description = forms.CharField(widget=forms.Textarea())
    code = forms.CharField(widget=forms.Textarea())
    tags = forms.CharField(max_length=255)
    language = forms.ModelChoiceField(queryset=Language.objects.all())

    def save(self):
        return Snippet.objects.create(title=self.cleaned_data['title'],
                                      description=self.cleaned_data['description'],
                                      code=self.cleaned_data['code'],
                                      tags=self.cleaned_data['tags'],
                                                       author=self.author,
                                      language=self.cleaned_data['language'])

Writing a View to Process the Form

Now you can write a short view called add_snippet to handle submissions. In the cab/views directory, create a file called snippets.py, and in it place the following code:

from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from cab.forms import AddSnippetForm

def add_snippet(request):
    if request.method == 'POST':
        form = AddSnippetForm(author=request.user, data=request.POST)
        if form.is_valid():
            new_snippet = form.save()
            return HttpResponseRedirect(new_snippet.get_absolute_url())
    else:
        form = AddSnippetForm(author=request.user)
    return render_to_response('cab/add_snippet.html',
                              { 'form': form })

This code will instantiate the form, validate the data, save the new Snippet, and return a redirect to the detail view of that snippet. (Again, always redirect after a successful POST.)

At first this looks great, but there's a problem lurking here. You're referring to request.user, which will be the currently logged-in user (Django automatically sets this up when the authentication system has been properly activated). But what happens if the person filling out this form isn't logged in?

The answer is that your data won't really be valid. When the current user isn't logged in, request.user is a “dummy” object representing an anonymous user, and it can't be used as the value of a snippet's author field. So what you need is some way to ensure that only logged-in users can fill out this form.

Fortunately, Django provides an easy way to handle this, via a decorator in the authentication system called login_required. You can simply import it and apply it to your view function, and anyone who's not logged in will be redirected to a login page:

from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from cab.forms import AddSnippetForm

def add_snippet(request):
    if request.method == 'POST':
        form = AddSnippetForm(author=request.user, data=request.POST)
        if form.is_valid():
            new_snippet = form.save()
            return HttpResponseRedirect(new_snippet.get_absolute_url())
    else:
        form = AddSnippetForm(author=request.user)
    return render_to_response('cab/add_snippet.html',
                              { 'form': form })
add_snippet = login_required(add_snippet)

Writing the Template to Handle the add_snippet View

From here you could write the cab/add_snippet.html template like this:

<html>
  <head>
    <title>Add a snippet</title>
  </head>
  <body>
    <h1>Add a snippet</h1>
    <p>Use the form below to submit your snippet; all fields are
       required.</p>
    <form method="post" action="">
      <p>{% if form.title.errors %}
      <span class="error">
      {{ form.title.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_title">Title:</label>
      {{ form.title }}</p>
      <p>{% if form.language.errors %}
      <span class="error">
      {{ form.language.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_languages">Language:</label>
      {{ form.language }}</p>
      <p>{% if form.description.errors %}
      <span class="error">
      {{ form.description.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_description">Description:</label></p>
      <p>{{ form.description }}</p>
      <p>{% if form.code.errors %}
      <span class="error">
      {{ form.code.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_code">Code:</label></p>
      <p>{{ form.code }}</p>
      <p>{% if form.tags.errors %}
      <span class="error">
      {{ form.tags.errors|join:", " }}
      </span>
      {% endif %}</p>
      <p><label for="id_tags">Tags:</label>
     {{ form.tags }}</p>
      <p><input type="submit" value="Submit"></p>
    </form>
  </body>
</html>

Automatically Generating the Form from a Model Definition

Although Django's form system lets you be pretty concise about writing and using this form, you still haven't arrived at an ideal solution. Setting up a form for adding or editing instances of a model is a pretty common thing, and it would be awfully annoying to keep writing these sorts of boilerplate forms over and over (especially when you've already specified most or all of the relevant information once in the definition of the model class).

Fortunately, there's a way to drastically reduce the amount of code you have to write. Provided you don't need too much in the way of custom behavior from your form, Django provides a shortcut class called ModelForm that can automatically generate a moderately customizable form from a model definition, including all the relevant fields and the necessary save() method. At its most basic, here's how it works:

from django.forms import ModelForm
from cab.models import Snippet


class SnippetForm(ModelForm):
    class Meta:
        model = Snippet

Subclassing ModelForm and supplying an inner Meta class that specifies a model will set up this new SnippetForm class to automatically derive its fields from the specified model. And ModelForm is smart enough to ignore any fields in the model defined with editable=False, so fields like the HTML version of the description won't show up in this form. The only thing lacking here is that the author field will show up. Luckily, ModelForm supports some customizations, including a list of fields to specifically exclude from the form, so you can simply change the SnippetForm definition to the following:

class SnippetForm(ModelForm):
    class Meta:
        model = Snippet
        exclude = ['author']

And it'll leave the author field out. Now you can simply delete cab/forms.py and rewrite cab/views/snippets.py like this:

from django.http import HttpResponseRedirect
from django.forms import ModelForm
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from cab.models import Snippet


class SnippetForm(ModelForm):
    class Meta:
        model = Snippet
        exclude = ['author']


def add_snippet(request):
    if request.method == 'POST':
        form = SnippetForm(data=request.POST)
if form.is_valid():
            new_snippet = form.save()
            return HttpResponseRedirect(new_snippet.get_absolute_url())
    else:
        form = SnippetForm()
    return render_to_response('cab/add_snippet.html',
                              { 'form': form })
add_snippet = login_required(add_snippet)

However, this isn't quite right. The Snippet needs to have an author filled in, but you've left that field out of the form. You could go back and define a custom __init__() method again and pass in request.user, but ModelForm has one more trick up its sleeve. You can have ModelForm create the Snippet object and return it without saving; you do this by passing an extra argument—commit=False—to its save() method. When you do this, save() will still return a new Snippet object, but it will not save it to the database. This will leave you free to add the user yourself and manually insert the new Snippet into the database:

from django.http import HttpResponseRedirect
from django.forms import ModelForm
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from cab.models import Snippet


class SnippetForm(ModelForm):
    class Meta:
        model = Snippet
        exclude = ['author']


def add_snippet(request):
    if request.method == 'POST':
        form = SnippetForm(data=request.POST)
        if form.is_valid():
            new_snippet = form.save(commit=False)
            new_snippet.author = request.user
            new_snippet.save()
            return HttpResponseRedirect(new_snippet.get_absolute_url())
    else:
        form = SnippetForm()
    return render_to_response('cab/add_snippet.html',
                              { 'form': form })
add_snippet = login_required(add_snippet)

Now you can open up cab/urls/snippets.py and add a new import:

from cab.views.snippets import add_snippet

and a new URL pattern:

url(r'^add/$', add_snippet, name='cab_snippet_add'),

Simplifying Templates That Display Forms

The template outlined previously will continue to work because the form's fields haven't changed. But again, it would be nice if Django provided an easy way to show a form in a template without requiring you to write out all the repetitive HTML and check for field errors. You've eliminated the tedium of defining the form class itself, so why not eliminate the tedium of templating it?

To deal with this, every Django form has a few methods attached to it that know how to render the form into different types of HTML:

as_ul(): Renders the form as a set of HTML list items (<li> tags), with one item per field

as_p(): Renders the form as a set of paragraphs (HTML <p> tags), with one item per paragraph

as_table(): Renders the form as an HTML table, with one <tr> per field

So, for example, you could replace the templating you've been doing so far (a set of HTML paragraph elements) with only the following:

{{ form.as_p }}

But there are a few things to note when using these methods:

  • None of them output the enclosing <form> and </form> tags because the form doesn't “know” how or where you plan to have the form submitted. You'll need to fill in these tags yourself, with appropriate action and method attributes.
  • None of them output any buttons for submitting the form. Again, the form doesn't know how you want it to be submitted, so you'll need to supply one or more <input type="submit"> tags yourself.
  • The as_ul() method doesn't output the surrounding <ul> and </ul> tags, and the as_table() method doesn't output the surrounding <table> and </table> tags. This is in case you want to add more HTML yourself (which is a common need for form presentation), so you'll need to remember to fill in these tags.
  • Finally, these methods are not easily customizable. When you just need a basic presentation for a form (especially for rapid prototyping so you can test an application), they're extremely handy, but if you need custom presentation you'll probably want to switch back to templating the form manually.

Editing Snippets

Now you have a system in place for users to submit their code snippets, but what happens if someone wants to go back and edit one? It's inevitable that someone will accidentally submit some code that has a typo or a minor error, or find a better solution for a particular task. It would be nice to let users edit their own snippets in those cases, so let's go ahead and set up snippet editing through a view called edit_snippet.

Fortunately, this is going to be easy. ModelForm also knows how to edit an existing object, which takes care of most of the heavy lifting. All you have to do, then, is handle two things:

  • Figure out which Snippet object to edit.
  • Make sure that the user who's trying to edit the Snippet is its original author.

You can handle the first part fairly easily: you can set up your edit_snippet view to receive the id of the Snippet in the URL and to look it up in the database. Then you can compare the snippet's author field to the identity of the currently logged-in user to ensure that they match. So let's start by adding a couple more imports to cab/views/snippets.py:

from django.shortcuts import get_object_or_404
from django.http import HttpResponseForbidden

The HttpResponseForbidden class represents an HTTP response with the status code 403, which indicates that the user doesn't have permission to do whatever he was trying to do. You'll use it when someone tries to edit a snippet that he didn't originally submit.

Here's the edit_snippet view:

def edit_snippet(request, snippet_id):
    snippet = get_object_or_404(Snippet, pk=snippet_id)
    if request.user.id != snippet.author.id:
        return HttpResponseForbidden()
    if request.method == 'POST':
        form = SnippetForm(instance=snippet, data=request.POST)
        if form.is_valid():
            snippet = form.save()
            return HttpResponseRedirect(snippet.get_absolute_url())
else:
        form = SnippetForm(instance=snippet)
    return render_to_response('cab/edit_snippet.html',
                              { 'form': form })
edit_snippet = login_required(edit_snippet)

To tell a ModelForm subclass that you'd like it to edit an existing object, you simply pass that object as the keyword argument instance; the form will handle the rest. And note that because the Snippet already has an author, and that value won't be changing, you don't need to use commit=False and then manually save the Snippet. The form won't change that value, so you can simply let it save as is.

Now you can add a URL pattern for it. First you change the import line in cab/urls/snippets.py to also import this view:

from cab.views.snippets import add_snippet, edit_snippet

and then you add the URL pattern:

url(r'^edit/(?P<snippet_id>d+)/$', edit_snippet, name='cab_snippet_edit'),

Because the form for both the edit_snippet view and the add_snippet view will have the same fields, you can simplify the templating a bit by using only one template and passing a variable that indicates whether you're adding or editing (so that elements like the page title can change accordingly). So let's change the add_snippet view's final line to pass an extra variable called add, set its value to True, and change the template name to cab/snippet_form.html:

return render_to_response('cab/snippet_form.html',
                          { 'form': form, 'add': True })

Then you can change the same line in the edit_snippet view to use cab/snippet_form.html and set the add variable to False:

return render_to_response('cab/snippet_form.html',
                          { 'form': form, 'add': False })

Now you can simply have one template—cab/snippet_form.html—which can look like this:

<html>
  <head>
    <title>{% if add %}Add a{% else %}Edit your{% endif %} snippet</title>
  </head>
  <body>
    <h1>{% if add %}Add a{% else %}Edit your{% endif %} snippet</h1>
    <p>Use the form below to {% if add %}add{% else %}edit {% endif %}
       your snippet; all fields are required</p>
    <form method="post" action="">
      {{ form.as_p }}
      <p><input type="submit" value="Send"></p>
    </form>
  </body>
</html>

Now you have forms, views, and templates that let users both add and edit their code snippets. Here's the finished cab/views/snippets.py file, for reference:

from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.forms import ModelForm
from django.shortcuts import get_object_or_404, render_to_response
from django.contrib.auth.decorators import login_required
from cab.models import Snippet


class SnippetForm(ModelForm):
    class Meta:
        model = Snippet
        exclude = ['author']


def add_snippet(request):
    if request.method == 'POST':
        form = SnippetForm(data=request.POST)
        if form.is_valid():
            new_snippet = form.save(commit=False)
            new_snippet.author = request.user
            new_snippet.save()
            return HttpResponseRedirect(new_snippet.get_absolute_url())
    else:
        form = SnippetForm()
    return render_to_response('cab/snippet_form.html',
                              { 'form': form, 'add': True })
add_snippet = login_required(add_snippet)

def edit_snippet(request, snippet_id):
    snippet = get_object_or_404(Snippet, pk=snippet_id)
    if request.user.id != snippet.author.id:
        return HttpResponseForbidden()
    if request.method == 'POST':
        form = SnippetForm(instance=snippet, data=request.POST)
        if form.is_valid():
            snippet = form.save()
            return HttpResponseRedirect(snippet.get_absolute_url())
    else:
        form = SnippetForm(instance=snippet)
    return render_to_response('cab/snippet_form.html',
                              { 'form': form, 'add': False })
edit_snippet = login_required(edit_snippet)

Looking Ahead

Before moving on, I would suggest taking a little time to work with Django's form system. Although you should have a good understanding of the basics by now, you'll probably want to spend some time looking over the full documentation for the django.forms package (online at http://docs.djangoproject.com/en/dev/topics/forms/) to get a feel for all of its features (including the full range of field types and widgets, as well as more advanced tricks for customizing form presentation).

When you're ready to come back, the next chapter will wrap up this application by adding the bookmarking and rating features, including lists of the most popular snippets and the necessary template extensions to determine whether a user has already bookmarked or rated a snippet.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset