Django form with choice field not providing None as a choice - django

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

Related

How do you validate a formset with a dynamically updating Choicefield?

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).

Validating a model field to be unique as lower case

I needed to validate a model field to be unique as lower case, but without actually saving the model field in lower case; e.g. if someone has already taken the username 'david', then the username 'David' would not be available. I tried various things, and eventually ended up doing the following:
def _perform_unique_checks(self, unique_checks):
errors = {}
for model_class, unique_check in unique_checks:
lookup_kwargs = {}
for field_name in unique_check:
f = self._meta.get_field(field_name)
lookup_value = getattr(self, f.attname)
if lookup_value is None:
continue
if f.primary_key and not self._state.adding:
continue
lookup_kwargs[str(field_name)] = lookup_value
if len(unique_check) != len(lookup_kwargs):
continue
if 'username' in lookup_kwargs:
lookup_kwargs['username'] = lookup_kwargs['username'].lower()
qs = model_class._default_manager.filter(**lookup_kwargs)
model_class_pk = self._get_pk_val(model_class._meta)
if not self._state.adding and model_class_pk is not None:
qs = qs.exclude(pk=model_class_pk)
if qs.exists():
if len(unique_check) == 1:
key = unique_check[0]
else:
key = NON_FIELD_ERRORS
errors.setdefault(key, []).append(
self.unique_error_message(model_class, unique_check))
... which works, but feels a little convoluted to me. I wondered whether any there's a more succinct way of achieving this?
Have you tried filtering on field_name__iexact, to do a case insensitive match?
The iregex, icontains and iexact filters should be able to do what you need.
First off, you can user the clean for this functionality (triggered when trying to validate a form):
def clean__field_name(self):
Also, you can add a unique=True constraint to the field in question. Then do a Try/Except during save to catch any IntegrityError which will tell you that the field is not unique. The database and model should do this work for you instead of trying to code around it.
Also see Case insensitive unique model fields in Django?
With more detail (database type, copy of model, are you using a Form?), I can expand on this answer.

SimpleListFIlter Default

I have a boolean field on my model that represents whether someone has canceled their membership or not. I am trying to create a custom SimpleListFilter that allows this field to be filtered on.
However, I really want to show only those who are not canceled by default. Is there someway to select the "No" option by default? This is my filter so far:
class CanceledFilter(SimpleListFilter):
title = 'Canceled'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'canceled'
def lookups(self, request, model_admin):
return (
(True, 'Yes'),
(False, 'No'),
)
def queryset(self, request, queryset):
if self.value() is True or self.value() is None:
return queryset.filter(canceled=True)
if self.value() is False:
return queryset.filter(canceled=False)
EDIT:
I should have been a bit clearer. I am specifically trying to do this in the Admin interface. When I add the above filter as a list_filter in admin. I get a filter on the side of the admin page with 3 choices: All, Yes and No.
I would like the "No" choice or none of the choices to be set by default. Instead the "All" choice is always set by default. Is there some none hacky way to set the default filter choice or something like that.
Basiclly in Admin when they view the Members, I only want to show the active (not canceled) by default. If they click "All" or "Yes" then I want to show the canceled ones.
Update:
Note this is the same as question Default filter in Django admin, but I that question is now 6 years old. The accepted answer is marked as requiring Django 1.4. I am not sure if that answer will still work with newer Django versions or is still the best answer.
Given the age of the answers on the other question, I am not sure how we should proceed. I don't think there is any way to merge the two.
Had to do the same and stumbled upon your question. This is how I fixed it in my code (adapted to your example):
class CanceledFilter(SimpleListFilter):
title = 'Canceled'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'canceled'
def lookups(self, request, model_admin):
return (
(2, 'All'),
(1, 'Yes'),
(0, 'No'),
)
def queryset(self, request, queryset):
if self.value() is None:
self.used_parameters[self.parameter_name] = 0
else:
self.used_parameters[self.parameter_name] = int(self.value())
if self.value() == 2:
return queryset
return queryset.filter(cancelled=self.value())
Some explanation is required. The querystring is just part of the URL, and exactly what the name implies: a query string. Your values come in as strings, not as booleans or integers. So when you call self.value(), it returns a string.
If you examine the URL you get when you click on the Yes/No, when not using a custom list filter, you'll see it encodes it as 1/0, not True/False. I went with the same scheme.
For completeness and our future readers, I also added 2 for All. Without verifying, I assume that was None before. But None is also used when nothing is selected, which defaults to All. Except, in our case it needs to default to False, so I had to pick a different value. If you don't need the All option, just remove the final if-block in the queryset method, and the first tuple in the lookups method.
With that out of the way, how does it work? The trick is in realising that self.value() just returns:
self.used_parameters.get(self.parameter_name, None)
which is either a string, or None, depending on whether the key is found in the dictionary or not. So that's the central idea: we make sure it contains integers and not strings, so that self.value() can be used in the call to queryset.filter(). Special treatment for the value for All, which is 2: in this case, just return queryset rather than a filtered queryset. Another special value is None, which means there is no key parameter_name in the dictionary. In that case, we create one with value 0, so that False becomes the default value.
Note: your logic was incorrect there; you want the non-cancelled by default, but you treat None the same as True. My version corrects this.
ps: yes, you could check for 'True' and 'False' rather than True and False in your querystring method, but then you'd notice the correct selection would not be highlighted because the first elements in your tuple don't match up (you're comparing strings to booleans then). I tried making the first elements in the tuples strings too, but then I'd have to do string comparison or eval to match up 'True' to True, which is kind of ugly/unsafe. So best stick to integers, like in my example.
If anyone is still interested in a solution for this, I used a different and IMHO much cleaner approach. As I'm fine with a default choice and the handling of it, I decided I just want to rename the default display label. This is IMHO much cleaner and you don't need any "hacks" to handle the default value.
class CompleteFilter(admin.SimpleListFilter):
'''
Model admin filter to filter orders for their completion state.
'''
title = _('completion')
parameter_name = 'complete'
def choices(self, changelist):
'''
Return the available choices, while setting a new default.
:return: Available choices
:rtype: list
'''
choices = list(super().choices(changelist))
choices[0]['display'] = _('Only complete')
return choices
def lookups(self, request, model_admin):
'''
Return the optionally available lookup items.
:param django.http.HttpRequest request: The Django request instance
:param django.contrib.admin.ModelAdmin model_admin: The model admin instance
:return: Optional lookup states
:rtype: tuple
'''
return (
('incomplete', _('Only incomplete')),
('all', _('All')),
)
def queryset(self, request, queryset):
'''
Filter the retreived queryset.
:param django.http.HttpRequest request: The Django request instance
:param django.db.models.query.QuerySet: The Django database query set
:return: The filtered queryset
:rtype: django.db.models.query.QuerySet
'''
value = self.value()
if value is None:
return queryset.filter(state__complete=True)
elif value == 'incomplete':
return queryset.filter(state__complete=False)
return queryset
In the choices() method, I just rename the display label from All to Only complete. Thus, the new default (which has a value of None is now renamed).
Then I've added all additional lookups as usual in the lookups() method. Because I still want an All choice, I add it again. However, you can also skip that part if you don't need it.
That's basically it! However, if you want to display the All choice on top again, you might want to reorder the choices list in the choices() method before returning it. For example:
# Reorder choices so that our custom "All" choice is on top again.
return [choices[2], choices[0], choices[1]]
Look at the section called "Adding Extra Manager Methods" in the link below:
http://www.djangobook.com/en/2.0/chapter10.html
You can add an additional models.Manager to your model to only return people that have not cancelled their membership. A rough implementation of the additional models.Manager would look like this:
class MemberManager(models.Manager):
def get_query_set(self):
return super(MemberManager, self).get_query_set().filter(membership=True)
class Customer(models.Model):
# fields in your model
membership = BooleanField() # here you can set to default=True or default=False for when they sign up inside the brackets
objects = models.Manager # default Manager
members = MemberManager() # Manager to filter for members only
Anytime you need to get a list of you current members only, you would then just call:
Customer.members.all()

How to validate number in django

I am learning Django,looked into django validation but the below type i want.searched in google no result.
In my app,their are two character fields,i want it to be validate so that the conditons are,
1.Either any one of the field is entered.
2.It should validate the entered data are integer.
that means,both fields are not mandatory,but any one is mandatory and that mandatory field should accept number only.
How to do it in django.
class MyForm(forms.Form):
field_one = forms.IntegerField(required=False)
field_two = forms.IntegerField(required=False)
def clean(self):
cleaned_data = self.cleaned_data
field_one = cleaned_data.get('field_one')
field_two = cleaned_data.get('field_two')
if not any([field_one, field_two]):
raise forms.ValidationError(u'Please enter a value')
return cleaned_data
Using an IntegerField will validate that only numeric characters are
present, covering your blank space use case.
Specifying required=False on both fields allows either field to be left blank.
Implementing clean() on the form gets you access to both fields.
.get() will return None if the key isn't found, so the use of
any([field_one, field_two]) will return true if at least one of the
values in the list isn't None. If neither value is found, the
ValidationError will be raised.
Hope that helps you out.

Django get_FIELD_display

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.