Django ChoiceField, ModelChoiceField validation - django

I see that forms.ChoiceField is using this code to validate the value:
def validate(self, value):
"""
Validates that the input is in self.choices.
"""
super(ChoiceField, self).validate(value)
if value and not self.valid_value(value):
raise ValidationError(
self.error_messages['invalid_choice'],
code='invalid_choice',
params={'value': value},
)
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
text_value = force_text(value)
for k, v in self.choices:
if isinstance(v, (list, tuple)):
# This is an optgroup, so look inside the group for options
for k2, v2 in v:
if value == k2 or text_value == force_text(k2):
return True
else:
if value == k or text_value == force_text(k):
return True
return False
and forms.models.ModelChoiceField this code:
def validate(self, value):
return Field.validate(self, value)
Q1. Why Django uses validation to check if the selected value (from dropdown) is indeed in the choice list for forms.ChoiceField?
Q2. When Django uses the validation from Q1, to check if the value is indeed in the choice list, why does not also check if the selected value is in the model records for forms.models.ModelChoiceField?

The validation process starts from form.full_clean() where you have form._clean_fields() and form._clean_form executed in this order.
Now if you take a closer look at what form._clean_fields() do, you will probably notice that it only calls field.clean(value, initial) and collects the results into a cleaned_data dict. So the interesting part is at field.clean, lets see what happens there:
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
First, we have a to_python call, followed by validate and finishing with run_validators.
So in terms of ModelChoiceField when you reach the .validate method, your choice is already a Model instance, thats why, this kind of validation (from Q2) is happening inside the to_python method.
def to_python(self, value):
if value in self.empty_values:
return None
try:
key = self.to_field_name or 'pk'
value = self.queryset.get(**{key: value})
except (ValueError, TypeError, self.queryset.model.DoesNotExist):
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value

one thing i can say is for forms.ChoiceField the input are coming from the user perspective means a user can use inspect element and enter a choice which doesnt appear from the backend .
but for models one the choices are directly coming from the backend or the database

Related

How do you make a lowercase field in a django model?

With this method, I can make a field save as lowercase, but this does not
change the field in the existing model (that is in memory).
def get_prep_value(self, value):
value = super(LowercaseField, self).get_prep_value(value)
if value is not None:
value = value.lower()
return value
I'm having a hard time figuring out how to force this field to lowercase without overriding save and doing the change there. But that splits the logic for this lowercase field. I'd like all of it in the field. What do I override so that setting this value forces lowercase in memory AND on in the DB?
I don't want to change a form, I want all the lowercase logic contained inside the field class.
I've found a partial work around like so:
def pre_save(self, model_instance, add):
""" Returns field's value just before saving. """
attr = getattr(model_instance, self.attname)
if attr is not None:
attr = attr.lower()
setattr(model_instance, self.attname, attr)
return attr
def get_prep_value(self, value):
value = super(LowercaseField, self).get_prep_value(value)
if value is not None:
value = value.lower()
return value
It has a bit of a code smell, and does not handle checking the value before a save, but I don't see how to do that without overriding setattr on the actual model class and catching dealing with that inside the model class itself.
You can override the "save" method in Django by adding the following code in your models.py file
def save(self, *args, **kwargs):
self.yourfiled = self.yourfield.lower()
return super(ModelsName, self).save(*args, **kwargs)
Of course is possible to handle all params with a loop.
For all existing record you can create a Management command that can convert all strings to lowercase
here the docs:
Writing custom django-admin commands
If you don't want to change the Save method, just add to the form the "|lower" tag that will be convert all string to lowercase in UI
{{ value|lower }}

How to mark a field as validated (for accessing in validated_data) in validate(self)?

Let's say I have a field I want to validate/clean. I'd normally go about it like this for a field I know I will use.
def validate_number(self, value):
if value == 7:
raise serializers.ValidationError('7 is bad!')
if value == 5:
return None # I want 5 to come back as None
return value
Let's say I'm not sure which fields I have on validation and I go through these fields in validate(), How would I achieve this same behavior?
def validate(self, data):
if data['number'] == 7:
raise serializers.ValidationError('7 is bad!')
if data['number'] == 5:
data['number'] = None
return data
Would I be able to access this as validated_data in a .create/.update method?
You should raise a ValidationError with a dictionary:
raise serializers.ValidationError({'number': '7 is bad!'})
Then, the return value of validate will be passed as validated_data which you'll have access once is_valid() is True.

How to set Django forms with initial values from request.GET? (with mixed data types)

I have a django form that has both CharFields as well ChoiceFields. The form uses HTTP GET.
The issue I am having is that request.GET.dict() only contains one value for each key, regardless of if the data is a list or not. I discovered that I can use request.GET.getlist() to get the all the data but it will return a list even if the item is not a list. This is problematic because it causes the CharFields to have the value [u'']
What is the recommended way of solving the problem?
My current kludge is the following:
initial_dict = {k: v for k, v in request.GET.iterlists()}
clean_dict = {}
for key, value in initial_dict.iteritems():
if value[0] == '':
clean_dict[key] = ''
elif len(value) == 1:
clean_dict[key] = value[0]
else:
clean_dict[key] = value
SellerSearchForm(initial=clean_dict)
But I can't beleve that this is the best way to do this.
I myself had this issue as well as I wanted to be able to create links for (partial) prefilled forms. I ended up creating a Mixin class for my forms that overrides the get_initial_for_field() method (source).
class InitialFromGETMixin:
def __init__(self, *args, initial_from_get=False, **kwargs):
super(InitialFromGETMixin, self).__init__(*args, **kwargs)
self.initial_from_get = initial_from_get
def get_initial_for_field(self, field, field_name):
"""
Special implementation of initial gathering if initial values are given through request GET object
"""
if self.initial_from_get:
value = self.initial.get(field_name)
if value is not None:
return value[0] if len(value) == 1 else value
else:
return field.initial
return super(InitialFromGETMixin, self).get_initial_for_field(field, field_name)
This way at least the form is responsible for translating the initial keys to readable forms in a similar way it does with the data attribute. But other than that, there is no better alternative as the form class does not assume initial data to be supplied in the structure request.GET stores the information.

Django custom UsernameField form validation not working

So I created a custom form field to validate for duplicate usernames. I'm using Django + Mongoengine as my database. I have that plugged and working with the django authentication system so I'm assuming it can be accessed from forms.py? Maybe that assumption is incorrect. So I have the field
class UsernameField(CharField):
def to_python(self, value):
if not value:
return ""
return value
def validate(self, value):
super(CharField, self).validate(value)
try:
# If the user object exists it won't throw an Error/Exception
user=User.objects.get(username=value)
raise ValidationError("Username already exists")
except:
pass
But when I actually use it in my form, it always seems to validate correctly even though I've called checked if form.is_valid() is True
You're raising exceptions in the try block but then snuffing them out in the except block with pass. Try this, it will check for the existing user and only fails if it exists.
try:
# If the user object doesn't exist, it validates
user=User.objects.get(username=value)
except django.core.exceptions.DoesNotExist:
pass
else:
#user does exist, barf.
raise ValidationError("Username already exists")
Bah, it was a dumb mistake on my part. For some reason I forgot that the Error I was trying to raise would be caught by the try and I would get sent to the except route. Changing it to this works
class UsernameField(CharField):
def to_python(self, value):
if not value:
return ""
return value
def validate(self, value):
super(CharField, self).validate(value)
usernameDuplicate = False
try:
# If the user object exists it won't throw an Error/Exception
user=User.objects.get(username=value)
usernameDuplicate = True
except:
pass
if usernameDuplicate==True:
raise ValidationError("Username already exists")

Custom Form Field in Django

I am trying to create a custom form field in Django.
class CustomTypedMultipleChoiceField(MultipleChoiceField):
def __init__(self, *args, **kwargs):
self.coerce = kwargs.pop('coerce', lambda val: val)
self.empty_value = kwargs.pop('empty_value', [])
super(CustomTypedMultipleChoiceField, self).__init__(*args, **kwargs)
def to_python(self, value):
"""
Validates that the values are in self.choices and can be coerced to the
right type.
"""
value = super(CustomTypedMultipleChoiceField, self).to_python(value)
if value == self.empty_value or value in self.empty_values:
return self.empty_value
new_value = []
for choice in value:
try:
new_value.append(self.coerce(choice))
except (ValueError, TypeError, ValidationError):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice})
return new_value
def validate(self, value):
if value != self.empty_value:
super(CustomTypedMultipleChoiceField, self).validate(value)
elif self.required:
raise ValidationError(self.error_messages['required'])
I am getting the error CustomTypedMultipleChoiceField has no attribute empty_values. This is the exact same code that Django in built TypedMultipleChoiceField is built with. So I dont understand why I am getting this error.
I also thought of sub-classing the TypedMultipleChoiceField, but I wanted its error to be different in to_python method and didn't want to return the value thing, so opted for this method.
Please help me.
I don't know if it's a typo or you intended that way but actually empty_values (in plural) is not defined in your code anywhere. I also take a look at the source code of the super class MultipleChoiceField and is not defined there either.
What I could find in the super super class of your class (ChoiceField) was a reference to validator.EMPTY_VALUES and of course, it is in capital letters.
The line more alike yours in the source code was this one:
if value == self.empty_value or value in validators.EMPTY_VALUES:
Take a look deep in your code and see if that was what you intended to do.
Hope this helps!