ModelForm and unique_together validation - django

Django 1.10
The documentaion says that ModelForm validates unique together if clean method is correctly overridden.
https://docs.djangoproject.com/en/1.10/topics/forms/modelforms/#overriding-the-clean-method
I have done something wrong as unique_together validation doesn't work.
>>> from wiki.models import Wiki
>>> Wiki.objects.all()
<QuerySet [<Wiki: image/1/fdfff>, <Wiki: image/1/fdffff>]>
Where image is related_model and 1 is related_id.
Could you help me understand what is wrong with this overriding?
class WikiForm(ModelForm):
class Meta:
model = Wiki
fields = ['related_model', 'related_id', 'article']
unique_together = (("related_model", "related_id"),)
def validate_related_model(self):
...
def validate_related_id(self):
...
def clean(self):
self.validate_related_model()
self.validate_related_id()
# To maintain unique_together validation,
# we must call the parent class’s clean() method.
return super(WikiForm, self).clean()

unique_together is a database-level constraint. It's supposed to be specified in the Meta class of the model, not the model form. For the validation to work as you would like, move it to the Wiki model. In your form, you probably won't even need to have those extra validation methods.
This doesn't look like it'd be the case for you, but also note that in order for the unique validation to work correctly, your model form must include all of the fields that are specified in the unique_together constraint. So, if related_model or related_id were excluded from the form, you'd need to do some extra work in order to allow the correct validation to happen:
def validate_unique(self):
# Must remove organization field from exclude in order
# for the unique_together constraint to be enforced.
exclude = self._get_validation_exclusions()
exclude.remove('organization')
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError, e:
self._update_errors(e.message_dict)
In the example above I am removing organization from the form's list of excluded fields because it's part of the unique_together constraint.

Related

Validate multiple foreign keys while saving a ModelForm

Explanation:
I have a model that has two foreign keys:
class Book(models.Model):
name = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=CASCADE)
created_by = models.ForeignKey(User, blank=True, null=True, on_delete=CASCADE)
I have a modelform for it:
class BookForm(modelForm):
class Meta:
model = Book
exclude = ('author', 'created_by',)
In my business logic, I want to save a new author and a book with it in the same view, and the created_by could be null in some cases. So, before saving an instance of a BookForm, I will create an author first. So, suppose that I have created an author, then I will do the below to save a book:
f = BookForm(post_data)
if form.is_valid():
instance = f.save(commit=False)
instance.author = author
instance.created_by = user
instance.save()
What I did so far is working fine, but the foreign key fields are not part of the validation. As you can see from the model class, author field is mandatory, but created_by field is optional. So, if I want to add some validations for them, I have to add them explicitly by customizing the is_valid() method, or by adding some if conditions before instance.author = author.
Question:
The bigger picture is that I have a model with much more foreign keys; some of them are mandatory, and the others are not. So, using the same approach I did above, I have to add a custom validation for each FK explicitly, which I consider it duplication and unclean as those FK fields validation is defined in my model class.
So, what is the best practice to validate automatically those multiple foreign keys while saving by checking the models validation directly and without mentioning each FK one by one?
Since the foreign key fields are not included in your form, there won't be any form validation on them. What you need is a model validation.
Django provides the model.full_clean() method, which performs all model validation steps.
You will need to call the method manually, since save() method does not invoke any model clean method.
See documentation here.
For example,
f = BookForm(post_data)
if form.is_valid():
instance = f.save(commit=False)
instance.author = author
instance.created_by = user
try:
instance.full_clean()
except ValidationError as e:
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
instance.save()
If you want to display the error messages to the user, simply pass e to the context and loop through the messages in your template, displaying them wherever you want. This seems the cleaner approach than adding errors to the form, because they are model validation errors not form errors.
In your view:
except ValidationError as e:
context = {'e':e} # add other relevant data to context
return render(request, 'your_template', context)
In your template:
{% for key, values in e.message_dict %}
{{key}}: {{value}}
{% endfor %}
If you want the errors added to the form, then use form.add_error:
except ValidationError as e:
for key, value in e.items():
# this adds none-field errors to the form for each model error
form.add_error(None, f'{key}: {value}')

Django Rest Framework, ModelSerializers and custom fields validation

Using latest version of Django and DRF.
I have a rather complex requirement I can't find a solution for. I'll try to simplify it.
Let's say I have a model that has two fields. field_a and field_b
I have a ModelSerializer for it. I POST a request with its fields. The fields get validated with the model and then against my two serializer functions validate_field_a and validate_field_b. All is well.
Now I'd like my POST request to include a third field that is not a member of that model. let's call it field_c. I have a custom def create(self, validated_data): in my serializer which saves everything to the database.
with regards to field_c I would like to:
Custom Validate it. just like I do with the other two fields.
Require that it is mandatory for the whole request to succeed and if it's not, issue a "Field is required" error just like if I forgot to POST one of my required model fields.
Have the chance to take field_c and save it onto a totally different unrelated Model's row in the db.
I can't seem to get around that. If I add field_c to the fields meta - it throws an exception saying justifiably that field_c is not in my model. If I don't include it in fields, the validate_field_c which I really want to put there doesn't even get called.
What can I do?
You can add the custom field in your serializer as a write_only field and override the create method so that you can handle the custom field's value.
Something like this:
class MySerializer(serializers.ModelSerializer):
field_c = serializers.CharField(write_only=True)
class Meta:
model = MyModel
fields = ('field_a', 'field_b', 'field_c')
def validate_field_c(self, value):
if value is 'test':
raise ValidationError('Invalid')
return value
def create(self, validated_data, **kwargs):
field_c = validated_data.pop('field_c')
return MyModel.objects.create(**validated_data)
Don't use ModelSerializer for this - use a serializer that recreates the same fields as your model & include field_c as you would.
I understand that you want your model to do some of the work in the validation process but the design of DRF is such that it isolates these responsibilities. You can read more about it here. Basically, the serializer should be the one doing all the validation heavy-lifting.
Of course, this means that you'll have to explicitly define the validation methods in the serializer.
In your custom create() method you can create the model instance or do whatever you want in it as required.

Django model validation

I'm facing a validation problem.
I need to use form validation and model validation together, but django (1.10) doesn't seem to like this.
Here is a short version of my setup:
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk.som_field != self.foo:
raise ValidationError("This did not validate")
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('fk',)
def view(request):
instance = MyModel(foo='bar')
form = MyModelForm(data=request.POST, instance=instance)
if form.is_valid():
# process
# redirect
# display template
So I have some model field validation in the model itself.
I need this here because it is re-used in other non-form related parts of my application.
And I have some user input validation in the form.
In this case, checking that the provided fk is valid and exists.
But when the form is validated and the user provided 'fk' is not valid, the form is rejected.
But the form also calls MyModel.full_clean add more model validation.
The problem is that calling MyModel.clean() without having any data in the field fk will raise a RelatedObjectDoesNotExist exception.
How can I do this the proper way ?
I feel that MyModel.full_clean() should not be called by the form until the form itself is valid to ensure that the model is validated with at least correct field types in it.
I could embed my MyModel.clean operations in a try/except that would catch a RealtedObjectDoesNotExist, but it has a bad code smell for me.
The fact that MyModel.fk exists should be ensured by the first form layer validation.
As you have found, the model form performs the instance validation (by calling self.instance.full_clean()`) whether or not the form is valid.
You could prevent this behaviour (I might try to override the _post_clean, but I think it's simpler to take account of the behaviour in the clean method.
Rather than catching the exception, it would be simpler to check that self.fk_id is not None before accessing self.fk.
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk_id is not None and self.fk.som_field != self.foo:
raise ValidationError("This did not validate")

Django form blank=False

Currently, if a field is required, this can be enforced via the blank = False argument, such as:
models.py
address1 = models.CharField(max_length=255,null=False,blank=False)
However, the validation is performed prior to the POST action, yielding something like this when trying to submit the form containing an empty field:
I would prefer the validation to be done during the post step, like this:
models.py
address1 = models.CharField(max_length=255,null=False,blank=true)
forms.py
class AddressForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
super(AddressForm,self).__init__(*args,**kwargs)
self.fields['address1'].required = True
And this yields the following result when trying to submit the form containing an empty field:
But the problem with this, (as far as I can tell) is that I need to explicitly state the required attribute for each field on a case-by-case basis.
Is there any way that I can associate blank=False as being representative of the required=True attribute, suppressing the first form validation (above), in favour of the second?
ModelForm runs form validation, then model validation:
There are two main steps involved in validating a ModelForm:
Validating the form
Validating the model instance
So you have to manually add the extra form validation that you want before the inherited model validations.
However, default ModelForm field for blank field is already required:
If the model field has blank=True, then required is set to False on
the form field. Otherwise, required=True
You can change the error message. If you use this additional validations a lot, you can use a Mixin:
class BlankToRequiredMixin(object):
def set_required(self):
model = self._meta.model
for field_name,form_field in self.fields.iteritems():
if not model._meta.get_field(field_name).blank:
form_field.error_messages={'required': 'This field is required'} # to make it required in addtion to non-blank set .required=True
Then, to set required=True for all fields that are non-blank in the model:
class AddressForm(forms.ModelForm,BlankToRequiredMixin):
def __init__(self,*args,**kwargs):
super(AddressForm,self).__init__(*args,**kwargs)
self.set_required()
In a similar way you can add other validations to the form fields, based on the model validation attributes. For the appearance, change the widget and set the field widget in the mixin.

Why is my forms clean method not doing anything?

I have two basic models that use model forms in the Django admin.
Models.py is similar to:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField()
class OtherModel(models.Model):
model = models.ForeignKey(FirstModel)
##Other fields that show up fine and save fine, but include some localflavor
Forms.py looks similar to:
class FirstModelForm(forms.ModelForm):
def clean(self):
#call the super as per django docs
cleaned_data = super(FirstModelForm, self).clean()
print cleaned_data
class Meta:
model = FirstModel
#other modelform is the same with the appropriate word substitutions and one field that gets overridden to a USZipCodeField
These are a stacked inline ModelAdmin with nothing special in the admin.py:
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (#my list of fields works correctly)
readonly_fields = (#couple read onlys that work correctly)
class FirstModelAdmin(admin.ModelAdmin):
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
I do have a User model, form and ModelAdmin that subclasses the User and UserCreationForm and overrides it's own clean method.This works exactly as expected.
The problem is with FirstModel and OtherModel. The clean methods I override in the ModelForm subclasses of FirstModelForm and OtherModelForm don't do anything. No exception thrown or a print of the cleaned_data. Just nothing. Everything else works as expected, but it's like my clean method isn't even there.
I got to be missing something simple, but I can't see what is. Any help would be great. Thanks!
By default, Django dynamically generates a model form for your model admins. You must specify that you want to use your custom forms by setting the form attribute.
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (...) # if this doesn't work after specifying the form, set fields for the model form instead
readonly_fields = (#couple read onlys that work correctly)
form = OtherModelForm
class FirstModelAdmin(admin.ModelAdmin):
form = FirstModelForm
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
You need to return the cleaned_data from the clean method in the form. If you look at the documentation for cleaning fields that rely on each other you'll notice:
...
# Always return the full collection of cleaned data.
return cleaned_data
It is possible that nothing survived the parent 'clean' method. If you are submitting data that won't validate because of the way your models are set up, cleaned_data will be empty. This is mentioned in the same doc linked by Timmy, where it says:
By the time the form’s clean() method is called, all the individual field clean methods will have been run (the previous two sections), so self.cleaned_data will be populated with any data that has survived so far. So you also need to remember to allow for the fact that the fields you are wanting to validate might not have survived the initial individual field checks.
In this case, if you have a URLField, the field validation is very strict, and unless you define 'verify_exists=False', it will also check if you are putting in a URL that returns a 404. In your case you would need to do this if you wanted to allow that:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField(verify_exists=False)
Outside of that, I have no idea what could be going on.