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!
Related
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
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.
I have following simple form field
class PhoneField(CharField):
widget = PhoneWidget
def clean(self, value):
value = re.sub('\D', '', super(PhoneField, self).clean(value))
if len(value) < 7:
raise ValidationError(_("Phone number is too short"), code='too_short')
return value
However, widget is still text input.
However, if I write
def __init__(self, *args, **kwargs):
kwargs['widget'] = PhoneWidget
super(PhoneField, self).__init__(*args, **kwargs)
Then it works perfectly. Digging in the code I noticed, that if widget is not specified in kwargs, then self.widget is used, but it is not a case. Why is that?
Note, that I don't pass widget in code.
field = PhoneField(label='Phone')
Found the problem.
The problem was in admin. Admin passed in kwargs default widget for CharField - widgets.AdminTextInputWidget.
I got form:
class SearchForm(Form):
owner = ModelMultipleChoiceField(queryset=User.objects.all(), required=False)
and after customizing get_queryset() of related view it works as expected but I got objects without owner. I want to add additional new choice on top of the list (0,'Without owner') so I could then filter only objects without owner.
How to add this option?
UPDATE:
I add the choice in form.__init__ and wrote custom clean method for it but if I choose added option something raises ValidationError before getting to my clean method.I'm guessing I have to override form.is_valid but I'm not sure how to do it so I can still use default is_valid method.
My code
def __init__(self, *args, **kwargs):
super(ClientListSearchForm, self).__init__(*args, **kwargs)
self.fields['owner'].choices = \
list(self.fields['owner'].choices)+[('0', 'n/a')]
def clean_owner(self):
logger.debug('CLEAN_OWNER:')
data = self.cleaned_data.get('owner')
logger.debug('data: %s' % data)
if data == 0:
logger.debug('Data zero - not assigned')
return data
users = User.objects.all()
if all(e in users for e in data):
logger.debug('Data in users - validating ok')
return data
else:
raise ValidationError('Incorrect owner')
I tried:
def is_valid(self):
try:
super(ClientListSearchForm, self).is_valid()
except ValidationError as e:
logger.debug('val error: %s' % e.args)
but it's nor validating nor caching exception
UPDATE2 Added custom validator
def userWithEmpty(value):
users = User.objects.values_list('pk').all()
v =list()
for va in value:
v.append(int(va))
u = list()
for us in users:
u.append(int(us[0]))
if not (all(e in u for e in v)or v ==0):
raise ValidationError('Invalid Value: %s' % value)
Is there a better way to convert every value in iterable than my for loops?
Didn’t post it as answer because there is a lot of place for improvement. Waiting for rants about what I'm doing wrong- and I will appreciate it all...
It STOPPED WORKING eee - there is something before validator from validators=[]
I ended up with other approach
class ModelMultipleChoiceWithEmptyField(ModelMultipleChoiceField):
def __init__(self, *args, **kwargs):
super(ModelMultipleChoiceWithEmptyField, self).__init__(*args, **kwargs)
self.choices = list(self.choices) + [('0', 'Brak')]
def clean(self, value):
if self.required and not value:
raise ValidationError(self.error_messages['required'], code='required')
if value == [u'0']:
return value
return super(ModelMultipleChoiceWithEmptyField,self).clean(value)
It's much cleaner and it works. Fell free to reuse and improve
I am writing an Edit form, where some fields already contain data. Example:
class EditForm(forms.Form):
name = forms.CharField(label='Name',
widget=forms.TextInput(),
initial=Client.objects.get(pk=??????)) #how to get the id?
What I did for another form was the following (which does not work for the case of the previous EditForm):
class AddressForm(forms.Form):
address = forms.CharField(...)
def set_id(self, c_id):
self.c_id = c_id
def clean_address(self):
# i am able to use self.c_id here
views.py
form = AddressForm()
form.set_id(request.user.get_profile().id) # which works in the case of AddressForm
So what is the best way to pass an id or a value to the form, and that could be used in all forms for that session/user?
Second: is it right to use initial to fill in the form field the way I am trying to do it?
You need to override the __init__ method for your form, like so:
def __init__(self, *args, **kwargs):
try:
profile = kwargs.pop('profile')
except KeyError:
super(SelectForm, self).__init__(*args, **kwargs)
return
super(SelectForm, self).__init__(*args, **kwargs)
self.fields['people'].queryset = profile.people().order_by('name')
and, obviously, build your form passing the right parameter when needed :)