0

I am using formset_factory to manage a couple identical forms on my page. In each form, there is a pair of chained dropdowns. DropdownA has an onchange event that request options for dropdownB (AJAX). This all works fine but when I go to submit my forms via a POST request, they all fail the forms.is_valid() check. Printing the errors of the submitted formset reveals why:

[{'DropdownB ': ['Select a valid choice. Like is not one of the available choices.']}, {'DropdownB ': ['Select a valid choice. < is not one of the available choices.']}]

There are two errors, one for each form. They are both complaining that the choice sent for DropdownB is not one of the available ('Like' and '<' respectfully).

Now, because I want to only populate DropdownB with certain choices based on what DropdownA selected, I purposefully defined DropdownB (a choicefield) with 0 choices.

DropdownB = ()
DropdownB = forms.ChoiceField(choices=op_choices, required=False)

How do I specify to the server what the valid choices are BASED ON what DropdownA's value is?

I tried to simplify this problem in the abstract above, but if you want the full code of the form, here you go:

class UnifiedSingleSearchBar(forms.Form):
# Dict to categorize field types
type_dict = {
    'DateField': 'Numeric',
    'DateTimeField': 'Numeric',
    'AutoField': 'Numeric',
    'CharField': 'String',
    'BooleanField': 'Bool',
}
operation_dict = {'Numeric':
                      (
                          ('>', '>'),
                          ('>=', '>='),
                          ('<', '<'),
                          ('<=', '<='),
                          ('=', '='),
                          ('>-<', 'Between'),
                          ('0', 'IS null'),
                          ('1', 'IS NOT null'),
                      ),
                    'String':
                        (
                            ('Like', 'Like'),
                            ('Is', 'Is')
                        ),
                    'Bool':
                        (
                            ('True', 'True'),
                            ('False', 'False')
                        )
                  }
searchabel_field_choices = ()
# To create the "field" dropdown, we loop through every field in the model and note its type.
for field in Mymodel._meta.fields:
    tuple = (
        (field.name, field.name),  # signifies a nested tuple
    )
    searchabel_field_choices = searchabel_field_choices + tuple
searchabel_field_choices = searchabel_field_choices + (('', '--------'),)
shared_attrs = {
    'autocomplete': 'off',
    'class': 'form-control datetimepicker-input',
}
searchable_field = forms.ChoiceField(choices=searchabel_field_choices, required=False)
op_choices = ()  # Should always start with an empty operations list since field has not yet been chosen
operation = forms.ChoiceField(choices=op_choices, required=False)

# 2 is usually only ever used if a range is being specified
# Numeric
date1 = forms.DateField(required=False, widget=DatePicker(attrs=shared_attrs))
date2 = forms.DateField(required=False, widget=DatePicker(attrs=shared_attrs))
datetime1 = forms.DateTimeField(required=False, widget=DateTimePicker(attrs=shared_attrs))
datetime2 = forms.DateTimeField(required=False, widget=DateTimePicker(attrs=shared_attrs))
integer = forms.IntegerField(required=False)
# Bool
bool = forms.BooleanField(required=False)
# String
string = forms.CharField(required=False)
Rosey
  • 739
  • 1
  • 12
  • 27
  • Are you aware that to make this really work you need to implement some javascript? Usually you change the contents of the dropdownB through a script that starts after changing dropdownA. Without javascripot you would need server interaction. And then you would need to change the dropdown in the forms init method. – ger.s.brett Aug 23 '18 at 16:16
  • What do you mean? I am changing dropdownB with Javascript. I thought mentioning my use of AJAX made that obvious but in case that means something different than what I though it did, yes I am using Javascript. My problem is exclusively server side though. I have no problem with the client experience. – Rosey Aug 23 '18 at 16:22
  • I overlooked first lines. You probably need to provide more of your code. Can you show your form? – ger.s.brett Aug 23 '18 at 16:29
  • I edited to include it in the question. – Rosey Aug 23 '18 at 16:59
  • Are you changing the op_choices in the ajax call? – ger.s.brett Aug 23 '18 at 18:33
  • No. The AJAX call just takes a string sent by the client, determines if that string is "valid" & if it is, returns an appropriate set of HTML – Rosey Aug 23 '18 at 18:51
  • How should the django from then know what is a valid entry? The op_choices will still be empty when it validates the entries. – ger.s.brett Aug 23 '18 at 22:17

1 Answers1

0

The answer Here fixed my problem. Each time you loop over a form in your formset, you read the value in DropdownA specified for that form in the request, and use that information to set the valid choices for DropdownB, again per that specific form.

EDIT: Adding extra info for those that still need clarification.

So after receiving the forms from the client, somewhere before you validate them with forms.is_valid() you would for loop over them, inspect the value specified for dropdownA and update the choices for dropdownB.

Most likely you are populating the dropdowns with values from your DB, so this usually makes these checks and updates easy. first you would see if the value specified by the client for dropdownA is a valid member of the database (gotta make sure no funny business went on) model1_record = model1.objects.get(<dropdownA val>) then assuming dropdownA is found in the DB, you are probably trying to limit the valid dropdownB choices to records associated to the one you identified already. You do this by updating dropdownB's queryset like so form.fields['dropdownB'].queryset = model2.objects.filter(model1FK=model1_record)

After doing that for all forms, now you can call forms.is_valid() and you shouldn't get that invalid choices error (well, if that ddB is indeed related to ddA).

Rosey
  • 739
  • 1
  • 12
  • 27