I need to raise an exception in a model's save method. I'm hoping that an exception exists that will be caught by any django ModelForm that uses this model including the admin forms.
I tried raising django.forms.ValidationError, but this seems to be uncaught by the admin forms. The model makes a remote procedure call at save time, and it's not known until this call if the input is valid.
Thanks, Pete
Since Django 1.2, this is what I've been doing:
class MyModel(models.Model):
<...model fields...>
def clean(self, *args, **kwargs):
if <some constraint not met>:
raise ValidationError('You have not met a constraint!')
super(MyModel, self).clean(*args, **kwargs)
def full_clean(self, *args, **kwargs):
return self.clean(*args, **kwargs)
def save(self, *args, **kwargs):
self.full_clean()
super(MyModel, self).save(*args, **kwargs)
This has the benefit of working both inside and outside of admin.
There's currently no way of performing validation in model save methods. This is however being developed, as a separate model-validation branch, and should be merged into trunk in the next few months.
In the meantime, you need to do the validation at the form level. It's quite simple to create a ModelForm subclass with a clean() method which does your remote call and raises the exception accordingly, and use this both in the admin and as the basis for your other forms.
Related
I am creating a django DB model and I want one the fields to be readonly. When creating a new object I want to set it, but later if someone tries to update the object, it should raise an error. How do I achieve that?
I tried the following but I was still able to update the objects.
from django.db import models as django_db_models
class BalanceHoldAmounts(django_db_models.Model):
read_only_field = django_db_models.DecimalField(editable=False)
Thank you
You can override it in the "save" method of the model and raise a Validation Error if someone tries to update that field.
def save(self, *args, **kwargs):
if self.pk:
previous_value = BalanceHoldAmounts.objects.get(pk=self.pk)
if previous_value.read_only_field != self.read_only_field:
raise ValidationError("The read_only_field can not be changed")
super().save(*args, **kwargs)
I'd like to make sure that nobody can't create an Invitation object with an email that is already in a database either as Invitation.email or as User.email.
To disallow creating Invitation with existing Invitation.email is easy:
class Invitation(..):
email = ...unique=True)
Is it also possible to check for the email in User table? I want to do this on a database or model level instead of checking it in serializer, forms etc..
I was thinking about UniqueConstraint but I don't know how to make the User.objects.filter(email=email).exists() lookup there.
You can override the save() method on the model, and check first in the users table. You should look that is a new model. Something like this I think:
class Invitation(..):
email = ...unique=True)
def save(self, *args, **kwargs):
if self.id is None and User.objects.filter(email=self.email).exists():
raise ValidationError('Email already used.')
else:
super().save(*args, **kwargs)
You can do it in the model.. as below. Or you can do it in the database with a Check Constraint (assuming postgres).. but you still can't avoid adding code to your view, because you'll need to catch the exception and display a message to the user.
class Invitation(models.Model):
def save(self, *args, **kwargs):
if (not self.pk) and User.objects.filter(email=self.email).exists():
raise ValueError('Cannot create invitation for existing user %s.' % self.email)
return super().save(*args, **kwargs)
PS: Some may ask why it is that I am passing *args and **kwargs to the superclass, or returning the return value.. when save has no return value. The reason for this is that I never assume that the arguments or return value for a method I am overriding won't change in the future. Passing them all through if you have no reason to intercept them, is just a good practice.
How about overriding the save method?
class Invitation(...):
...
def save(self, *args, **kwargs):
# check if an invitation email on the user table:
if User.objects.get(id=<the-id>).email:
# raise integrity error:
...
# otherwise save as normal:
else:
super().save(*args, **kwargs)
How do we enforce Django Admin to correctly call .update() instead of .save() to avoid triggering checks meant for object creation?
This is the models.py:
class BinaryChoice():
# field definitions
...
def save(self, *args, **kwargs):
# check if binary
if self.question.qtype == 2:
if self.question.choices.count() < 2:
super(BinaryChoice, self).save(*args, **kwargs)
else:
raise Exception("Binary question type can contain at most two choices.")
else:
super(BinaryChoice, self).save(*args, **kwargs)
This passes the test, no surprises:
class SurveyTest(TestCase):
def test_binary_choice_create(self):
q1 = Question.objects.create(survey=survey, title='Have you got any internship experience?', qtype=Question.BINARY)
BinaryChoice.objects.create(question=q1, choice="Yes")
BinaryChoice.objects.create(question=q1, choice="No")
with self.assertRaises(Exception):
BinaryChoice.objects.create(question=q1, choice="Unsure / Rather not say")
The .save() correctly checks that there isn't already 2 binary choices related to the same Question. However, in Django Admin, when using the interface to update the value (anything arbitrary, for example changing the value from "Yes" to "Sure") and saving it, one would expect the .update() method to be called.
It turns out, according to Django docs and also a relevant thread here, the .save() method is called instead. So now our update operation would fail when there's already 2 BinaryChoice, even if you intend to update a value in-place using the Django Admin's default interface.
For completeness sake, this is admin.py:
#admin.register(BinaryChoice)
class BinaryChoiceAdmin(admin.ModelAdmin):
pass
Instead of trying to patch the ModelAdmin why don't you simply fix your save method? Simply check if the object already has a pk or not before saving:
class BinaryChoice():
# field definitions
...
def save(self, *args, **kwargs):
# check if binary
# Here ↓
if not self.pk and self.question.qtype == 2:
if self.question.choices.count() < 2:
super(BinaryChoice, self).save(*args, **kwargs)
else:
raise Exception("Binary question type can contain at most two choices.")
else:
super(BinaryChoice, self).save(*args, **kwargs)
What is the main difference between clean and full_clean function in Django model ?
From the documentation:
Model.full_clean(exclude=None, validate_unique=True):
This method calls Model.clean_fields(), Model.clean(), and
Model.validate_unique() (if validate_unique is True), in that order
and raises a ValidationError that has a message_dict attribute
containing errors from all three stages.
Model.clean():
This method should be used to provide custom model validation, and to
modify attributes on your model if desired.
For more detailed explanation, have a look at the Validating objects section of the documentation.
they are not against each other usually you call Model.full_clean()
to be able to trigger Model.clean() for costume validation
for example:
from django.core.exceptions import ValidationError
from django.db import models
class Brand(models.Model):
title = models.CharField(max_length=512)
def clean(self):
if self.title.isdigit():
raise ValidationError("title must be meaningful not only digits")
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)
here are three steps involved in validating a model:
Validate the model fields - Model.clean_fields()
Validate the model as a whole - Model.clean()
Validate the field uniqueness - Model.validate_unique()
All three steps are performed when you call a model’s full_clean() method. for more information click here
In my view:
def delete_payment(request, id):
thePayment = Payment.objects.filter(id=id)
thePayment.delete()
return HttpResponseRedirect('/invoices/open/')
In my model:
def delete(self, *args, **kwargs):
raise Exception('foo')
super(Payment, self).delete(*args, **kwargs)
I'm finding that the exception doesn't get raised unless I delete the instance from within the admin view. That is, I can't get delete() to be called properly if I use my own view.
Manager.filter() returns a QuerySet, not a Model. QuerySet.delete() doesn't invoke Model.delete() but rather operates directly on the database.