Skip to main content

A Django app that simplifies custom data validation for models

Project description

django-model-validation

PyPI - Version

django-model-validation simplifies custom data validation for Django models.

Quick Navigation

Features

  • Simple validator definition
  • Validators are automatically executed and errors accumulated
  • Adds a boolean property for validity checking that can optionally be cached in the database.
  • Numerous helper functions for filtering by validity

Installation

Requires Python version 3.9 or higher, Django version 3.2 or higher and pip.

pip install django-model-validation

Add 'django_model_validation' to your INSTALLED_APPS setting.

INSTALLED_APPS = [
    ...
    'django_model_validation',
]

Example

Replace the Django's Model base class by ValidatingModel and annotate a validation method with the @validator decorator.

from django.db import models
from django_model_validation.models import ValidatingModel, validator


class Person(ValidatingModel):
    date_of_birth = models.DateField()
    date_of_death = models.DateField(blank=True, null=True)
    is_alive = models.BooleanField(default=True)

    @validator
    def validate_biographical_data(self):
        if self.date_of_death is not None and self.is_alive:
            return "A date of death should not be set if the person is alive."

The validate_biographical_data function is now automatically executed when attempting to save the model from a form. In case of failure, it raises a ValidationError encapsulating the error message.

A model may have multiple custom validators. They all get executed, and if any of them raise errors, these, along with any other validation errors are combined into a single ValidationError.

Full Usage

Writing validators

For convenience, instead of raising a ValidationError, custom validators may return any of the following:

  • a string containing an error message (as in the example above)
  • a dictionary that maps field names to lists of strings, or ValidationError objects
  • a ValidationError
  • a list (or any iterable) containing any of the above types, even mixed, or
  • a boolean indicating validation success.

Validation is considered successful only if the method does not raise an exception and, either returns nothing or returns True.

Returning iterables is particularly useful, as it allows validators to be written as a generator functions, simplifying the code when multiple validations are performed in a single function.

Examples:

@validator
def validate_biographical_data(self):
    if self.date_of_death is not None:
        if self.is_alive:
            yield "A date of death should not be set if the person is alive."
        if self.date_of_death < self.date_of_birth:
            yield ValidationError("Date of death should not be before the date of birth.")
        if self.date_of_death > date.today():
            yield {"date_of_death": "Date of death should not be in the future."}


@validator
def validate_completeness(self):
    if self.date_of_birth > date.today():
        return {"date_of_death": "If the person is not alive, please provide a date of death."}

Triggering validations manually

Custom validators are triggered during Django's standard validation process (i.e., as part of the full_clean method). While this process is automatically invoked by model forms, it is not automatic when manually creating or updating an object using, e.g., save(). In this case, it's necessary to call full_clean manually before saving.

To manually execute only the custom validators, call run_custom_validators() instead of full_clean().

To manually execute a specific custom validator only, simply execute the decorated method. If the validation fails, the decorator ensures the method raises a ValidationError containing all returned errors (if any).

Additionally, the decorator introduces some convenience functions to the method object:

person = Person(...)

person.validate_completeness.get_validation_error()  # Returns the ValidationError instead of raising it.
person.validate_completeness.is_valid()  # Runs the validation and returns a bool indicating validation success.

Boolean validity checking can be simplified by specifying the property_name option:

@validator(property_name='is_data_complete')
def validate_completeness(self):
    if self.date_of_birth > date.today():
        return {"date_of_death": "If the person is not alive, please provide a date of death."}

This sets up a model property that, when accessed, internally runs the is_valid method of the validator, allowing easy access to validity like this:

if person.is_data_complete:
    ...

Enable validity caching

To enable database caching for the validity property, set the cache option to True in the @validator decorator. This creates a hidden boolean field on the model, storing the result of the validity check whenever the model is saved.

@validator(cache=True, property_name='is_data_complete')
def validate_completeness(self):
    ...

With this configuration, subsequent access to the property or the is_valid() method won't trigger the validation but instead return the cached result.

Caveats:

  • If attributes are modified but the object is not saved, the validity cache won't update, potentially leading to an outdated result.
  • When adding a validator with cache=True or modifying the cache option of an existing one, a database migration is necessary. This can be created automatically with makemigrations.
  • The cache field is initially empty. Refer to the "Cache Maintenance" section on how to automatically populate it upon creation.
  • It is advisable to set the cache option together with the property_name option. When specified, the property name serves as the field name. If no property name is set, a field name is generated automatically, but accessing the cache is then only possible using the is_valid() method.

Analyse data validity

The validity cache can be used to filter querysets by validity according to specific or all validators. Since the property name of a cached validator is a model field, it can simply be used to filter querysets. For added convenience, several utility functions are available through the validator method when it's accessed through the model class.

# Obtain filtered querysets
Person.validate_completeness.get_valid_objects()
Person.validate_completeness.get_invalid_objects()

# Retrieve a Q object for use in queryset filtering
Person.validate_completeness.get_is_valid_condition()

# Check if all objects are valid
Person.validate_completeness.is_all_valid()

Furthermore, the model class offers additional tools for analysing data validity across all cached validators simultaneously.

# Custom pre-filtered managers
Person.valid_objects.all()
Person.invalid_objects.all()

# Retrieve a Q object for use in queryset filtering
Person.get_custom_validity_condition()

# Checks if all custom validation passes for all objects
Person.check_custom_validators_globally()

Cache maintenance

While the library handles caching automatically in most scenarios, here are instances where manual examination and manipulation of the cache are helpful. The following provides a brief overview of available functions.

person = Person(...)

# For a specific validator
person.validate_completeness.update_cache()
person.validate_completeness.clear_cache()
is_cached = person.validate_completeness.is_cached()
cached_result = person.validate_completeness.get_cache()

# For all custom validators at once
person.update_validator_caches()
person.clear_validator_caches()
person.are_validation_results_cached()

# For a specific validator and all objects in the database at once
Person.validate_completeness.update_cache()
Person.validate_completeness.clear_cache()
Person.validate_completeness.is_all_cached()
Person.validate_completeness.get_is_cached_condition()

# For all custom validators and all objects in the database at once
Person.update_validator_caches_globally()
Person.clear_validator_caches_globally()
Person.are_validation_results_cached_globally()
Person.get_are_validation_results_cached_condition()

Additionally, the cache of a specific validator (or all validators) can be updated within a migration. This proves especially useful for initial population of a cache of a validator that is added to an existing model.

To do this, simply append the UpdateModelValidatorCache to the end of the operations list that adds the validation cache field, listing the validators for which the cache should be updated.

operations = [
    ...,
    UpdateModelValidatorCache('MyModel', MyModel.validate_something, MyModel.validate_something_else)
]

Caution: This may call migrations to fail when validator code undergoes changes at a later stage. For instance, introducing new fields and modifying an existing validator to accommodate them will result in migration failures, as the validator code cannot be executed on previous model versions. If faced with such issues, consider removing obsolete UpdateModelValidatorCache operations from old migrations. Alternatively, consider manually populating the cache using RunPython.

Disable automatic validation

To accept invalid data by default, set the auto option to False. This is particularly useful for cached validators used to track data quality.

@validator(auto=False)
def validate_completeness(self):
    ...

Full configuration

All keyword arguments of the @validator decorator:

  • auto (bool): If True, the validator will automatically run as part of the model's full_clean method. Defaults to True.
  • cache (bool): If True, a ModelValidatorCacheField will be implicitly added to the model which will be used to store the validation result. Defaults to False.
  • auto_use_cache (bool): If True, calling the is_valid() method or the validity property will automatically use the cached validation result if available. Defaults to True.
  • auto_update_cache (bool): If True, the validator will automatically be executed and the cache updated accordingly when the model is saved using the save() method. Defaults to True.
  • property_name (str, optional): Custom name for the boolean validity property associated with this validator. If the cache is enabled, this will be used as the name of the cache field, otherwise a name will be constructed. If the cache is disabled, this will be used as the name of a property of the model that acts as a proxy to the is_valid() method of this validator.
  • property_verbose_name (str, optional): If set, this will be used as verbose_name` for the cache field as well as for generic error messages.

License

django-model-validation is distributed under the terms of the MIT License.

Project details


Release history Release notifications | RSS feed

This version

0.1

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_model_validation-0.1.tar.gz (15.6 kB view hashes)

Uploaded Source

Built Distribution

django_model_validation-0.1-py3-none-any.whl (15.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page