Django gotchas - part 1

During the last month I’ve been immersed in Django almost full time in my job (almost as I had a small glitch in which I had to develop a C# application for a PDA). During this last month I came across several, as I call them, Django gotchas and I will try to document them here.

I believe it is worth writing about them as:

  1. The writings will serve as a memory aid for me later on
  2. I’ve seen too many people asking the same question over and over in #django

Gotcha #1

In the project I worked on I came across the need to chain two ChoiceField(). That is, given a particular choice selected in a ChoiceField() the choices in a second ChoiceField() would change. This is the oh-so-ever-spoken-about chain of select widgets. Nothing fancy really yet tricky enough to warrant some self-promotion here.

However before we move on …

BIG FAT DISCLAIMER: I’m still very new to Django so if anything expressed below is blatantly wrong feel free to correct me and even sue me. Now that we got that out of the way let’s move on.

Here’s my take on this issue. Given two ChoiceFields() in which values selected in CF1 = forms.ChoiceField() will guide the values in CF2 = forms.ChoiceField(), how do we implement this?

Assume we have two choice fields. The first field can take a value in [(1, 'a'), (2, 'b'), (3, 'c')] and the second field will have its choices guided by the value selected in the first field. So, firstly we must have a form:

def MyForm(forms.Form):
    cf1 = forms.ChoiceField(choices = [(1, 'a'), (2, 'b'), (3, 'c')])
    cf2 = forms.ChoiceField()

And a view that processes it:

def myview(request):
    if request.POST:
        form = MyForm(request.POST)
 
        if form.is_valid():
             # Do your stuff here
             return HttpResponseRedirect('/some/url/')
    else:
        form = MyForm()
 
    return render_to_response('templates/myform.html', {'form': form})

This is all pretty standard, so how do we make the values in the second choice field depend on the value selected on the first choice field?

Dynamic loading of values - AJAX

Well, my take on it was to have some AJAX action in my templates. Moreover, I decided to use jQuery which really simplified my life. What a joy!

The jQuery code to achieve this is really simple:

    $(function(){
	$("select#id_cf1").change(function(){
		$.getJSON("/ajax_load/" + $(this).val(),{}, function(j){
			var options = '';
			for (var i = 0; i < j.values.length; i++)
				options += '' + j.values[i].display + '';
			$("select#id_cf1").html(options);
		})
	})

All right, maybe it doesn’t look that easy. Let’s look at the most important bits in a bit more of detail:

The two key points in the above function are .change() and $.getJSON().

  1. $(”select#id_cf1″).change(): from the jQuery documentation: Triggers the change event of each matched element. That is, every time the selection changes our function will be called.
  2. $.getJSON(): again from the jQuery documentation: Load JSON data using an HTTP GET request. The appropriate values for our second ChoiceField() will be loaded from the URL “/ajax_load/some_id” and will come in the JSON format.

    The view that corresponds to /ajax_load/some_id/ would look something like this:

    from django.utils import simplejson
     
    def get_2ndCF_values_json(request, cf1_id):
        results = {'success': False}
        try:
            cf1_choice_instance = Model.objects.get(id = cf1_id)
            cf2_objects = cf1_choice_instance.model2_set.order_by('field')
            results = {'success': True, 'values':[{'display': x.__unicode__(), 'value': x.pk} for x in cf2_objects ]}
     
        except Model.DoesNotExist:
            results = {'success': False, 'values':[{'display': 'Select a value in CF1', 'value': -1}]}
     
        return HttpResponse(simplejson.dumps(results), mimetype='application/json')

    When Django renders our template and we start playing about with our newly created chained ChoiceFields we are so overcome with joy that we want to cry. However such joy lasts not for long. Until we hit the submit button to be more precise.

    The dreaded form submission

    When we try to submit the form form.is_valid() will complain. With reason. The problem here is that we have manually tweaked the values of our second ChoiceField but only in the HTML! We need to reflect those changes in our form field as well.

    In the view that processes the form we usually find an if request.POST:, in its body is exactly where we have to do the appropriate tweaks.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    def myview(request):
        if request.POST:
            form = MyForm(request.POST)
     
            # here's where we do the extra magic
            cf1_object = Model.objects.get(id = request.POST['cf1'])
            form.fields['cf2'].choices = [(request.POST['cf2'], Model_2.objects.get(id = request.POST['cf2']).__unicode__()),] + [(c.id, c.__unicode__()) for c in cf1_object.cf2_set.exclude(id = request.POST['cf2'])]
            # now we're ready to see if it's all valid
     
            if form.is_valid():
                 # Do your stuff here
                 return HttpResponseRedirect('/some/url/')
        else:
            form = MyForm()
     
        return render_to_response('templates/myform.html', {'form': form})

    The only difference with our original view lies in lines 6 and 7. In line 6 we simply look up the object selected in the first ChoiceField, this is so that we can do a reverse lookup on it. In line 7 we set the choices of the second ChoiceField appropriately. The first tuple is the one selected by the user so we place it first in the list of choices. This is along the lines of what we did in our AJAX loader but in our form processing view. This step is necessary as the validation of the form will complain if we don’t do it that way.

    Wrap up

    This is a very hackish way of chaining two ChoiceFields and I’m sure that a more elegant way is possible. Hackish mainly because it involves loading data through AJAX into the HTML document and then manually patching the form in the view (yuck!) Still this has worked for me and I sure hope that it does for you too.

    If you think there’s a better way of dealing with this issue please let me know!

    Comments 4

    1. Claudiu wrote:

      Indeed, very useful information. Thanks!

      Posted 09 Oct 2008 at 6:10 am
    2. Alexandre wrote:

      Hi, i’m having problems with you example, nothing happens with the second choice field on the change event of the first one…

      Maybe i’m doing something wrong:

      1) i placed this inside urls.py
      (r’^myview/$’, ‘Representantes.dynamic_choice.views.myview’),
      (r’^ajax_load/(\d+)/$’, ‘Representantes.dynamic_choice.views.get_2ndCF_values_json’),

      2) my template “myform.html” looks like this:
      http://dpaste.com/hold/88658/

      Thanks

      Posted 04 Nov 2008 at 11:20 am
    3. Ulises wrote:

      @Alexandre

      Things I would do to debug the problem:
      1) use a browser and try /ajax_load/number_here/ and see that I actually get the json response back
      2) use firebug and look at the requests (if you’re not already doing that)
      3) check that field ids match those you use in your javascript code
      4) careful with character escaping (in your template you have a < instead of <) and finally
      5) go to #django in freenode and ask as people there are really friendly and helpful

      Hope that helps.

      Posted 04 Nov 2008 at 11:38 am
    4. Rok Jaklič wrote:

      Hi.

      I have came up to the similar problem and tried to solve it with like this in forms.py:

      def __init__( self, *args, **kwargs ):
      product_categories = None
      if ‘product_categories’ in kwargs:
      product_categories = kwargs.pop( ‘product_categories’ )
      super( NewProductForm, self ).__init__( *args, **kwargs )
      if product_categories:
      self.fields["product_category"].widget.choices = product_categories

      …where product_category is something like:
      product_category = forms.ChoiceField( choices = [], required = False, label = _( ‘Product category’ ), initial = ‘r’ )

      I almost gave up and was thinking of “manually” creating forms , … and I really do not understand why this “thingy” did not work when I POSTed data, … but luckily I have found your solution.

      Thank you.

      Posted 03 Jun 2009 at 8:49 am

    Trackbacks & Pingbacks 2

    1. From Old enough to know better » Dynamic select fields with JQuery and django on 19 May 2009 at 10:43 am

      [...] 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. [...]

    2. From Dynamic select fields with JQuery and django « Formerly Conversal on 01 Nov 2009 at 2:44 pm

      [...] 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. [...]

    Post a Comment

    Your email is never published nor shared. Required fields are marked *