You can add a clean_fields()
method to the child model with extra validators.
Helpers:
from typing import Any, Callable, Collection, Optional
from typing_extensions import TypeAlias
from django.db import models
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
ValueValidator: TypeAlias = Callable[[Any], None]
FieldsValidatorsDict: TypeAlias = dict[str, list[ValueValidator]]
def run_validators_in_fields_validators_dict(
model_instance: models.Model, validators_dict: FieldsValidatorsDict, exclude: list[str] = None
) -> None:
"""
Run validators in fields-to-validators dict.
Useful for adding extra validation to the fields in child model that came from the parent.
(It's not possible to change `validators` of fields from base class)
Example:
```
class Animal(models.Model):
name = models.CharField(validators=[...])
class Dog(Animal):
_extra_fields_validators = {"name": [...]}
def clean_fields(self, exclude = None):
errors = {}
try:
run_validators_in_fields_validators_dict(self, self._extra_fields_validators, exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
try:
super().clean_fields(exclude=exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
if errors:
raise ValidationError(errors)
```
"""
if exclude is None:
exclude = []
errors = {}
for field_name, validators in validators_dict.items():
if field_name in exclude:
continue
try:
_run_validators_on_field(model_instance, field_name, validators)
except ValidationError as e:
errors[field_name] = e.error_list
if errors:
raise ValidationError(errors)
def _run_validators_on_field(model_instance: models.Model, field_name: str, validators: list[ValueValidator]) -> None:
field = model_instance.__class__._meta.get_field(field_name)
raw_value = getattr(model_instance, field.attname)
clean_value = field.to_python(raw_value)
errors = []
for validator in validators:
try:
validator(clean_value)
except ValidationError as e:
errors.extend(e.error_list)
if errors:
raise ValidationError(errors)
validate_str_no_underscore = RegexValidator(
regex=r"^[^_]*\Z",
message="Should not contain underscore",
)
Usage:
class Animal(models.Model):
name = models.CharField()
class Dog(Animal):
_extra_fields_validators: FieldsValidatorsDict = {"name": [validate_str_no_underscore]}
def clean_fields(self, exclude: Optional[Collection[str]] = None) -> None:
errors = {}
try:
run_validators_in_fields_validators_dict(self, self._extra_fields_validators, exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
try:
super().clean_fields(exclude=exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
if errors:
raise ValidationError(errors)