Speeding Up Django Forms

Lets say you have a model with a foreign key to another model that has thousands of records and you want to use forms.ModelForm to make a form. What could go wrong? Well the default is Django is going to build a dropdown with all those foreign key records. Amongst other things, this will put a huge load on your db. Especially if this form is used a lot. I have found two ways to deal with this.

Method 1: Shadow the Field

Lets say the field is “employees”. Use the modelform exclude variable to exclude employees. Then add another field like this:

class MyForm(forms.ModelForm):
    my_employee = forms.CharField(required=False, max_length=100, label='Employee')

    class Meta:
        model = MyModel
        exclude = ('employee', )

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)

        if self.instance.id:
            self.fields['my_employee'].initial = self.instance.employee

    def clean_my_employee(self):
        """ I use jquery autocomplete so some cleaning is needed."""
        my_employee = self.cleaned_data['my_employee']
        # Clean it
        self.my_employee = x  #  some code to create a valid id for the foreign key
        return my_employee

    def save(self, commit=True):
        obj = super(MyForm, self).save(commit=False)
        obj.employee = self.my_employee
        return obj

I use jquery autocomplete for the field on the form. I cache the allowed values and pass them to javascript. Then I use additional javascript to require that the user select from the provided choices. Here is the javascript:

var limit_autocomplete= function (id_autocomplete, field_name, valid_choices){
 var ac_field = $('#'+id_autocomplete);
 var dialog_message = $('<div id="MyDialog"><p>You must select one of the available choices for ' + field_name + ' or clear this field.</p></div>');

 var OK_clicked = function(){
 $(this).dialog("close");
 ac_field.focus();
 ac_field.trigger('keydown');
 };

 var check_autocomplete = function(){
 var input_value = ac_field.val();
 var i = $.inArray(input_value, valid_choices);
 console.log(input_value + ' '+i);
 if (i === -1 && input_value.length>1){
 dialog_message.dialog({modal: true, buttons: [{text: "OK", click: OK_clicked}]});
 }
 };

 //ac_field.on("autocompletechange", check_autocomplete);
 ac_field.focusout(check_autocomplete);
};

I also use this javascript to prevent autocomplete from submitting the form when the enter key is pressed.

Method 2: Over-ride the Field

What if you still want a drop-down? For example, lets say that even though you have 5000 employees, you only want a small subset? If you just change the choices attribute on the field in __init__, it’s too late. Django has already hit your db and constructed the full drop down. I was at a loss at this point. Stackoverflow came to the rescue. Here is the solution:

class MyForm(forms.ModelForm):
    employees = forms.ModelChoiceField(required=False, queryset=Employees.objects.none())

    class Meta:
        model = Model_w_Employees

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)

        self.fields['employee'].queryset = my_make_queryset()

Note that you can still use the same field name as in the model.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s