I have read the docs and I can only find guidelines on how to write a clean method for a field within a form: https://docs.djangoproject.com/en/3.1/ref/forms/validation/#cleaning-a-specific-field-attribute
However I have created a field which inherits from ModelChoiceField. I wish to add some custom validation and cleaning, attached to the field and not the form, because the field is used in multiple forms, hence keeping it DRY.
I can take a stab at creating a clean method, but eactly what args are passed in, and what should be returned seems to be lacking in the documentation, or I can't find it.
Here my field that I wish to add custom cleaning and validation to:
class FooChoiceField(forms.ModelChoiceField):
def __init__(self, required=True):
queryset = Foo.objects.filter(enabled=True).order_by('name')
super().__init__(
widget=forms.RadioSelect,
queryset=queryset,
to_field_name='id', # The radio button value field
required=required,
empty_label=None,
)
self.error_messages = {
'required': "Please select a Foo.",
'invalid_choice': "Invalid Foo selected, please try again.",
}
# Pass the whole DB object into the template so one can access all fields
def label_from_instance(self, obj):
return obj
Heres a guess at it, although it is called, the cleaned field always ends up as None, even when its valid:
class FooChoiceField(forms.ModelChoiceField):
...
def clean(self, value):
if value != 'correct':
raise ValidationError("Value is challenged in it's correctness")
return value
def validate(self, obj):
if obj.foo != 'foo':
raise ValidationError("Validation Error on foo")
If this is a model field, and the validation is re-used, you should move the validation on the model itself
def validate_correct(value):
if value != 'correct':
raise ValidationError("!", code='incorrect')
class MyModel(models.Model):
my_field = models.CharField(
max_length=31,
validators=[validate_correct],
)
If you want to keep your new form field, you should add some validators too
class MyModelChoiceFields(forms.ModelChoiceField):
default_validators = [validate_correct]
default_error_messages = {'incorrect': "This is not correct"}
Related
Let's say I have the following serializers:
class AnswerSerializer(ModelSerializer):
answer_text=CharField()
def validate_answer_text(self, value):
...
return value
def validate(self, value):
...
return value
class QuestionSerializer(ModelSerializer):
question_text=CharField()
answer=AnswerSerializer(many=True, read_only=False)
def validate_question_text(self, value):
...
return value
def validate(self, value):
...
return value
If validate_answer_text or validate in AnswerSerializer or validate_question_text in QuestionSerializer raise a ValidationError, the validate of QuestionSerializer won't be run. Thus, I cannot explain all the problem of the POST data.
Is there a way to run the validate function of a serializer even if one of it's field validator or children serializer validation failed and then combine all the errors ?
I have tried the following but did not succeed make it work properly. It does run both validate function and other validators but you cannot nest AllErrorSerializer and more importantly, it does not work when you do not have error: you can't save instance because you have inspect serializer.data.
EDIT
Due to Willem Van Onsem answer, I ended up with the following solution:
#serializers.py
class AnswerSerializer(ModelSerializer):
answer_text=CharField()
class Meta:
model=Answer
...
def validate_answer_text(self, value):
...
return value
def validate(self, value):
...
return value
class QuestionSerializer(ModelSerializer):
question_text=CharField()
answer=AnswerSerializer(many=True, read_only=False)
class Meta:
model=Question
...
def validate_question_text(self, value):
...
return value
class BasicAnswerSerializer(ModelSerializer):
answer_text=CharField()
class Meta:
model=Answer
...
class BusinessRuleValidator(ModelSerializer):
question_text=CharField()
answer=BasicAnswerSerializer(many=True, read_only=False)
class Meta:
model=Question
...
def validate(self, value):
...
return value
#views.py
class QuestionViewSet(ModelViewSet):
...
def create(self, request):
validator = BusinessRuleValidator(data=request.data)
validator.is_valid()
serializer = QuestionSerializer(data=request.data)
serializer.is_valid()
if (len(validator.errors) or len(serializer.errors)):
return Response(merge(validators.errors, serializer.errors), status=404)
serializer.create()
return Response('created', status=201)
It makes no sense to run validate when of of the fields is invalid. Django will first validate the individual fields, and then construct a dictionary that contains the validated data and thus run the .validate(…) method with that validated data.
But since the data of (at least) one of the fields is invalid, thus thus means that we can not construct such dictionary if valid data, and therefore the precondition of the .validate(…) method no longer holds. In order to fix this, first these fields should be available.
For example your serializer might have a boolean field. If a value tralse is for example passed to that field, and the field requires to be true or false, then what value should be passed for that field? A random boolean, the string tralse?
Another field validator can simply require that the field is part of the request. This thus means that if that field validator fails, there is simply no value for that field. So the only sensical thing to do might be to omit it from the validated_data dictionary, but the validate method takes as precondition that all required fields are in the validated_data dictionary. It thus again makes no sense to run validate on that data.
Im trying to validate in a django form if the user entered a numeric value on a field called "usd_value" using the clean method like this :
Form.py
class CostItemsForm(ModelForm):
def __init__(self, *args, **kwargs):
super(CostItemsForm, self).__init__(*args, **kwargs)
class Meta:
model = CostItems
fields = [
'group',
'description',
'usd_value',
'rer',
'pesos_value',
'supplier',
'position',
'observations',
'validity_date',
]
def clean_usd_value(self):
if self.cleaned_data.get('usd_value'):
try:
return int(self.cleaned_data['usd_value'].strip())
except ValueError:
raise ValidationError("usd_value must be numeric")
return 0
But is not working, i mean, if i leave the field empty or enter a text value there, alert doesn't activate at all and i got error (obviously) if i try to save the form. Any help ??
Here's my views.py
class CostItemInsert(View):
template_name='cost_control_app/home.html'
def post(self, request, *args, **kwargs):
if request.user.has_perm('cost_control_app.add_costitems'):
form_insert = CostItemsForm(request.POST)
if form_insert.is_valid():
form_save = form_insert.save(commit = False)
form_save.save(force_insert = True)
messages.success(request, "cost item created")
#return HttpResponseRedirect(reverse('cost_control_app:cost_item'))
else:
messages.error(request, "couldn't save the record")
return render(request, self.template_name,{
"form_cost_item":form_insert,
})
else:
messages.error(request, "you have no perrmissions to this action")
form_cost_item = CostItemsForm()
return render(request, self.template_name,{
"form_cost_item":form_cost_item,
})
I think your function name is wrong. Your field name is usd_value but your function is clean_usd. Change it to clean_usd_value and it should work.
Check Django doc section The clean_<fieldname>().
Edit
Also your return value for your clean method is wrong. Check the django doc example, you need to return the cleaned_data not 0:
def clean_usd_value(self):
cleaned_data = self.cleaned_data.get('usd_value'):
try:
int(cleaned_data)
except ValueError:
raise ValidationError("usd_value must be numeric")
return cleaned_data
But on a second throught, you might not even need the clean_usd_value method at all, django form field should have the default validation for you. Remove entirely the clean_usd_value method and see if it works.
I don't think you need custom validation for this. In fact, I think the builtin validation for django.forms.FloatField is going to be better than what you have.
Based on your error, I'm assuming that the form isn't using a FloatField for usd_value, and that's a bit odd. Make sure that your CostItems model has usd_value defined as a django.db.models.FloatField like below.
from django.db import models
class CostItems(models.Model):
usd_value = models.FloatField()
# other stuff
Once you do this, your CostItemsForm should automatically use django.forms.FloatField for usd_value. If it doesn't, you can always define this field explicitly.
from django import forms
class CostItemsForm(ModelForm):
usd_value = forms.FloatField(required=True)
class Meta:
model = CostItems
fields = [
'group',
'description',
'usd_value',
'rer',
'pesos_value',
'supplier',
'position',
'observations',
'validity_date',
]
If neither of these suggestions is helpful, please post your CostItems model.
I've created a form which is a forms.ModelForm. On the "view" side, I've created a view which is a generic.UpdateView.
In those 2 differents classes, I have is_valid() on one side, and form_valid() on the other side.
class ProfileForm(FormForceLocalizedDateFields):
class Meta:
model = Personne
fields = ('sexe', 'statut', 'est_fumeur',
'est_physique', 'date_naissance')
exclude = ('user', 'est_physique')
# blabla fields declaration
def is_valid(self):
pass
and edit view:
class EditView(LoginRequiredMixin, generic.UpdateView):
model = Personne
template_name = 'my_home/profile/edit.html'
form_class = ProfileForm
success_url = reverse_lazy('my_home_index')
# blabla get_initial() and get_object() and get_context_data()
def form_valid(self, form):
# username = form.cleaned_data['username']
# Hack: redirect on same URL:
# - if user refreshes, no form re-send
# - if user goes back, no form re-send too, classical refresh
site_web = u"{0}://{1}".format(
self.request.scheme, self.request.META['HTTP_HOST']
)
return HttpResponseRedirect(u'{0}{1}'.format(
site_web, self.request.META['PATH_INFO']
))
My form shows 3 fields of 3 different models :
User,
Person which has a foreign key to User
Picture which has a foreign key to Person
Where should I create the code that update those fields, and why?
generic.UpdateView is supposed to help us when updating fields, but it seems that when you have fields not belonging to the model you edit, you have to write all the "update" by hand.
is_valid on the surface just tells you whether or not the form is valid, and thats the only job it should ever do..
From the source code:
def is_valid(self):
"""
Returns True if the form has no errors. Otherwise, False. If errors are
being ignored, returns False.
"""
return self.is_bound and not self.errors
Underneath this, what it also does is (from docs)
run validation and return a boolean designating whether the data was valid:
The validation is ran because errors is a property that will call full_clean if the validation hasn't been called yet.
#property
def errors(self):
"Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean()
return self._errors
Where should I create the code that update those fields, and why?
In the form_valid method because by this point you've found out that your validation has verified that it is safe to update your model.
I have changed a ForeignKey in a model form, to use a TextBox instead.
Then I override clean method to return the object based on the name field (instead id field)
class SongForm(forms.ModelForm):
artist = forms.CharField(widget=forms.TextInput())
def clean_artist(self):
data = self.cleaned_data['artist']
artist = Artist.objects.get(name=data)
self.cleaned_data['artist_id'] = artist.id
return artist
class Meta:
model = Song
It saves the form correctly, how ever when it renders again appears the id value instead the name value. How may I change the display values of a django form? I think overriding init will do it, but can't find where is the value property
I just wrote Field and Widget subclasses, that solve this particular problem and could be used with JS autocompletion, for example - and is reusable. Still, it required more work than your solution and I'm not sure whether you'll want to use mine or not. Either way - I hope I'll get few upvotes - I spent quite some time and effort writing this...
Instead of defining your ModelForm like you did and messing with clean_ I suggest something like that:
class SongForm(forms.ModelForm):
artist = CustomModelChoiceField( queryset = Artist.objects.all(), query_field = "name" )
class Meta:
model = Song
Now, CustomModelChoiceField (I can't think of better name for the class) is ModelChoiceField subclass, which is good, because we can use queryset argument to narrow acceptable choices. If widget argument is not present, like above, the default one for this field is used (more about it later). query_field is optional and defaults to "pk". So, here is the field code:
class CustomModelChoiceField( forms.ModelChoiceField ):
def __init__( self, queryset, query_field = "pk", **kwargs ):
if "widget" not in kwargs:
kwargs["widget"] = ModelTextInput( model_class = queryset.model, query_field = query_field )
super( CustomModelChoiceField, self ).__init__( queryset, **kwargs )
def to_python( self, value ):
try:
int(value)
except:
from django.core.exceptions import ValidationError
raise ValidationError(self.error_messages['invalid_choice'])
return super( CustomModelChoiceField, self ).to_python( value )
What body of __init__ means is that setting widget = None during creation of CustomModelChoiceField gives us plain ModelChoiceField (which was very helpful while debugging...). Now, actual work is done in ModelTextInput widget:
class ModelTextInput( forms.TextInput ):
def __init__( self, model_class, query_field, attrs = None ):
self.model_class = model_class
self.query_field = query_field
super( ModelTextInput, self ).__init__( attrs )
def render(self, name, value, attrs = None ):
try:
obj = self.model_class.objects.get( pk = value )
value = getattr( obj, self.query_field )
except:
pass
return super(ModelTextInput, self).render( name, value, attrs )
def value_from_datadict( self, data, files, name ):
try:
return self.model_class.objects.get( **{ self.query_field : data[name] } ).id
except:
return data[name]
It's essentially TextInput, that is aware of two additional things - which attribute of which model it represents. ( model_class should be replaced with queryset for narrowing of possible choices to actually work, I'll fix it later). Looking at implementation of value_from_datadict it's easy to spot why to_python in the field had to be overridden - it expects int value, but does not check if it's true - and just passes the value to associated model, which fails with ugly exception.
I tested this for a while and it works - you can specify different model fields by which form field will try to find your artist, form error handling is done automatically by base classes and you don't need to write custom clean_ method every time you want to use similar functionality.
I'm too tired right now, but I'll try to edit this post (and code) tomorrow.
I have just got it, the initial hash was what I was missing:
if self.instance.id:
val = self.initial['artist']
self.initial['artist'] = Artist.objects.get(id=val).name
I have a Django form with a username and email field. I want to check the email isn't already in use by a user:
def clean_email(self):
email = self.cleaned_data["email"]
if User.objects.filter(email=email).count() != 0:
raise forms.ValidationError(_("Email not available."))
return email
This works, but raises some false negatives because the email might already be in the database for the user named in the form. I want to change to this:
def clean_email(self):
email = self.cleaned_data["email"]
username = self.cleaned_data["username"]
if User.objects.filter(email=email, username__ne=username).count() != 0:
raise forms.ValidationError(_("Email not available."))
return email
The Django docs say that all the validation for one field is done before moving onto the next field. If email is cleaned before username, then cleaned_data["username"] won't be available in clean_email. But the docs are unclear as to what order the fields are cleaned in. I declare username before email in the form, does that mean I'm safe in assuming that username is cleaned before email?
I could read the code, but I'm more interested in what the Django API is promising, and knowing that I'm safe even in future versions of Django.
Update
.keyOrder no longer works. I believe this should work instead:
from collections import OrderedDict
class MyForm(forms.ModelForm):
…
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
field_order = ['has_custom_name', 'name']
reordered_fields = OrderedDict()
for fld in field_order:
reordered_fields[fld] = self.fields[fld]
for fld, value in self.fields.items():
if fld not in reordered_fields:
reordered_fields[fld] = value
self.fields = reordered_fields
Previous Answer
There are things that can alter form order regardless of how you declare them in the form definition. One of them is if you're using a ModelForm, in which case unless you have both fields declared in fields under class Meta they are going to be in an unpredictable order.
Fortunately, there is a reliable solution.
You can control the field order in a form by setting self.fields.keyOrder.
Here's some sample code you can use:
class MyForm(forms.ModelForm):
has_custom_name = forms.BooleanField(label="Should it have a custom name?")
name = forms.CharField(required=False, label="Custom name")
class Meta:
model = Widget
fields = ['name', 'description', 'stretchiness', 'egginess']
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
ordered_fields = ['has_custom_name', 'name']
self.fields.keyOrder = ordered_fields + [k for k in self.fields.keys() if k not in ordered_fields]
def clean_name(self):
data = self.cleaned_data
if data.get('has_custom_name') and not data.get('name'):
raise forms.ValidationError("You must enter a custom name.")
return data.get('name')
With keyOrder set, has_custom_name will be validated (and therefore present in self.cleaned_data) before name is validated.
The Django docs claim that it's in order of the field definition.
But I've found that it doesn't always hold up to that promise.
Source: http://docs.djangoproject.com/en/dev/ref/forms/validation/
These methods are run in the order
given above, one field at a time. That
is, for each field in the form (in the
order they are declared in the form
definition), the Field.clean() method
(or its override) is run, then
clean_(). Finally, once
those two methods are run for every
field, the Form.clean() method, or its
override, is executed.
There's no promise that the fields are processed in any particular order. The official recommendation is that any validation that depends on more than one field should be done in the form's clean() method, rather than the field-specific clean_foo() methods.
The Form subclass’s clean() method. This method can perform any
validation that requires access to multiple fields from the form at
once. This is where you might put in things to check that if field A
is supplied, field B must contain a valid email address and the like.
The data that this method returns is the final cleaned_data attribute
for the form, so don’t forget to return the full list of cleaned data
if you override this method (by default, Form.clean() just returns
self.cleaned_data).
Copy-paste from https://docs.djangoproject.com/en/dev/ref/forms/validation/#using-validators
This means that if you want to check things like the value of the email and the parent_email are not the same you should do it inside that function. i.e:
from django import forms
from myapp.models import User
class UserForm(forms.ModelForm):
parent_email = forms.EmailField(required = True)
class Meta:
model = User
fields = ('email',)
def clean_email(self):
# Do whatever validation you want to apply to this field.
email = self.cleaned_data['email']
#... validate and raise a forms.ValidationError Exception if there is any error
return email
def clean_parent_email(self):
# Do the all the validations and operations that you want to apply to the
# the parent email. i.e: Check that the parent email has not been used
# by another user before.
parent_email = self.cleaned_data['parent_email']
if User.objects.filter(parent_email).count() > 0:
raise forms.ValidationError('Another user is already using this parent email')
return parent_email
def clean(self):
# Here I recommend to user self.cleaned_data.get(...) to get the values
# instead of self.cleaned_data[...] because if the clean_email, or
# clean_parent_email raise and Exception this value is not going to be
# inside the self.cleaned_data dictionary.
email = self.cleaned_data.get('email', '')
parent_email = self.cleaned_data.get('parent_email', '')
if email and parent_email and email == parent_email:
raise forms.ValidationError('Email and parent email can not be the same')
return self.cleaned_data