Django :: looking at newforms

(This is rapidly getting outdated, go read the source at http://code.djangoproject.com/browser/django/trunk/django/newforms for the current state…).

Django is getting new form handling code, currently in the module django.newforms but it will migrate to django.forms in a future release. The code is very new and under active development. Here are just some quick notes after browsing the code (r3945).

The newforms package is divided into fields, forms, util, and widgets. Widgets are classes that have a render method that returns the html widget as a unicode string (flatatt is a function that flattens a dictionary):

[sourcecode language=”python”]
class TextInput(Widget):
def render(self, name, value, attrs=None):
if value is None: value = ”
final_attrs = dict(self.attrs, type=’text’, name=name)
if attrs:
final_attrs.update(attrs)
if value != ”: final_attrs[‘value’] = value # Only add the ‘value’ attribute if a value is non-empty.
return u'<input %s />’ % flatatt(final_attrs)
[/sourcecode]

The currently defined widgets include TextInput, Textarea, and CheckboxInput.

Fields are classes encapsulating the validation of input data. They should have constructors that take a widget parameter in addition to captioning validation parameters like max_length etc., and a to_python method that converts an input value to a Python value:

[sourcecode language=”python”]
class MyField(django.newforms.Field):
def __init__(self, …, required=True, widget=None):
# set the widget
super(MyField, self).__init__(self, required, widget)
… save other validation parameters here …

def to_python(self, value):
# validates empty values vs. required fields
super(MyField, self).to_python(value)
… validate value here …

[/sourcecode]

The current field classes include CharField, IntegerField, DateField, DateTimeField, RegexField, EmailField, and BooleanField. The date fields are by default US centric but you can pass a tuple of format strings in the input_formats parameter to suit your local traditions. The django.newforms.util.ValidationErrors that are thrown currently contain English text unless I’m reading the code the wrong way (it’ll probably be internationalized shortly).

Finally the Form class contains a metaclass to allow for convenient declaration of fields

[sourcecode language=”python”]
class MyForm(django.newform.Form):
name = CharField(max_length=35)
dob = DateField(input_formats=[‘%d/%m/%Y’], required=False, widget=TextInput)

[/sourcecode]

The field names are available to methods of MyForm in the self.fields dictionary.The default widget is TextInput.

Posted in django | Leave a comment

Django :: Custom Field

If you don’t do so allready, you should really get into the habit of looking at the validation errors your users are seeing. If you’re using the idiomatic Django view code this is very easy to do. After you get a POST request, you’ll verify the data:

[sourcecode language=”Python”]
# validate the form
errors = sform.get_validation_errors(new_data)
[/sourcecode]

then all you have to do is write the errors dict to a log (a database table with a big message field works as well as most other approaches). When I did this with a project where I’d used the date widget from a couple of posts ago, users were having big problems entering dates correctly (it was a quick project for users that shouldn’t have any problems entering dates in new formats, but still there were problems).

The first solution I wanted to try was to allow users to enter dates in their normal format. Normal for these users meant that October 30th, 2006 would either be entered as 30.10.2006 or 30/10/2006. You can do this in Django by simply writing a validator for this pattern, but since I knew I needed to write custom field classes for other things I wanted to do, this is the solution I ended up with:

[sourcecode language=”Python”]
import re, datetime
from django import forms
from django.core import validators

class BirthDay(forms.FormField):
@staticmethod
def validator(field_data, all_data):
# split on . or / and expect to get three items
fields = re.split(r'[\./]’, field_data)
if len(fields) != 3:
errtxt = "Date must look like: 30.10.2006."
raise validators.ValidationError(errtxt)
try:
fields = map(int, fields)
except ValueError:
raise validators.ValidationError("Only use numerical date parts.")
try:
d = datetime.date(fields[2], fields[1], fields[0])
except ValueError:
raise validators.ValidationError("Invalid date.")
today = datetime.date.today()
if d> today:
raise validators.ValidationError("You’re not born yet.")
if today.year – d.year> 120:
errtxt = "Invalid date: did you enter a 4 digit year?"
raise validators.ValidationError(errtxt)
return True

@staticmethod
def html2python(data):
# data is allready validated
fields = map(int, re.split(r'[\./]’, data))
return datetime.date(fields[2], fields[1], fields[0])

def __init__(self, field_name=”, is_required=False):
self.field_name = field_name
self.validator_list = [BirthDay.validator]
self.is_required = is_required

def render(self, data):
widget = ‘<input id="%s" name="%s" type=text maxlength=10 size=10 value="%s" />’
return widget % (self.get_id(), self.field_name, forms.escape(data))
[/sourcecode]

If you look closely it’s all almost trivial code. I try to be as thorough as possible in the validator so that I can issue relevant error messages. The html2python method relies on validation allready being done, which it would be when following the standard Django idiom (you cannot in general call the validator from here since the validator might need other fields in order to validate…) The magic happens in the render method, by making sure you provide the correct id and name for the input tag, and feed it data that the user has allready provided.

After this is done, you can use the new class in the custom manipulator we wrote last time:

[sourcecode language=”Python”]
class SuggestionForm(forms.Manipulator):
def __init__(self):
self.fields = [
forms.TextField(‘name’, is_required=True),
BirthDay(‘dob’, is_required=False),
forms.LargeTextField(‘comment’, is_required=True),
]
[/sourcecode]

This was perhaps not the best documented corner of Django, but the code in the forms module was exceedingly easy to follow.

Posted in django | Leave a comment

Django :: setup for the form examples

I normally develop on Windows and deploy on Linux which has so far worked without a single problem. On my development system I followed the effbot receipe to get Django up and running in five minutes. That receipe was written before Django 0.95 was released, and we’re not going to be using the database, so you can simplify the process by downloading and installing version 0.95 from the Django download page. I’m using Python 2.4.3, but this should work with Python 2.5 also. The Django 0.95 release doesn’t support the sqlite3 that comes bundled with Python 2.5, so you’d have to use the current development version until a new official release is cut if this is going to be important to you (as I was saying, for these examples it won’t be).

I did use the effbot’s exemakerutility to turn django-admin.py into an executable. Exemaker is very cute, I highly recommend it. If you elect to not use exemaker, you’ll have to change the first line below to something that will find django-admin.py (it will normally be in c:\python24\scripts).

[sourcecode language=”powershell”]
c:\> django-admin startproject mysite
c:\> cd mysite
c:\mysite> mkdir templates
c:\mysite> python manage.py startapp myapp
[/sourcecode]

Change settings.py, adding “/mysite/templates” to TEMPLATE_DIRS, and adding “mysite.myapp” to INSTALLED_APPS. Then start the server:

[sourcecode language=”powershell”]
c:\mysite> python manage.py runserver
[/sourcecode]

That’s it, click here to check that it worked — you should be seeing a page that says “It worked!” 😉

Posted in django | Leave a comment

Django :: interacting with the browser

All of the Django tutorials that were around when I started learning about it focused very much on the database and models — and Django really does shine in this area. I felt there wasn’t enough documentation on how to interact with the end-user’s browser however, which is why I decided to write this. In particular I will focus on code that will go into the view.py file which would be called the controller code in a MVC framework. This code is responsible for the server-side of a form submission, i.e. selecting the widgets that are used, validating input, etc. The view code will fill a Context that’s used to render a template to generate the html that goes to the user. I will not cover any advanced features of the Django template language. In many (most) situations the view code will manipulate a database based on what the user enters on a form, generally through the database access layer Django creates from the model code. The view code doesn’t have to interact with Django model code of course, it could just as well use SQLAlchemy or SQLObject to e.g. run reports on a legacy database that Django’s database layer doesn’t support, or simply send an email. In versions prior to 0.95, Django required you to setup a database even if you were not going to use it but this is no longer the case (thanks Adrian).

In this post I’ll simply try to get a bare bones form up where a user can enter his name, his date of birth, and a message. There are probably easier ways to accomplish such a simple task, and if you look at the end-result it is quite underwhelming in it’s functionality, but I’ll remedy that in a later posting… For now I’m satisfied that when the user presses a button marked send a message is printed on the console.

Routing the url: We want the user to come to our form by going to http://www.mysite.com/suggestions/. To accomplish this we’ll have to change the urls.py file, adding the following line to the urlpatterns:

[sourcecode language=”python”]
(r’^suggestions/$’, ‘mysite.myapp.views.suggestion_form’),
[/sourcecode]

This tells Django that requests for that url should be routed to the suggest_form function in the views.py file in the myapp directory.

Next we have to subclass django.forms.Manipulator, so that we can define the widgets we want the form to have (e.g. forms.LargeTextField for the user suggestion). In views.py add the following:

[sourcecode language=”python”]
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_responsefrom django import forms

class SuggestionForm(forms.Manipulator):
def __init__(self):
self.fields = [
forms.TextField(‘name’, is_required=True),
forms.DateField(‘dob’, is_required=False),
forms.LargeTextField(‘comment’, is_required=True),
]

def send_email(self, data):
print ‘Got name =’, data[‘name’]
print ‘Got dob =’, data[‘dob’]
print ‘Got comment =’, data[‘comment’]
[/sourcecode]

This is called a custom manipulator in the Django documentation, but if you?re like me you?ll end up writing a lot of these. This class will encapsulate much of the ?business? logic of the form, as opposed to the ?processing? logic that we?ll get to next.

The prosessing logic of a form submission goes as follows (this is going to be the same logic for pretty much all forms):

  1. if the request is a POST request, and the data validates, perform the form action and redirect the user to a result page (this prevents multiple submissions if the user hits the back button).
  2. if it is a POST request and the data doesn?t validate,display the form and notify the user of the validation errors.
  3. if it is a GET request, clear all fields and display the form

That becomes the idiomatic/boilerplate:

[sourcecode language=”python”]
def suggestion_form(request):
sform = SuggestionForm()

if request.POST:
# Create a copy of the form data because
# do_html2python below works in-place.
new_data = request.POST.copy()
# validate the form
errors = sform.get_validation_errors(new_data)

if not errors:
# convert text from form to Python objects
sform.do_html2python(new_data)
# and perform form action
sform.send_email(new_data)

return HttpResponseRedirect(‘thankyou’)
else:
# it’s a GET request
errors = new_data = {}

# FormWrapper collects the widgets for easy access through
# Django’s template language.
fwrap = forms.FormWrapper(sform, new_data, errors)
return render_to_response(‘suggestion_form.html’, dict(form=fwrap))
[/sourcecode]

Write the template: Finally it’s time to write the html. Store it in your template directory and call it suggestion_form.html:

[sourcecode language=”html”]
<html>
<head>
<title>Comment Form</title>
<style type="text/css"> /* some minimal layout */
label, span.widget { display:block; float:left; }
label { width:10em; text-align:right; margin-right:0.3em;}
br { clear:left; } .required { font-weight: bold; }
span.error { font-weight:bold; color:red }
</style>
</head>
<body>
<form method="POST" action=".">
<label for="id_name" class="required">Name</label>
<span class="widget">{{ form.name }}</span>
{% if form.name.errors %}
<span class="error">{{ form.name.errors|join:", " }}</span>
{% endif %}
<br>
<label for="id_dob">Birthdate</label>
<span class="widget">{{ form.dob }}</span>
{% if form.dob.errors %}
<span class="error">{{ form.dob.errors|join:", " }}</span>
{% endif %}
<br>
<label for="id_comment" class="required">Suggestion</label>
<span class="widget">{{ form.comment }}</span>
{% if form.comment.errors %}
<span class="error">{{ form.comment.errors|join:", " }}</span>
{% endif %}
<br>
<input type=submit value="Send">

<script type="text/javascript">
document.getElementById("id_name").focus();
</script>
</form>
</body>
</html>
[/sourcecode]

The little snippet of javascript puts the cursor in the first field. The id of the widget produced by the field classes are named with the prefix “id_” in front of their name and this needs to be matched up with the “for” value in the <label ...>.

To finish up, add the following to urls.py

[sourcecode language=”python”]
(r’^suggestions/thankyou/$’,
‘django.views.generic.simple.direct_to_template’, {‘template’: ‘thankyou.html’}),
[/sourcecode]

And add the following to c:/mysite/templates/thankyou.html

[sourcecode language=”html”]
<html>
<body>
Thanks for your submission.
</body>
</html>
[/sourcecode]

That’s it! You should be able to start the development server and test it out by clicking here.

Posted in Uncategorized | Leave a comment