Disassociate Foreign Key on django model for validation during editing - python

I have the following model:
class Event(models.Model):
assigned_to= models.ForeignKey(user)
(...) # Some other fields
I create a Event instance
event = Event(assigned_to=someuser, **kwargs)
event.save()
If I have to use the Model validation features while editing the event,
event.assigned_to = None
for doing event.full_clean(), django complains that it cannot set null a foreign key relation.
Cannot assign None: "Event.assigned_to" does not allow null values.
How should I get around this to get errors using the ValidationError raised during the full_clean() for a None specified to the foreignkey field.

I managed to generate errors by using null=True on ForeignKey as #Mad Wombat suggested, and by overriding the clean method on the model to check if the assigned_to on the instance is not None
def clean(self):
if self.assinged_to is None:
raise ValidationError({'assigned_to': "Whatever error"})
As full_clean() calls the clean() method, it raised the validation error as needed.

Related

How to run custom model validations for M2M in clean() when adding a new model instance

I have a model in Django with a ManyToManyField relationship to another model. Something that looks like this:
class MyModel(models.Model):
id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
models = models.ManyToManyField(
OtherModel,
related_name='my_models',
)
In my clean() method, I have a bunch of custom validation rules that are run in the model fields. Some of them actually run in the ManyToManyField. All validations work fine when creating model instances with an uploader tool that parses data from a csv into a model instance or when saving an existing model in the Admin.
However, I want to be able to add model instances directly in the Admin but because of the ManyToMany relationship, which I found out that is not set until after the save() method, some validations throw a ValueError: "<MyModel: Some Model>" needs to have a value for field "id" before this many-to-many relationship can be used. ([ref][1])
Then, what I did is a very ugly hack to to the validation in the clean() method to bypass it by catching the ValueError:
def _some_validation(self):
try:
if self.my_models.count() == 0:
raise ValidationError("This is an error message thrown by ...")
except ValueError as e:
print(e)
Then it works fine to add a model instance via Admin, however, it doesn't run the validation on the ManyToMany field, allowing for inconsistent data.
Is there a way that I can re-run the clean() method after the M2M relationship is set when creating a new model instance via the add_form in the admin?
Note: I am not using custom forms as I found no need since the Django's default generated add form in the Admin satisfies my needs.
[1]: https://docs.djangoproject.com/en/3.1/topics/db/examples/many_to_many/
You can try to listen to the m2m_changed signal
def my_models_changed(sender, **kwargs):
# Do something
pass
m2m_changed.connect(my_models_changed, sender=MyModel.my_models.through)
https://docs.djangoproject.com/en/3.1/ref/models/relations/#django.db.models.fields.related.RelatedManager.add
https://docs.djangoproject.com/en/3.1/ref/signals/#django.db.models.signals.m2m_changed

How to test uniqueness in a Model.clean() function?

I have a model with a UniqueConstraint:
class MyModel(models.Model)
name = models.CharField()
title = models.CharField()
class Meta:
constraints = [ models.UniqueConstraint(
fields=['name', 'title'],
name="unique_name_and_title") ]
This works fine and raises an IntegrityError when 2 objects with the same values are created.
The problem is UniqueConstraint doesn't present a pretty ValidationError to the user. Usually, I would add these in the Model.clean() class, but if I do this then it will fail on an Update because the instance being updated will already be present:
def clean(self):
if MyModel.objects.filter(title=self.title, name=self.name):
raise ValidationError({'title':'An object with this name+title already exists'})
I How do I create a ValidationError that passes if it's an UPDATE not an INSERT?
I know I could do this on a ModelForm and use self.instance to check if the instance already exists, but I want to apply this to the Model class and not have to rely on a ModelForm.
You can exclude the object from the queryset you check:
def clean(self):
qs = MyModel.objects.exclude(pk=self.pk).filter(title=self.title, name=self.name)
if qs.exists():
raise ValidationError({'title':'An object with this name+title already exists'})
return super().clean()
If the object is not yet saved, it will check for .exclude(pk=None), but that will not exclude any objects, since the primary key is non-nullable.
It is more efficient to use .exists() [Django-doc] here, since it limits the bandwidth from the database to the Django/Python layer.

TypeError in models

I'm getting this error:
user = models.OneToOneField(User)
TypeError: init() missing 1 required positional argument: 'on_delete'
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class UserProfileInfo(models.Model):
# creating relationship
user = models.OneToOneField(User)
# additional attributes
portfolio = models.URLField(blank=True)
picture = models.ImageField(upload_to='profile_pics', blank=True)
def __str__(self):
return self.user.username
As the error indicates, you need to specify what should happen, given the object to which you refer is removed, by the on_delete= parameter [Django-doc]. For example:
class UserProfileInfo(models.Model):
# creating relationship
user = models.OneToOneField(User, on_delete=models.CASCADE)
# additional attributes
portfolio = models.URLField(blank=True)
picture = models.ImageField(upload_to='profile_pics', blank=True)
def __str__(self):
return self.user.username
The options here are:
CASCADE
Cascade deletes. Django emulates the behavior of the SQL constraint
ON DELETE CASCADE and also deletes the object containing the
ForeignKey.
Model.delete() isn't called on related models, but the pre_delete
and post_delete signals are sent for all deleted objects.
PROTECT
Prevent deletion of the referenced object by raising ProtectedError,
a subclass of django.db.IntegrityError.
SET_NULL
Set the ForeignKey null; this is only possible if null is True.
SET_DEFAULT
Set the ForeignKey to its default value; a default for the
ForeignKey must be set.
SET()
Set the ForeignKey to the value passed to SET(), or if a callable
is passed in, the result of calling it. In most cases, passing a
callable will be necessary to avoid executing queries at the time your
models.py is imported (...)
DO_NOTHING
Take no action. If your database backend enforces referential
integrity, this will cause an IntegrityError unless you manually add
an SQL ON DELETE constraint to the database field.
A similar question is answered here Getting TypeError: __init__() missing 1 required positional argument: 'on_delete' when trying to add parent table after child table with entries
Basically following should fix url problem
From Django 2.0 on_delete is required:
user = models.OneToOneField(User, on_delete=models.CASCADE)
put 'on_delete = models.CASCADE' in the constructor
what it does: when you will delete the user object as you have referenced it in the user field of your model. it will also delete the model object of UserProfileInfo for taht particular user.

Django - validate unique for a calculated field in the Model and also in the ModelForm

TL;DR both my model and my form calculate the value of the field number_as_char. Can I avoid the double work, but still check uniqueness when using the model without the form?
I use Python 3 and Django 1.11
My model looks as follows:
class Account(models.Model):
parent_account = models.ForeignKey(
to='self',
on_delete=models.PROTECT,
null=True,
blank=True)
number_suffix = models.PositiveIntegerField()
number_as_char = models.CharField(
max_length=100,
blank=True,
default='',
unique=True)
#classmethod
def get_number_as_char(cls, parent_account, number_suffix):
# iterate over all parents
suffix_list = [str(number_suffix), ]
parent = parent_account
while parent is not None:
suffix_list.insert(0, str(parent.number_suffix))
parent = parent.parent_account
return '-'.join(suffix_list)
def save(self, *args, **kwargs):
self.number_as_char = self.get_number_as_char(
self.parent_account, self.number_suffix)
super().save(*args, **kwargs)
The field number_as_char is not supposed to be set by the user because it is calculated based on the selected parent_account: it is obtained by chaining the values of the field number_suffix of all the parent accounts and the current instance.
Here is an example with three accounts:
ac1 = Account()
ac1.parent_account = None
ac1.number_suffix = 2
ac1.save()
# ac1.number_as_char is '2'
ac2 = Account()
ac2.parent_account = ac1
ac2.number_suffix = 5
ac2.save()
# ac2.number_as_char is '2-5'
ac3 = Account()
ac3.parent_account = ac2
ac3.number_suffix = 1
ac3.save()
# ac3.number_as_char is '2-5-1'
It is NOT an option to drop the field and use a model property instead, because I need to ensure uniqueness and also use that field for sorting querysets with order_by().
My form looks as follows:
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = [
'parent_account', 'number_suffix', 'number_as_char',
]
widgets = {
'number_as_char': forms.TextInput(attrs={'readonly': True}),
}
def clean(self):
super().clean()
self.cleaned_data['number_as_char'] = self.instance.get_number_as_char(
self.cleaned_data['parent_account'], self.cleaned_data['number_suffix'])
I included number_as_char in the form with widget attribute readonly and I use the forms clean() method to calculate number_as_char (it has to be calculated before validating uniqueness).
This all works (the model and the form), but after validating the form, the value of number_as_char will be calculated again by the models save() method. Its not a big problem, but is there a way to avoid this double calculation?
If I remove the calculation from the forms clean() method, then the uniqueness will not be validated with the new value (it will only check the old value).
I don't want to remove the calculation entirely from the model because I use the model in other parts without the form.
Do you have any suggestions what could be done differently to avoid double calculation of the field?
I can't see any way around doing this in two places (save() and clean()) given that you need it to work for non-form-based saves as well).
However I can offer two efficiency improvements to your get_number_as_char method:
Make it a cached_property so that the second time it is called, you simply return a cached value and eliminate double-calculation. Obviously you need to be careful that this isn't called before an instance is updated, otherwise the old number_as_char will be cached. This should be fine as long as get_number_as_char() is only called during a save/clean.
Based on the information you've provided above you shouldn't have to iterate over all the ancestors, but can simply take the number_as_char for the parent and append to it.
The following incorporates both:
#cached_property
def get_number_as_char(self, parent_account, number_suffix):
number_as_char = str(number_suffix)
if parent_account is not None:
number_as_char = '{}-{}'.format(parent_account.number_as_char, number_as_char)
return number_as_char
To be sure that the caching doesn't cause problems you could just clear the cached value after you're done saving:
def save(self, *args, **kwargs):
self.number_as_char = self.get_number_as_char(
self.parent_account, self.number_suffix)
super().save(*args, **kwargs)
# Clear the cache, in case something edits this object again.
del self.get_number_as_char
I tinkered with it a bit, and I think I found a better way.
By using the disabled property on the number_as_char field of your model form, you can entirely ignore users input (and make the field disabled in a single step).
Your model already calculates the number_as_char attribute in the save method. However, if the Unique constraint fails, then your admin UI will throw a 500 error. However, you can move your field calculation to the clean() method, leaving the save() method as it is.
So the full example will look similar to this:
The form:
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = [
'parent_account', 'number_suffix', 'number_as_char',
]
widgets = {
'number_as_char': forms.TextInput(attrs={'disabled': True}),
}
The model:
class Account(models.Model):
# ...
def clean(self):
self.number_as_char = self.get_number_as_char(
self.parent_account, self.number_suffix
)
super().clean()
That way anything that generates form based on your model will throw a nice validation error (provided that it uses the built-in model validation, which is the case for Model Forms).
The only downside to this is that if you save a model that triggers the validation error, you will see an empty field instead of the value that failed the validation - but I guess there is some nice way to fix this as well - I'll edit my answer if I also find a solution to this.
After reading all the answers and doing some more digging through the docs, I ended up using the following:
#samu suggested using the models clean() method and #Laurent S suggested using unique_together for (parent_account, number_suffix). Since only using unique_together doesn't work for me because parent_account can be null, I opted for combining the two ideas: checking for existing (parent_account, number_suffix) combinations in the models clean() method.
As a consecuence, I removed number_as_char from the form and it is now only calculated in the save() method. By the way: thanks to #solarissmoke for suggesting to calculated it based on the first parent only, not iterating all the way to the top of the chain.
Another consecuence is that I now need to explicitly call the models full_clean() method to validate uniqueness when using the model without the form (otherwise I will get the database IntegrityError), but I can live with that.
So, now my model looks like this:
class Account(models.Model):
parent_account = models.ForeignKey(
to='self',
on_delete=models.PROTECT,
null=True,
blank=True)
number_suffix = models.PositiveIntegerField()
number_as_char = models.CharField(
max_length=100,
default='0',
unique=True)
def save(self, *args, **kwargs):
if self.parent_account is not None:
self.number_as_char = '{}-{}'.format(
self.parent_account.number_as_char,
self.number_suffix)
else:
self.number_as_char = str(self.number_suffix)
super().save(*args, **kwargs)
def clean(self):
qs = self._meta.model.objects.exclude(pk=self.pk)
qs = qs.filter(
parent_account=self.parent_account,
number_suffix=self.number_suffix)
if qs.exists():
raise ValidationError('... some message ...')
And my form ends up like this:
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = [
'parent_account', 'number_suffix',
]
EDIT
I'll mark my own answer as accepted, because non of the suggestions fully suited my needs.
However, the bounty goes to #samus answer for pointing me in the right direction with using the clean() method.
Another way - probably not as good though - would be to use Django signals. You could make a pre_save signal that would set the correct value for number_as_char field on the instance that's about to get saved.
That way you don't have to have it done in a save() method of your model, OR in the clean() method of your ModelForm.
Using signals should ensure that any operation that uses the ORM to manipulate your data (which, by extend, should mean all ModelForms as well) will trigger your signal.
The disadvantage to this approach is that it is not clear from the code directly how is this property generated. One has to stumble upon the signal definition in order to discover that it's even there. If you can live with it though, I'd go with signals.

Overwriting serializer with request data including nulls for absent keys

Let's imagine there is a serializer like this:
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = (
'title',
'description'
)
Where description is nullable. What I want is that the request data completely overwrite the serializer data on PUT request (when updating an existing model instance obviously). If I do:
event_serializer = EventSerializer(event, data=request_data)
It does overwrite everything, but it doesn't nullify description if it is absent from the request. Is there a way to do that without doing manually:
data['description'] = data.get('description', None)
One option is to define the description field on the serializer and use default like:
class EventSerializer(serializers.ModelSerializer):
# Use proper field type here instead of CharField
description = serializers.CharField(default=None)
class Meta:
model = Event
fields = (
'title',
'description'
)
See the documentation as well:
default
If set, this gives the default value that will be used for the
field if no input value is supplied. If not set the default behavior
is to not populate the attribute at all.
May be set to a function or other callable, in which case the value
will be evaluated each time it is used.
Note that setting a default value implies that the field is not
required. Including both the default and required keyword arguments is
invalid and will raise an error.

Categories