Search code examples
djangodjango-formsform-fields

Validate a dynamic select field in Django


I'm using Django 1.4 with Python 2.7 on Ubuntu 12.10.

I have a form where I need to populate a few drop-downs dynamically (using jQuery) but need 2 of them to be required and the 3rd to be optional.

I'm using Tastypie to help with the API to get the options. Basically the first drop-down is populated with industry level codes for schools. Once a code is selected a category drop-down is populated for all categories for that code. Once the category is chosen a subcategory drop-down is populated for all subcategories for that combination of code and category.

I'm able to require the code drop-down (it's not dynamically populated). However, I'm having a tough time getting the category drop-down to be required. There are basically 2 routes I can take - front-end validation or back-end validation. I'm trying to go with back-end validation so I can easily create further validation if needed.

Here is the form:

class SchoolProductForm(forms.ModelForm):
    cip_category = forms.ChoiceField(required=True,
                                     choices=(('', '----------'),))

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

        self.fields['short_description'].widget = TA_WIDGET
        self.fields['salary_info'].widget = TA_WIDGET
        self.fields['job_opportunities'].widget = TA_WIDGET
        self.fields['related_careers'].widget = TA_WIDGET
        self.fields['meta_keywords'].widget = TI_WIDGET
        self.fields['meta_description'].widget = TI_WIDGET
        self.fields['cip'].queryset = models.CIP.objects.filter(
            parent_id__isnull=True)


    class Meta:
        model = models.SchoolProduct
        exclude = ('campus',)

I've tried to override the clean method. I've tried to create a field specific clean method. Neither seem to work.

Variations of the following:

def clean(self):
    super(SchoolProductForm, self).clean()
    if cip_category in self._errors:
        del self._errors['cip_category']
    if self.cleaned_data['cip_category'] == '----------':
        self._errors['cip_category'] = 'This field is required.'

    return self.cleaned_data

This gives an error that there is no cip_category in cleaned_data, which makes sense because it didn't validate.

I've tried variations with the field specific clean:

def clean_cip_category(self):
    data = self.cleaned_data['cip_category']
    self.fields['cip_category'].choices = data

    return data

But get a validation error on the page stating my choice is not one of the available choices.

I've tried to create a dynamic field type (several variations):

class DynamicChoiceField(forms.ChoiceField):
    def valid_value(self, value):
        return True

class SchoolProductForm(forms.ModelForm):
    cip_category = DynamicChoiceField(required=True,
                                      choices=(('', '----------'),))

But it accepts ---------- as a valid option (which I don't want) and causes an error since the ORM tries to match a value of ---------- in the database (which it won't find).

Any ideas?


Solution

  • I was able to solve this with a little overriding of a method in ChoiceField.

    I added the field to the form and handled the pre-population with the self.initial:

    class SchoolProductForm(forms.ModelForm):
        cip_category = common_forms.DynamicChoiceField(
            required=True, choices=(('', '----------'),))
    
        def __init__(self, *args, **kwargs):
            super(SchoolProductForm, self).__init__(*args, **kwargs)
    
            self.fields['short_description'].widget = TA_WIDGET
            self.fields['salary_info'].widget = TA_WIDGET
            self.fields['job_opportunities'].widget = TA_WIDGET
            self.fields['related_careers'].widget = TA_WIDGET
            self.fields['meta_keywords'].widget = TI_WIDGET
            self.fields['meta_description'].widget = TI_WIDGET
            self.fields['cip'].queryset = models.CIP.objects.filter(
                parent_id__isnull=True)
    
            # Get the top parent and pre-populate
            if 'cip' in self.initial:
                self.initial['cip'] = models.CIP.objects.get(
                    pk=self.initial['cip']).top_parent()
    
        class Meta:
            model = models.SchoolProduct
            exclude = ('campus',)
    

    Where DynamicChoiceField is:

    class DynamicChoiceField(forms.ChoiceField):
        def valid_value(self, value):
            return True
    

    Then, in the view I added handling in the form_valid override:

    def form_valid(self, form):
        self.object = form.save(commit=False)
    
        # Handle the CIP code
        self.object.cip_id = self.request.POST.get('cip_subcategory')
        if self.object.cip_id == '':
            self.object.cip_id = self.request.POST.get('cip_category')
    
        self.object.save()