Madness

20 Apr

Firefox, with less than 2 dozen tabs open, running on my AMD Turion laptop with 2GB of RAM.

Django Dynamic Formset, v1.2

13 Apr

So, it’s two months later than I’d estimated, but sometime this morning, I committed version 1.2. In spite of the delay (which is a story for another time), I’m pretty excited about this release — I got to add a couple of nifty features, and squash some bugs in the process (thanks for the bug reports and patches, guys).

Here’s the list of changes:

  • Inline formsets created with “can_delete” set to True are now supported properly; clicking the “remove” link hides the form and sets the DELETE field, so Django handles the deleting when the page is POSTed.
  • Added form templates: you can now specify a form that will be cloned to generate the forms in the formset. As a side-effect, you can now delete all forms in a formset, and the “add” link still works as expected.
  • Clicking the “add” link now clones the last form, instead of the first; this works much better in the admin, especially if you have one or more extra forms (thanks justhamade).
  • Added an optional setting “extraClasses”; set this is an array of CSS classes, and they’ll be applied to the formset’s rows in turn. So, to get the row-striping effect in the Django admin, you’d do something like this:
    $('...').formset({extraClasses: ['row1', 'row2']});
    

    Adding and removing forms keeps the classes in sync, so your stripes don’t get all mix’d up — hopefully, this is a feature, but time will tell.

  • Updated examples and documentation.

As always, you can download the latest release from the Google Code page, or use the links below:

Django Dynamic Formset v1.1 released

30 Nov

Almost two months ago, I released the Django Dynamic Formset plugin, with the hope that someone out there might find it useful. Since then, I’ve received a good bit of feedback, which I’ve been meaning to roll into a new release. Last weekend, I finally made some time to update the documentation and examples, and today, I updated the project site on Google Code.

This new release contains two bugfixes (thanks to Wilson.Andrew.J and an anonymous fella), as well as a few more examples (thanks lfborjas). Here’s the short list of changes:

  • Fixed bug that erased the values on all checkboxes/radiobuttons in a cloned form
  • Fixed a bug in the way the “add new” event handler was being assigned (discoverd this while adding support for multiple formsets)
  • Added support for multiple formsets on the same page — see documentation
  • Added two new examples
  • Updated the documentation

You can download the releases from the Google Code page, or use the links below:

Big Thanks to all those who reported bugs, contributed patches or just left comments — you guys rock.

Using a FormWizard in the Django admin

28 Oct

A few weeks ago, I saw this question on Stackoverflow, asking how to integrate a Form Wizard with the Django admin. Although there’s an accepted answer, it doesn’t include any code, and doesn’t appear to take advantage of Django 1.1 features (more on these later), which make integrating custom views with the admin much easier and more seamless. I thought I’d share my solution here, in case there’s others out there asking the same question. You can download the source code for this example, if you’d like to follow along.

For our example, we’ll develop a wizard to add employers to a database, for a fictional recruitment application. Adding an employer will be a three-step process:

  1. First, choose a username and password, which the employer will use to login. Make sure to verify the password, by entering it twice.
  2. Enter the name and email address of the contact person for this employer.
  3. Enter the company’s name, description, address and website URL.

Clicking on “add new” on the employer changelist page, should display the wizard, rather than the default form .

Assumptions:

A number of features I mention are specific to Django 1.1 — if you’re already using 1.1, great! If not, maybe this’ll give you one more reason to upgrade? I sure hope so. I’ll assume you already have a functional installation of Django; head over to the docs if you need help setting up Django. I’ll also assume you know how to start a Django project — there’s plenty of introductory tutorials out there, including this one in the Django docs.

All set? Well, let’s get started then.

Step 1: Setup

  1. Create a new Django project and add a new app to it (We’ll call it “testapp”). Add the app to INSTALLED_APPS in your settings.py. Also, add “django.contrib.admin” to INSTALLED_APPS.
  2. Configure the DATABASE_* settings for your database of choice — I’ll be using SQLite.
  3. Add the path(s) to the folder where you’ll store your templates in TEMPLATE_DIRS
  4. Edit the main Urlconf, and follow the comments in it to enable the admin urls.
  5. Run “syncdb” to create the database tables, then “runserver” to start the development server.

Done? On to step 2 then.

Step 2: Create the model

Next, we’ll create the Employer model. Save the code below as “testapp/models.py” (move your mouse over the code below, and click the “copy to clipboard” link, then paste into a new file):

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

class Employer(models.Model):
    user = models.OneToOneField(User)
    company_name = models.CharField(max_length=60)
    company_description = models.TextField(blank=True)
    address = models.TextField()
    website = models.URLField(verify_exists=False, blank=True)

    class Meta:
        ordering = ('company_name',)

    def __unicode__(self):
        return self.company_name

Pretty simple stuff. We’ll store the login and contact information for an employer in the User table, and the company information in the Employer table. Next, we’ll create our forms.

Step 3: Create the forms

We’ll need three forms for our wizard, one for each step of the employer creation process. Here’s the code (save as “testapp/forms.py”):

from django import forms
from django.forms import fields, models, widgets
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django.contrib.formtools.wizard import FormWizard
from django.utils.encoding import force_unicode
from testapp.models import Employer

class ContactForm(forms.Form):
    first_name = fields.CharField(max_length=50, label="Contact's first name")
    last_name = fields.CharField(max_length=50, label="Contact's last name")
    email = fields.EmailField(label="Contact's email address")

class EmployerForm(models.ModelForm):
    class Meta:
        model = Employer
        exclude = ('user',)

For the first step in the wizard, we’ll reuse the UserCreationForm from django.contrib.auth.forms. The ContactForm and EmployerForm handle the second and third steps, respectively. So far, this is all pretty basic forms stuff; if any of this is new to you, you really should check out the forms documentation.

Next, we’ll create our wizard. A wizard is simply a subclass of django.contrib.formtools.wizard.FormWizard, which defines a “done” method, specifying what action to perform once all the wizard’s forms have been submitted; the actual forms are passed as a list to the subclass’s constructor. Once you’ve created an instance of your wizard, you can use it in a Urlconf, just like any other view (remember, a view doesn’t have to be a function — it just has to be callable).

We’ll create the wizard below the other forms:

class EmployerCreationWizard(FormWizard):
    @property
    def __name__(self):
        return self.__class__.__name__

    def done(self, request, form_list):
        data = {}
        for form in form_list:
            data.update(form.cleaned_data)
        # First, create user:
        user = User(
            username=data['username'],
            first_name=data['first_name'],
            last_name=data['last_name'],
            email=data['email']
        )
        user.set_password(data['password1'])
        user.save()
        # Next, create employer:
        employer = Employer.objects.create(
            user=user,
            company_name=data['company_name'],
            address=data['address'],
            company_description=data.get('company_description', ''),
            website=data.get('website', '')
        )
        # TODO: Display success message and redirect to changelist.

create_employer = EmployerCreationWizard([UserCreationForm, ContactForm, EmployerForm])

The code above should be mostly straightforward — we create an instance of User, save it, then pass it to the “create” method of the Employer model (well, strictly speaking, it’s the Employer model’s manager). Notice we call “set_password” on the User instance to handle the password hashing stuff for us.

We also define a “__name__” property on the wizard. We do this because I’ve noticed a number of Django decorators raise an AttributeError when you use them to decorate an instance, complaining they can’t find __name__ (Python instances don’t have a __name__ property, though functions and classes do). Since we’ll be decorating our wizard instance in a moment, we provide the __name__ property, so everyone’s happy.

See the #TODO at the end? We’ll be adding code to handle that in a moment. First, though, a small detour to register our Employer model with the admin.

Step 4: Register the Employer model with the admin

Now things start to get interesting. We’ll take advantage of two Django 1.1 features, to integrate the create_employer wizard with the admin:

  1. The “ModelAdmin.get_urls” method returns the URLs to be used for a ModelAdmin subclass in the same way as a Urlconf; because of this, you can easily add new urlpatterns to your ModelAdmin subclass.
  2. The “ModelAdmin.admin_site.admin_view” wrapper can be applied to any view, to mark it as an admin view. Each time the view is accessed, it checks the current user’s permissions and redirects to the login page, if necessary. It also marks the wrapped view as non-cacheable, by default (in effect, admin_view is equivalent to applying the staff_member_required and never_cache decorators).

Here’s the code for our ModelAdmin subclass; save this as “testapp/admin.py”:

from django.conf.urls.defaults import url, patterns
from django.contrib import admin
from django.utils.encoding import force_unicode
from django.utils.functional import update_wrapper
from testapp.models import Employer
from testapp.forms import create_employer

class EmployerAdmin(admin.ModelAdmin):
    def get_urls(self):
        def wrap(view):
            def wrapper(*args, **kwds):
                kwds['admin'] = self   # Use a closure to pass this admin instance to our wizard
                return self.admin_site.admin_view(view)(*args, **kwds)
            return update_wrapper(wrapper, view)

        urlpatterns = patterns('',
            url(r'^add/$',
                wrap(create_employer),
                name='testapp_employer_add')
        )
        urlpatterns += super(EmployerAdmin, self).get_urls()
        return urlpatterns

admin.site.register(Employer, EmployerAdmin)

There’s a few things to note in the above code:

  1. We bind the create_employer wizard to the pattern ‘^add/$’, which is the same pattern used by ModelAdmin.add_view; since our urlpattern appears before the default patterns, we’ve effectively overriden the default add view.
  2. We define a decorator “wrap”, which applies the admin_site.admin_view wrapper to our wizard, and calls “update_wrapper” on the wrapped function to preserve its name and docstring.
  3. We pass a reference to the current ModelAdmin instance to the wizard; this allows us access to the admin from within the wizard’s methods.

If you browse to the admin add view for the employer (if you’re using the development server defaults, your URl should look like: http://localhost:8000/admin/testapp/employer/add/), you should get a TemplateNotFound error. Let’s create a template, so we can see how everything looks.

Step 5: Create a template

By default, the wizard looks for the template “forms/wizard.html”. You can change this by overriding the “get_template” method, but we’ll use the default for now. Save the HTML below as “templates/forms/wizard.html” (assuming your TEMPLATE_DIRS in settings.py includes “templates”):

{% extends "admin/change_form.html" %}
{% load i18n %}

{% block content %}
<div id="content-main">
    <form {% if form.form.is_multipart %}enctype="multipart/form-data" {% endif %}method="post" action="" id="{{ opts.module_name }}_form">
        <div>
            {% if form.form.errors %}
            <p class="errornote">
                {% blocktrans count form.form.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
            </p>
            <ul class="errorlist">
                {% for error in form.form.non_field_errors %}
                <li>{{ error }}</li>{% endfor %}
            </ul>
            {% endif %}

            {% for fieldset in form %}
              {% include "admin/includes/fieldset.html" %}
            {% endfor %}

            <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
            {{ previous_fields|safe }}

            <div class="submit-row">
                <input type="submit" value="{% ifequal step step_count %}Finish{% else %}Next &raquo;{% endifequal %}" class="default" name="_save" />
            </div>

            <script type="text/javascript">document.getElementById("{{ form.first_field.auto_id }}").focus();</script>
        </div>
    </form>
</div>
{% endblock %}

Now go back to the admin add view for employer and hit refresh.

Oops.

What happened to the breadcrumbs? The page title? The form??

Don’t despair though…the hard part’s over. Go on and take a break — stretch, get coffee, grab a snack, talk to another human being.

Back already? You’re pretty quick. Let’s fix the template now, shall we?

Step 6: Putting things in Context

As you’ve probably figured out by now, our template context is a few variables short. From the ModelAdmin source code (somewhere around line 772 in “django/contrib/admin/options.py”):

context = {
            'title': _('Add %s') % force_unicode(opts.verbose_name),
            'adminform': adminForm,
            'is_popup': request.REQUEST.has_key('_popup'),
            'show_delete': False,
            'media': mark_safe(media),
            'inline_admin_formsets': inline_admin_formsets,
            'errors': helpers.AdminErrorList(form, formsets),
            'root_path': self.admin_site.root_path,
            'app_label': opts.app_label,
        }

We can safely ignore a number of these (such as “is_popup”, “show_delete” and “inline_admin_formsets”), but the others need to be present for the template to render correctly. Fortunately, the FormWizard provides a solution: the “parse_params” method.

According to the docstring, parse_params is a “hook for setting some state, given the request object, and whatever *args and **kwargs were passed to __call__()“. Remember the “admin” keyword argument we passed in get_urls? That gets passed along to parse_params, where we can extract it, and use it to populate FormWizard.extra_context. Add the code below to the EmployerCreationWizard in “testapp/forms.py”:

def parse_params(self, request, admin=None, *args, **kwargs):
    self._model_admin = admin # Save this so we can use it later.
    opts = admin.model._meta # Yes, I know we could've done Employer._meta, but this is cooler :)
    self.extra_context.update({
        'title': u'Add %s' % force_unicode(opts.verbose_name),
        'current_app': admin.admin_site.name,
        'has_change_permission': admin.has_change_permission(request),
        'add': True,
        'opts': opts,
        'root_path': admin.admin_site.root_path,
        'app_label': opts.app_label,
    })

Go to the employer add view again and hit refresh…much better. Our title now shows up, and breadcrumbs work. But what is up with our form?

It turns out that the “form” our template expects isn’t an instance of “django.forms.BaseForm”, but rather an instance of “django.contrib.admin.helpers.AdminForm”. What we need is to convert our BaseForm instance to an AdminForm instance. Again, the FormWizard comes to the rescue, by providing the “render_template” method. Add the code below to the EmployerCreationWizard, below parse_params:

def render_template(self, request, form, previous_fields, step, context=None):
    from django.contrib.admin.helpers import AdminForm
    # Wrap this form in an AdminForm so we get the fieldset stuff:
    form = AdminForm(form, [(
        'Step %d of %d' % (step + 1, self.num_steps()),
        {'fields': form.base_fields.keys()}
        )], {})
    context = context or {}
    context.update({
        'media': self._model_admin.media + form.media
    })
    return super(EmployerCreationWizard, self).render_template(request, form, previous_fields, step, context)

The AdminForm constructor takes a form instance, a list of fieldsets and a dictionary of prepopulated fields. We pass it the form instance passed to render_template, a single fieldset comprising all the fields in the form, and an empty dictionary (since we don’t care about the prepopulated fields). Next, we update the supplied context with the form media, then pass both the form and the updated context to the superclass render_template.

Go to the add view once more, and hit refresh. Sweet, eh?

Step 7: Wrapping up

Remember that #TODO in EmployerCreationWizard.done? Let’s fix that now. Replace the #TODO line with the following lines:

# Display success message and redirect to changelist:
return self._model_admin.response_add(request, employer)

That’s it, we’re done. Go on over to the add view and try adding a couple of employers.

I hope someone out there finds this useful. If you’ve got other tips for integrating custom views with the admin, share them in the comments (or, post a link to an article you wrote on your blog).

Download the demo project:

customadmin.zip (~7.49KB)

jQuery plugin: chained select lists

14 Oct

A few days ago I was looking for a jQuery plugin that would allow me link select lists together, so that selecting an option in one list (the parent) would update the options in the other list (the target). I’d used linkedSelect on a previous project, but for some reason, my ISP kept returning “IP has been banned” each time I tried to browse to the site.

As it turns out, this was a good thing. Eventually, I found this post on Remy Sharp’s blog, and his selectChain plugin was almost exactly what I was looking for. Almost.
There were a few changes I needed to make first:

  1. Add support for the metadata plugin: since I’ll mostly be using this in my Django projects, being able to specify plugin options declaratively means I can wrap the plugin in a custom widget.
  2. Add support for caching
  3. Update target list when the page is loaded, if the parent list has an option selected.
  4. Preserve the selected option in the target list, if possible (as long as the updated options contain the previous option), when it is updated
  5. Add a few niceties, such as a preloader GIF :)

To use the plugin, download it and include a reference to it in your HTML page (don’t forget to include a reference to jQuery first!). I’ll assume, you’ve already written the server-side code in your choice framework. If you’d like to be able to customize the plugin behavior declaratively, for specific selects, you’ll also need to download the metadata plugin.

Examples

We’ll use the following HTML:

<form method="post" action="...">
  <label for="id_author">Author:</label>
  <select name="author" id="id_author">
    <option value="1">Enid Blyton</option>
    <option value="2">J. K. Rowling</option>
    <option value="3">Roald Dahl</option>
  </select>
  <label for="id_book">Book:</label>
  <select name="book" id="id_book">
  </select>
</form>

Include the following script before the closing BODY tag, or in your HEAD:

$(function() {
  $('#id_book').chainedSelect({
    parent: '#id_author',
    url: 'http://your.server/app',
    value: 'id',
    label: 'text'
  });
});

That’s it. Your server application should return a JSON-encoded list of objects, having “id” and “text” properties. The “id” will be used as the value of the generated option, the “text” as its label. Selecting an author from the list, should display a list of their books.

If you’re using the metadata plugin, you can move the options into the target select list. Change the HTML for the “book” list to this:

<select name="book" id="id_book" class="chainedSelect {parent: '#id_author', url: 'http://your.server/app', value: 'id', label: 'text'}">
</select>

Then, replace the previous script with the following:

$(function() {
  $('.chainedSelect').chainedSelect();
});

Simpler, isn’t it?

Options

Here’s a list of options you can pass, either as metadata, or in the call to “$.chainedSelect”, to customize the plugin:

  • parent: A jQuery ID selector, for the parent list – must begin with a ‘#’. Required.
  • url: The URL to the server component. The server will be passed a single querystring parameter named “q”, whose value will be the selected option in the parent list. It should return a JSON-encoded list of objects, which will be used to populate the target list. Required.
  • value: The property in the objects returned from the server, which should be used as the value for the generated option. Defaults to “pk”
  • label: The property in the objects returned from the server, which should be used as the label for the generated option. Defaults to “name”
  • type: The HTTP method used to submit the data to the server – defaults to “GET”.
  • preloadUrl: A URL to a GIF image, displayed while waiting for an AJAX request to complete. For best results, images should be as small as possible – no larger than 16×16 pixels
  • error: Use this to specify a function that will be called if the AJAX request returns an error

Prerequisites

jQuery, of course (tested with 1.2.6)
A server-side component, to generate the JSON used to populate the select lists

Download

jquery.chainedSelect source (6.3KB)
jquery.chainedSelect minified (2.1KB)

If you find this useful, or have any questions, ideas or issues, leave a comment.

jQuery plugin: Django Dynamic Formset

10 Oct

Several months ago, I posted this snippet over at Django Snippets. Since then, I’ve used it a few times, and eventually made it into a jQuery plugin. I always intended to release the plugin, but didn’t have time to write some decent documentation, and do a demo project.

After reading the comments on the snippet, I finally decided to bite the bullet this weekend, so here it is. To use it, download one of the releases (I’d recommend the one with the demo project), unzip it, and check out README.txt and INSTALL.txt.

Usage

I’ll assume you’ve already created your formsets. You can create
formsets using any of the provided methods: both regular formsets
(created with the “formset_factory“) and inline formsets
(created with the “inlineformset_factory“) are supported.

I’ll assume you’ve already created your formsets. You can create formsets using any of the provided methods: both regular formsets (created with the “formset_factory“) and inline formsets (created with the “inlineformset_factory“) are supported.

  1. First, copy jquery.formset.js to your MEDIA_ROOT; don’t forget to include the jQuery library too!
  2. Include a reference to the script in your template; again, don’t forget to reference the jQuery library (before the reference to the script).
  3. Render the formset as you normally would — I usually use a table but you can use DIVs, Ps or whatever you desire. Let’s use the example markup below:
    <form id="myForm" method="post" action="">
      <table border="0" cellpadding="0" cellspacing="0">
        <tbody>
        {% for form in formset.forms %}
          <tr>
            <td>{{ form.field1 }}</td>
            <td>{{ form.field2 }}</td>
            <td>{{ form.field3 }}</td>
          </tr>
        {% endfor %}
        </tbody>
      </table>
      {{ formset.management_form }}
    </form>
    
  4. Add the following script to your template (before the closing BODY tag, or in your HEAD, below the reference to jquery.formset.js):
    <script type="text/javascript">
      $(function() {
        $('#myForm tbody tr').formset({
          prefix: '{{ formset.prefix }}'
        });
      })
    </script>
    

    Notice how our jQuery selector targets the container for each form? We could have assigned a class to each TR and used that instead:

      $('.form-container-class').formset(...);
    

    Either way is fine, really :)

  5. That’s it. Fini. Save your template and navigate to the appropriate view in your application, and you should see an “add another” link. Clicking on it should add another instance of your form to the page. You can remove instances by clicking the “remove” link; if there’s only one form in the formset, the remove link isn’t shown.

License

This plugin is released under the New BSD License – use it as you wish.

Quick Download

For the impatient, you can download the releases here:

You might also want to check out the Google Code project.

If you find this useful, feel free to leave a nice comment saying so :) If you find bugs, want to submit a patch, or have an idea for a cool enhancement, submit them to the issue tracker.

Update (3oth Nov, 2009)

Version 1.1 is now available — I’ve updated the download links above to point to it. For details of the changes in 1.1, see the accompanying blog post.

Parsing experiments in Python

7 Oct

Recently, I’ve been experimenting with parser generators in Python (we use Python for a lot of our projects at Evince). Previously, I’d hand-written recursive-descent parsers for simple parsing tasks, but I’d never felt comfortable enough using them in production code.

A couple of weeks ago, Essien showed me Jane – a toy language he’d been developing, which compiled to C. After a few minutes exploring his implementation, I tried to quickly code a backend that generated Python code. My goal wasn’t really a fully-functional implementation, just to find out how far I could go. I did get it semi-functional – it choked on nested blocks, because of Python’s indentation.

Anyways, looking at flex and bison got me thinking “surely, there’s got to be stuff like this in Python?” so I did some googling and came across an O’Reilly article on pyparsing. My test project was an expression parser – addition, subtraction, multiplication and division, with support for floating-point numbers and variables. After running into problems trying to use pyparsing with recursive productions, I did some more searching and landed on this page…woot!

Eventually, I selected SPARK (because it looked interesting) and SimpleParse (because it was based on the fast mxTextTools library). I tried out SimpleParse first, and had a parser in minutes – pretty much the time it took me to type out the grammar. It took a bit longer to generate an AST (mostly time spent studying the documentation, and trying several wrong approaches…hey, I’m new at this stuff :), but eventually, I had that done too. Just for good measure, I threw in peak.util.assembler and compiled the AST to Python bytecode, and finally used the “new” module to generate a native Python function. Fun!

The next day, I showed all this to Bayo, and suggested he could use it in the payroll application (part of a larger HR application we’re developing). While discussing his current implementation, he mentioned expressions depending on other expressions, and I thought “that’s easy; functions can call other functions, no problem”. Of course, this meant the parser had to support function calls. I thought “okay, whatever, I’ll have a go at it later”.

At about 2:30 am, I was about to go to bed, and thought I’d just take a look, and see how much work it’d be adding support for functions. 3 minutes later, I was done with the implementation – I added a new production for (no-argument) functions, added a new AST node and modified my compiler to emit the appropriate bytecode for function calls (the “dis” module came in pretty handy here). Needless to say, I’m hooked.

Since then, I’ve tried to implement a vCard parser (the formal grammar specification for vCard 3.0 is here), port Jane’s compiler to Python and build a simple template language. I’ve run into, what I’d consider limitations of SimpleParse: its inability to specify ignored characters, and it’s lack of support for back-tracking. Possibly, though, these are as a result of my own inexperience, but I’d like to try out a few other parser-generators first. Next up: PLY.

Follow

Get every new post delivered to your Inbox.