Hey there!
I have a situation that i want to share here, and maybe get some additional toughts on it.
The models involved:
from django.db import models
class PointRule(models.Model):
name = models.CharField(
verbose_name=_("Rule name"),
help_text=_("This name is going to be used only for internal reference"),
max_length=128,
)
starts_at = models.DateField(
verbose_name=_("Starts at"),
help_text=_("The first day (inclusive) that this rule will be on effect"),
)
ends_at = models.DateField(
verbose_name=_("Ends at"),
help_text=_("The last day (inclusive) that this rule will be on effect"),
)
# Other fields
def clean(self):
if self.starts_at > self.ends_at:
raise ValidationError(
{"ends_at": _("The ends at date must be greater or equal than the starts at")}
)
Pretty straight forward validation here, i want to assert that the start date is greater thant the end date. Assuming that this fields are required (non-blank, non-nullable), from my point of view my clean
method shouldnt have to worry about starts_at
or ends_at
being None
or any other non-date value.
That’s not the case though, if i go the shell and pass this scenario a TypeError is raised because one or both values are None:
from some_app.models import PointRule
r = PointRule(name="foo", starts_at=None, ends_at=None)
r.full_clean()
TypeError: '>' not supported between instances of 'NoneType' and 'NoneType'
I wonder if there’s any benefits of adding a mechanism to prevent that the clean method is called when we know we have errors on fields already.
That would be a good case on this example, on the full_clean method, django called clean_fields
and we had some errors populated from both of the fields (starts_at and ends_at). So my clean method wouldn’t need to be worried about it.
Of course, right now this can be done on the clean
method to prevent the TypeError
for being raised, but it looks i bit wacky:
# PointRule model
def clean(self):
if self.starts_at is None or self.ends_at is None:
return
# Validation continues, asserted that the values aren't None
This works great for a few fields, but when the clean
method contains more fields to validate it makes not really a great experience.
Of course one can go and create a “BaseModel”, override the full_clean
method to prevent that the clean method is not called when there’s any errors on the validation, looking like:
# I removed some comments to better describe where the changes can be done
from django.db import models
class BaseModel(models.Model):
def full_clean(self, exclude=None, validate_unique=True, validate_constraints=True):
errors = {}
if exclude is None:
exclude = set()
else:
exclude = set(exclude)
try:
self.clean_fields(exclude=exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
try:
################################
if not errors:
self.clean()
###############################
except ValidationError as e:
Right, now if we inherit from the BaseModel, we can assert that the clean method would only be called when there’s no errors, and in fact a ValidationError is raised, and we can remove that if statement we added:
class PointRule(BaseModel): # <<<< Inherit the base model
name = models.CharField(
verbose_name=_("Rule name"),
help_text=_("This name is going to be used only for internal reference"),
max_length=128,
)
starts_at = models.DateField(
verbose_name=_("Starts at"),
help_text=_("The first day (inclusive) that this rule will be on effect"),
)
ends_at = models.DateField(
verbose_name=_("Ends at"),
help_text=_("The last day (inclusive) that this rule will be on effect"),
)
# Other fields
def clean(self):
# Our clean method doesnt need to worry about bad data
if self.starts_at > self.ends_at:
raise ValidationError(
{"ends_at": _("The ends at date must be greater or equal than the starts at")}
)
Then on the shell again:
from some_app.models import PointRule
r = PointRule(name="foo", starts_at=None, ends_at=None)
r.full_clean()
django.core.exceptions.ValidationError: {'starts_at': ['This field cannot be null.'], 'ends_at': ['This field cannot be null.']}
Looks great! Sadly it doesn’t work with ModelForms (and the Admin), because on the ModelForm validation the fields would be validated on the form, and if there’s any errors already on that fields, the full_clean
method would receive a exclude
parameter, and consequently the errors
variable inside the full_clean
method would have no errors and we go back to the same. (Of course we could check the exclude
parameter on full_clean
and not call clean
if there’s any fields excluded, but then we come to a point of ambiguity: Is there any errors or should a field just be ignored?)
I wonder if there’s any point on adding a new parameter to the full_clean
method: “errors” for instance. This errors could receive the errors from the ModelForm that were validating.
Are your toughts on this?
Cheers