Why doesn't django's model.save() call full_clean()? - python

I'm just curious if anyone knows if there's good reason why django's orm doesn't call 'full_clean' on a model unless it is being saved as part of a model form.
Note that full_clean() will not be called automatically when you call your model’s save() method. You’ll need to call it manually when you want to run one-step model validation for your own manually created models.
django's full clean doc
(NOTE: quote updated for Django 1.6... previous django docs had a caveat about ModelForms as well.)
Are there good reasons why people wouldn't want this behavior? I'd think if you took the time to add validation to a model, you'd want that validation run every time the model is saved.
I know how to get everything to work properly, I'm just looking for an explanation.

AFAIK, this is because of backwards compatibility. There are also problems with ModelForms with excluded fields, models with default values, pre_save() signals, etc.
Sources you might be intrested in:
http://code.djangoproject.com/ticket/13100
http://groups.google.com/group/django-developers/browse_frm/thread/b888734b05878f87

Because of the compatibility considering, the auto clean on save is not enabled in django kernel.
If we are starting a new project and want the default save method on Model could clean automatically, we can use the following signal to do clean before every model was saved.
from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save
#receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
instance.full_clean()

The simplest way to call the full_clean method is just to override the save method in your model:
class YourModel(models.Model):
...
def save(self, *args, **kwargs):
self.full_clean()
return super(YourModel, self).save(*args, **kwargs)

Commenting on #Alfred Huang's answer and coments on it. One might lock the pre_save hook down to an app by defining a list of classes in the current module (models.py) and checking against it in the pre_save hook:
CUSTOM_CLASSES = [obj for name, obj in
inspect.getmembers(sys.modules[__name__])
if inspect.isclass(obj)]
#receiver(pre_save)
def pre_save_handler(sender, instance, **kwargs):
if type(instance) in CUSTOM_CLASSES:
instance.full_clean()

If you have a model that you want to ensure has at least one FK relationship, and you don't want to use null=False because that requires setting a default FK (which would be garbage data), the best way I've come up with is to add custom .clean() and .save() methods. .clean() raises the validation error, and .save() calls the clean. This way the integrity is enforced both from forms and from other calling code, the command line, and tests. Without this, there is (AFAICT) no way to write a test that ensures that a model has a FK relation to a specifically chosen (not default) other model.
class Payer(models.Model):
name = models.CharField(blank=True, max_length=100)
# Nullable, but will enforce FK in clean/save:
payer_group = models.ForeignKey(PayerGroup, null=True, blank=True,)
def clean(self):
# Ensure every Payer is in a PayerGroup (but only via forms)
if not self.payer_group:
raise ValidationError(
{'payer_group': 'Each Payer must belong to a PayerGroup.'})
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)
def __str__(self):
return self.name

Instead of inserting a piece of code that declares a receiver, we can use an app as INSTALLED_APPS section in settings.py
INSTALLED_APPS = [
# ...
'django_fullclean',
# your apps here,
]
Before that, you may need to install django-fullclean using PyPI:
pip install django-fullclean

A global pre_save signal can work well if you want to always ensure model validation. However it will run into issues with Django's auth in current versions (3.1.x) and could cause issues with models from other apps you are using.
Elaborating on #Peter Shannon's answer, this version will only validate models inside the module you execute it in, skips validation with "raw" saves and adds a dispatch_uid to avoid duplicate signals.
from django.db.models.signals import pre_save
import inspect
import sys
MODELS = [obj for name, obj in
inspect.getmembers(sys.modules[__name__], inspect.isclass)]
def validate_model(sender, instance, **kwargs):
if 'raw' in kwargs and not kwargs['raw']:
if type(instance) in MODELS:
instance.full_clean()
pre_save.connect(validate_model, dispatch_uid='validate_models')

Related

Django: What is the simplest way to asynchronously execute some function when there is a change to the database?

I am familiar wıth the DBMS_ALERT feature in Oracle that sends an alert to the Operating System when there is a change to the database, and am somewhat familiar with database triggers in general. I have browsed through the django docs on signals, and inclined to use them if there is not a simpler way.
All I want to do is update or create an external file on the systems file system whenever there is an update or created record in the database. I would like this method to be called and defined right in models.py as depicted below.
models.py
from django.db import models
from django.shortcuts import reverse
class Entry(models.Model):
subject = CharField(max_length=20, unique=True)
content = models.TextField(blank=False)
class Meta:
ordering = ['subject']
def __str__(self):
"""String for representing the Model object."""
return self.subject
def get_absolute_url(self):
"""Returns the url to access a detail record for this entry."""
return reverse('entry_detail', args=[int(self.id)])
def ondbchange_updatecorrfile(self):
# this method would be called upon change in the model Entry, so it is already aware of object
util.save_entry(self.subject,self.content) # file corresponding to row is updated or created
What would be the simplest method to implement the ondbchange_updatecorrfile(self) method above?
I have looked at the below code from this source
models.py
class Entry(models.Model):
...
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=Entry)
def queue_task(sender, instance, created, **kwargs):
tasks.foo.delay(object=instance)
Then foo is some function in another class that would update the file. Since the database Model is the class that is aware of the change in the Model's underlying database, do we really need to use a signal if the desired function is already in the model and "self aware" of the database change?
Any help deeply appreciated. Please be specific since the devil is in the details.
The post_save signal is sent by the model Entry after a save, but according to this source, the signal does not include the model changes in its update_fields parameter. These must be manually inserted in the override of the function where the save() happens; which defeats the purpose. I initially assumed that the signal would automatically include this information.

How can I implement a verified field in django?

In Django I'd like to add a field "verified" of type BooleanField to my models which shall indicate if the current model instance has been reviewed by a staff user member or not. Whenever a model instance field other than the "verified" field changed the verified field value shall be reset to False. Whenever only the "verified" field has been changed it's value shall be taken as is (most of the time True but potentially False as well).
One possibility would be to reset the "verified" field in post-save signals handlers considering update_fields passed to save(). However using signals seems to be considered an anti-pattern in almost all use cases. Instead one should override the save() method. But still when overriding save I'd have to determine update_fields manually somehow. Otherwise I've no information about which fields changed.
How can I implement something like this most easily. I'd prefer a solution using a third-party package w.o. custom hacks or a solution without any dependencies to other packages. However using django-model-utils monitorfield, django-dirtyfields for a custom implementation or something equivalent would be ok as well.
Using dirty-fields seems to be easiest to implement a verified field. So far I came up with something like follows:
DJANGO-APP/models.py:
from django.db import models
from dirtyfields import DirtyFieldsMixin
class VerifiedModel(DirtyFieldsMixin, models.Model):
"""
Abstract class which allows to extend models with user verification model field.
"""
ENABLE_M2M_CHECK = True
verified = models.BooleanField(
default=False,
help_text="The verification status. True means meta-data is verified. False means meta-data is not verified. The verification status is reset to False whenever a field is set.",
)
def _update_verified_field(self):
"""
To be called in inheriting model's save() method.
"""
if self.is_dirty():
if not 'verified' in self.get_dirty_fields():
self.verified = False
class Meta:
abstract = True
class ModelToBeVerified(VerifiedModel):
...
def save(self, *args, **kwargs):
...
self._update_verified_field()
return super(ModelToBeVerified, self).save(*args, **kwargs)

Why overriden save() method is bypassed?

In a Django model, I want to avoid doubles so I wrote this:
class Points(TimeStampedModel):
....
def save(self, *args, **kwargs):
if self.pk:
super(Points, self).save(*args, **kwargs)
else: # save() is a creation here, not an update
if Points.objects.filter(benef_card=self.benef_card,
spendable_at=self.spendable_at).exists():
pass
else:
super(Points, self).save(*args, **kwargs)
I was very surprised to find this result in my database:
I suppose there is something wrong with my code, but I'd like to know how these doubles could exist inspite of the protection I wrote in my save() method?
I think what you want instead is:
class Points(TimeStampedModel):
# ...
class Meta:
unique_together = ('benef_card', 'spendable_at')
Then you don't need to override save -- the uniqueness will be handled by a DB constraint and it is generally the way to go. This approach is better because save is not always called (example: bulk operations) so you might get different behavior across your app.
You might also want to check out update_or_create which just returns an object with attributes you need, creating it if it doesn't exist.
You could use Django signals instead to check before save.

Cancel saving model when using pre_save in django

I have a model:
class A(models.Model):
number = models.IntegerField()
But when I call A.save(), I want to ensure that number is a prime (or other conditions), or the save instruction should be cancelled.
So how can I cancel the save instruction in the pre_save signal receiver?
#receiver(pre_save, sender=A)
def save_only_for_prime_number(sender, instance, *args, **kwargs):
# how can I cancel the save here?
See my another answer: https://stackoverflow.com/a/32431937/2544762
This case is normal, if we just want to prevent the save, throw an exception:
from django.db.models.signals import pre_save, post_save
#receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
# some case
if case_error:
raise Exception('OMG')
I'm not sure you can cancel the save only using the pre_save signal. But you can easily achieve this by overriding the save method:
def save(self):
if some_condition:
super(A, self).save()
else:
return # cancel the save
As mentioned by #Raptor, the caller won't know if the save was successful or not. If this is a requirement for you, take look at the other answer which forces the caller to deal with the "non-saving" case.
If the data's always coming from a Form and you have a straightforward test for whether or not the save should occur, send it through a validator. Note, though, that validators aren't called for save() calls originating on the backend. If you want those to be guarded as well, you can make a custom Field, say class PrimeNumberField(models.SmallIntegerField) If you run your test and raise an exception in the to_python() method of that custom field, it will prevent the save. You can also hook into the validation of a specific field by overriding any of several other methods on the Field, Form, or Model.

How to create a django User using DRF's ModelSerializer

In django, creating a User has a different and unique flow from the usual Model instance creation. You need to call create_user() which is a method of BaseUserManager.
Since django REST framework's flow is to do restore_object() and then save_object(), it's not possible to simply create Users using a ModelSerializer in a generic create API endpoint, without hacking you way through.
What would be a clean way to solve this? or at least get it working using django's built-in piping?
Edit:
Important to note that what's specifically not working is that once you try to authenticate the created user instance using django.contrib.auth.authenticate it fails if the instance was simply created using User.objects.create() and not .create_user().
Eventually I've overridden the serializer's restore_object method and made sure that the password being sent is then processes using instance.set_password(password), like so:
def restore_object(self, attrs, instance=None):
if not instance:
instance = super(RegisterationSerializer, self).restore_object(attrs, instance)
instance.set_password(attrs.get('password'))
return instance
Thanks everyone for help!
Another way to fix this is to overwrite pre_save(self, obj) method in your extension of viewsets.GenericViewSet like so:
def pre_save(self, obj):
""" We have to encode the password in the user object that will be
saved before saving it.
"""
viewsets.GenericViewSet.pre_save(self, obj)
# Password is raw right now, so set it properly (encoded password will
# overwrite the raw one then).
obj.user.set_password(obj.user.password)
Edit:
Note that the obj in the code above contains the instance of User class. If you use Django's user model class directly, replace obj.user with obj in the code (the last line in 2 places).
I'm working with DRF. And here is how I create users:
I have a Serializer with overrided save method:
def save(self, **kwargs ):
try:
user = create_new_user(self.init_data)
except UserDataValidationError as e:
raise FormValidationFailed(e.form)
self.object = user.user_profile
return self.object
create_new_user is just my function for user creation and in the view, I just have:
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
It seems like you should be overriding restore_object() in your serializer, not save(). This will allow you to create your object correctly.
However, it looks like you are trying to abuse the framework -- you are trying to make a single create() create two objects (the user and the profile). I am no DRF expert, but I suspect this may cause some problems.
You would probably do better by using a custom user model (which would also include the profile in the same object).

Categories