November 1, 2009

Dynamic select fields with JQuery and django

Posted in Uncategorized tagged , , , , at 5:44 pm by Simon Greenwood

Update Feb 2012 a few people have come across this and I don’t think it works with django any more, especially since it doesn’t pass form validation (although I think that can be disabled) and probably doesn’t work with csrf.

I don’t post much about my experiences with django as I’m generally slowly learning and not really coming up with any revelations. However, this week, I’ve come up against a problem that has half an answer, but not a satisfactory one, and one that no-one on the django-users mailing list seems to have been able to resolve.

Your form has nested categories that depend on each other: a top category and a sub category, rather like eBay. Rather than reloading the whole page to get a list of subcategories when a top category is selected, it would look much nicer just to populate the subcategory select field. This is a job for javascript, specifically AJAX, and to save reinventing the wheel, it’s useful to use a library, which in my case is currently JQuery.

This blog post and this blog post set me off in the right direction by showing how to use JQuery’s getJSON function and how to craft custom fields for the lookup.

First, you’ll need to define a form. In this case I’m using a ModelForm with a couple of custom field definitions. This is probably slightly redundant as the field choices will be looked up by the ModelForm factory, but we are changing the definition of the prodsubcat field by disabling its widget, so I’ve also included the field choices for completeness:

class ProductForm(ModelForm):
    prodtopcat = forms.ModelChoiceField(ProductTopCategory.objects, widget=forms.Select)
    prodsubcat = forms.ModelChoiceField(ProductSubCategory.objects, widget=forms.Select(attrs={'disabled': 'true'}))

    class Meta:
        model = Product

Something I bashed my head against for a while was from Dustin’s post: setting the choices option on the widget as follows to display a message in the disabled subcategory field:

prodsubcat = forms.ModelChoiceField(ProductSubCategory.objects, widget=forms.Select(attrs={'disabled': 'true'}), choices=(('-1','Select Make'),))

which looks neat, but wipes out the choices dictionary that is loaded at render time. This is important as it’s that dictionary that the form submission process validates against. I got stuck on that for a day or so.

The javascript to make the JSON request sits in the SCRIPT section in the HEAD part of your template and is pretty simple. It also needs a current version of jQuery to be present:

  $(function(){
    $("select#id_prodtopcat").change(function(){
      $.getJSON("/products/feeds/subcat/"+$(this).val()+"/", function(j) {
        var options = '<option value="">---------- </option>';
        for (var i = 0; i < j.length; i++) {
          options += '<option value="' + parseInt(j[i].pk) + '">' + j[i].fields['longname'] + '</option>';
        }
        $("#id_prodsubcat").html(options);
        $("#id_prodsubcat option:first").attr('selected', 'selected');
        $("#id_prodsubcat").attr('disabled', false);
      })
      $("#id_prodtopcat").attr('selected', 'selected');
    })
  })

This calls a django view that returns a JSON object using HTTP GET. The view looks like this:

def feeds_subcat(request, cat_id):
	from django.core import serializers
	json_subcat = serializers.serialize("json", ProductSubCategory.objects.filter(prodcat = cat_id))
	return HttpResponse(json_subcat, mimetype="application/javascript")

This returns an object filtered on the ProductTopCategory relationship and passes it through the serialiser. The jQuery function(j) formats the JSON object as a HTML SELECT field and writes it to the form using JQuery’s document.write function.

You should now have a form that among other things has an active top category select field and a disabled sub category select field. Selecting an option in the top category field should enable the sub category and populate it with a filtered set of options.

Submit the form, and if you’re using the standard django method of submission and validation, it should pass and you can continue to process the form.

The thing that I got stuck on was the validation aspect: in retrospect it’s fairly obvious that the form will validate against the object that it has in memory, but I got sidetracked for a while in other solutions such as attempting to replace the submitted form data, which can’t be done as request.POST is read only (you could make a copy but that really just adds to the codebase), and also creating custom validation for the field that did a lookup for a record that matched the selected option, thus overriding validation of the rendered field.

It’s still a little bit hacky in that it needs two lookups but on the other hand the alternative is to load a potentially big directory structure into the browser memory in order to filter the choices.

Inline editing with Jquery, Jeditable and django

Posted in Uncategorized tagged , , , , at 5:40 pm by Simon Greenwood

…and two come along at once

In attempting to keep my current project easy to use and pleasingly contemporary, I decided to get javascripty on the user dashboard by enabling editing in place, like all the best Web 2.x sites *cough*.

Inline editing is where a user can change aspects of a page by clicking on the data to be changed, editing in a form object and sending the changed data back to the server. As I have been using Jquery for most of my UI needs, I looked for a solution that used it and found jeditable, a plugin that does exactly what I want it to do, but doesn’t seem to have been documented for django as yet, so here’s a first crack at making it work.

My user dashboard is pretty straighforward from a django perspective and uses the standard AUTH_PROFILE module. It may have to inherit some additional models shortly but in the spirit of keeping it simple, I use django.contrib.auth and a user profile model called UserProfile.

Jeditable is simple enough in a Jquery kind of way: enclose the data that you want to be edited in a div with a class identifier and specify a URL that will process the edited data in the editable function.The demo code is all PHP, but it’s simple enough to translate that to django:

$(document).ready(function() {
     $('.edit').editable('/users/dashboard/edit/{{ user.username }}/', {
     	style: 'display: inline'
     });
});

My URL pattern is fairly easy to work out from that:

(r'^dashboard/edit/(?P<username>\w+)/$', 'edit_dashboard'),

In order to make it generic as possible, there should be one function for the editable area and one django view. This is the science bit. Jeditable sends a POST request which by default is formed thusly:

id=elements_id&value=user_edited_content

This isn’t explained that well on the Jeditable page in my opinion, but the author is Finnish so we’ll let him off. What it means is that the name of the div class is sent a value pair with ‘id’ and the edited data is sent as ‘value’. This is modifiable but it’s as good a default as any to use.

In keeping with django’s MVC structure, set the div element name (‘id’) to be the same as the associated model field. There’s probably a more dynamic way of doing this, such as defining the div element as the field name from the dictionary created by the view, but creating it by hand will be fine. Each div element therefore looks something like this:

<div class="edit" id="first_name">{{ user_obj.user.first_name }}</div>

Pass the field and its value to the view using request.POST.get():

            field = request.POST.get('id', '')
            value = request.POST.get('value', '')

This is where I got a bit stuck for a while. I have the field and I have the value, how do I pass them to the object to be saved? The answer was found in this post by James Bennett. A model, or object derived from a model has an API that calls all sorts of useful information about your model, and, more to the point can be invoked to modify that information in a generic way. The _meta functions are part of django’s internals, and as a comment in the post points out, might be subject to change, but we don’t need to the _meta functions, as the API call that we’re interested in, __setattr__ is also exposed directly to the model or object, so the field and value can be passed to the database like this:

user_obj.__setattr__(field, value)

and can be saved with a simple save() method. Try it in a shell.

UPDATE: this doesn’t work with the User.first_name and User.last_name fields. They can be accessed using

user_obj.user.__setattr__(field, value)

and saved with user_obj.user.save(). The first_name and last_name field in auth seem to be largely deprecated and so might be better off in UserProfile: it certainly saves a couple of lines of code.

Jeditable expects a response, which a PHP author would roll themselves. Fortunately, django has HttpResponse. I return the value variable, which is displayed as the edited data. The stored data will not be shown until the page is fully reloaded.

This example is pretty primitive really, and there are things that seem to be able to be improved. The database query is a simple user.get_profile() call but this request gets made for every item that is changed, which seems inefficient. It seems there should be something a bit more asynchronous about it , possibly using JSON or XML and making a periodic save rather than a query for every item. A bit of processing power could be saved if we checked if the data had changed before saving it. However, at the moment I’m a programmer with a deadline 😉