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)
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).
Related
Goal was to implement a simple View for the Users to Select columns dynamically with some added calculated info (annotations on some specific columns) and also let them filter on fields.
Thankful for any comments, since this took me quite a few hours to get it working properly I thought I would provide a short writeup for anyone looking at a similar problem :)
Used Modules/Libraries etc:
Django-Filter
Django_Tables2
Bootstrap-Select to properly display Multiple Choice Fields
Example Model which we would like to use:
class Summary(models.Model):
billing_date = models.DateField(verbose_name='Billing Date')
period = models.CharField(max_length=10, verbose_name='Period')
operator = models.CharField(max_length=50, verbose_name='Operator')
product = models.CharField(max_length=30, verbose_name='Product')
...
The filters are really straightforward, the only special case here is that the we want an empty queryset initially and some fields should be required.
"info" will hold the select columns of our "Summary" Model, "product" and "operator" are just fields in Summary.
class AdHocReportFilter(django_filters.FilterSet):
info = django_filters.MultipleChoiceFilter(choices=report_field_choices, label='Available Fields', required=True)
product = django_filters.ModelChoiceFilter(queryset=Product.objects.all(), label='Products', required=True)
operator = django_filters.CharFilter(field_name="operator", lookup_expr='contains', label='Operator')
....
def __init__(self, *args, **kwargs):
super(AdHocReportFilter, self).__init__(*args, **kwargs)
if self.data == {}:
self.queryset = self.queryset.none()
Template:
Nothing interesting to show here, you can use Bootstrap-Select to tidy up your Multi Select Fields (there are quite a few nice writeups about that available).
Make sure to put your Table into an "if" as the object may or may not exist depending on your view (if someone wants an example of the template let me know)
View:
Extract your GET requests accordingly (either as list or simple value depending on your available filters).
The actual filter itself depends on what you want the user to be able to filter, make sure to either make all necessary fields required or replace them with some standard value as the filter will not accept None Types.
"field__contains" is your friend here since it will also show values on not selected fields!
Special Case, if there can actually be "Null" Values in the DB for specific fields, move them to another filter likethe below example of the Q - query!
Fortunately "values" accepts "*list" which is a simple list of all our available columns.
The annotations are just dependant on what you want to achieve.
Call the Table Object with the added argument "user_columns" which holds our list so we can build the required Table.
#login_required
def ad_hoc_report(request):
template_name = 'non_voice/ad_hoc_report.html'
filter = AdHocReportFilter(request.GET, queryset=Summary.objects.all())
info = request.GET.getlist('info', None)
product = request.GET.get('product', '')
operator = request.GET.get('operator', '')
start_date = request.GET.get('start_date', None)
end_date = request.GET.get('end_date', None)
if operator is None:
operator = ''
result_object = Summary.objects.filter(product__contains=product, ).filter((Q(operator__contains=operator)|Q(operator__isnull=True)).values(*info).annotate(
Amount=Sum("amount"), Count=Sum("count"))
table = AdHocReportTable(data=result_object, user_columns=info)
return render(request, template_name, {'filter': filter, 'table': table})
Table:
This was the difficult part and only possible with lots and lots of reading various stack overflow comments :)
First of all define your calculated annotation columns and set the required Meta info, the '...' is a built in placeholder without knowing the column name in advance (which helps us to move our calculated columns to the end of the Table)
In the init we first check if our "self.base_columns" are consistent with what we provided and remove columns which were deselected by our user, otherwise it would still show them empty even after filtering. (Maybe there is a nicer way to do this, haven't found it yet)
In the next step add the columns selected by our user dynamically from the mentioned above "user_columns" which we passed in the views.py
class AdHocReportTable(tables.Table):
Amount = tables.Column(verbose_name='Amount')
Count = tables.Column(verbose_name='Count')
class Meta:
# '...' is a built in placeholder!
sequence = ('...', 'Amount', 'Count')
template_name = "django_tables2/bootstrap4.html"
attrs = {'class': 'table table-hover', }
# This makes it possible to pass a dynamic list of columns to the Table Object
def __init__(self, data, user_columns, *args, **kwargs):
if user_columns:
calulated_columns = ['Amount', 'Count']
# Removes deselected columns from the table (otherwise they are shown empty)
for key, val in self.base_columns.items():
if key not in user_columns and key not in calulated_columns:
del self.base_columns[key]
# Add the Selected Columns dynamically to the Table
for col in user_columns:
self.base_columns[col] = tables.Column(verbose_name=col)
super(AdHocReportTable, self).__init__(data, user_columns, *args, **kwargs)
I have a choice field in a form defined as
REFERRAL_CHOICES = (
(None, 'Please choose'),
('search', 'From a search engine'),
('social', 'From a social network'),
)
referral_source = forms.ChoiceField(
choices=REFERRAL_CHOICES
)
I also have a clean_company_size function which checks if the field is set to a good value:
def clean_company_size(self):
company_size = self.cleaned_data.get('company_size', None)
if company_size is None:
raise ValidationError('Please select a company size')
return company_size
If I add a or company_size == 'None' condition to the above None check, all works well. However, I am curious why the None value is being cast to a string. What is the best way of accomplishing a default prompts in a choice field and having that field be required?
All POST and GET variables, as well as Select HTML tag values are initially sent as strings. Django converts them to -say- int when it is deductable from the model. In your case it is not possible to distinguish a "None" string from a "None" object, they are both possible values. You may prefer using "" instead of None in your REFERRAL_CHOICHES
I am trying to access data.get_age_display in my email template. I can't seem to get the display of this. I am not sure what I am doing wrong, I've using get_FIELD_display numerous times before but passed as context to a normal template. Is there something different with forms?
class RequestForm(forms.Form):
ADULT = 1
SENIOR = 2
STUDENT = 3
AGE_GROUP = (
(ADULT, 'Adult'),
(SENIOR, 'Senior'),
(STUDENT, 'Student'),
)
name = forms.CharField(max_length=255)
phone = forms.CharField(max_length=15)
age = forms.ChoiceField(choices=AGE_GROUP)
details = forms.CharField(widget=forms.Textarea())
def save(self):
order = Order(
name = self.cleaned_data['name'],
phone = self.cleaned_data['phone'],
age = self.cleaned_data['age'],
details = self.cleaned_data['details'],
)
order.save()
template = loader.get_template('request_email.txt')
# send over the order object in an email extracted so they can handle the ticket order
context = Context({
'data': order,
})
#import pdb; pdb.set_trace()
email_subject = 'Request Tickets'
mail_managers(email_subject, template.render(context))
in my request_email.txt all I am doing is {{ data.get_age_display }} any ideas?
Jeff
You haven't shown the code for the Order model that you're creating. Are you sure that the age field on the model has choices set?
Any reason you're not using a ModelForm? You're creating an Order object within the form's save() method, but not returning it. A modelform would do that for you, as well as removing the need to redeclare the fields for the form.
I know this is coming WAAAAAY later than the question being posted but here's my answer for completeness and anyone else who might benefit from it :-)
I'm going to assume that in AGE_GROUP, ADULT, SENIOR and STUDENT are integers. Your form cleaning will NOT automatically clean the string contained in the POST and return an integer. So in this code:
context = Context({
'data': order,
})
you would think order.age is referring to an integer but that is, in fact, incorrect. It's burned me a few times before because this will correctly save the integer to the physical table, but the order instance still has the string representation of the age field.
You could do one of two things:
1. Clean this in the field:
clean_age(self):
return int(self.cleaned_data['age'])
or create a new field type:
def MyChoiceField(forms.ChoiceField):
def clean(self, value):
if not value:
if self.required:
raise forms.ValidationError(self.error_messages['required'])
return None
else:
return None
return int(value)
link that to the form field:
age = MyChoiceField(choices=AGE_GROUP)
and then you'll be able to apply this logic to any other such choice field in future. Personally, I find the latter approach the best one and I stick all my custom field types into a form_utils file so that I can use them everywhere. Another gotcha is that forms.charField doesn't automatically strip the entered text and you can use this approach to fix that too.
I have a form which has two fields for Integers:
class DemoForm(forms.Form):
b_one = forms.IntegerField(
error_messages={
'required':'Please enter a valid number.'
},
label = 'NumberOne',
required = True,
help_text = 'e.g. 266492'
)
b_two = forms.IntegerField(
error_messages={
'required':'Please enter a valid number.'
},
label = 'NumberTwo',
required = True,
help_text = 'e.g. 262865',
)
and I am validating these fields as
def clean_b_one(self):
self.validate_form(self.cleaned_data['b_one'])
def clean_b_two(self):
self.validate_form(self.cleaned_data['b_two'])
Now what I want to do is in validate_form I check, if these numbers exists in database, else raising forms.ValidationError
But what I also want to do some other validations when these form fields are valid, basically some check on the form based on input and raise some custom errors, where can I add logic? or what is the best way of doing it?
You can do individual field verifying in clean_b_one like you have, and raise ValidationErrors if something doesn't fit. Or override the clean method to do cross-field checking. General documentation to be found here.
I'm using a ChoicesField in my form, but I want to put a divider in it like this:
COUNTRIES = (
('CA', _('Canada')),
('US', _('United States')),
(None, _('---')), # <----------
('AF', _('Afghanistan')),
('AX', _('Aland Islands')),
('AL', _('Albania')),
('DZ', _('Algeria')),
('AS', _('American Samoa')),
# ...
class AddressForm(forms.Form):
country = forms.ChoiceField(choices=COUNTRIES, initial='CA')
What's the easiest way to make that unselectable, or at least give an error if the user picks it?
You can use this: https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices
You would format it like this:
COUNTRIES = [
("CA", _("Canada")),
("US", _("United States")),
# This format will indent the nested tuples over and the
# "-----" will be un-selectable
#
("---------------", ( # This will be a header for the items nested below
("AF", _("Afghanistan")),
('AX', _('Aland Islands')),
('AL', _('Albania')),
('DZ', _('Algeria')),
('AS', _('American Samoa')),
)
),
]
You could write a clean method to raise a validation error if any divider is selected.
class AddressForm(forms.Form):
country = forms.ChoiceField(choices=COUNTRIES, initial='CA')
def clean_country(self):
data = self.cleaned_data["country"]
if not data:
raise forms.ValidationError("You must select a valid country.")
return data
I don't know what version of Django you are using, but I am on 1.10.1 and I used the following:
ICONS = (
(None, ''),
(None, '==Extra Contact Info=='),
('phone', 'Phone'),
('phone', 'Phone (square)'),
('fax', 'Fax'),
('envelope', 'E-mail (black)'),
('envelope-o', 'E-mail (white/clear)'),
(None, ''),
(None, '==Social Media=='),
('facebook', 'Facebook'),
('facebook-official', 'Facebook (official)'),
('facebook-square', 'Facebook (square)'),
('google-plus', 'Google Plus'),
...
)
and that's all I used, and if a user selects any list item in a drop-down menu that has a 'None' value in it, it will bark at the user saying "This field is required."
Now... of course in my project, the choices list is being used in my whatever.com/admin page, but that may not be relevant. What is relevant however, is that you have to ensure your model (or form) class field does not contain "blank = True". By default it should be false if you omit it, in other words the field won't accept null or empty string values. That should be all you need...