https://github.com/hapytex/django-enforced-choices
Enforce the list of possible values at the database level.
https://github.com/hapytex/django-enforced-choices
check-constraints database django django-models
Last synced: about 1 year ago
JSON representation
Enforce the list of possible values at the database level.
- Host: GitHub
- URL: https://github.com/hapytex/django-enforced-choices
- Owner: hapytex
- License: other
- Created: 2023-09-13T14:22:37.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2024-04-04T18:54:39.000Z (about 2 years ago)
- Last Synced: 2025-05-13T01:49:57.603Z (about 1 year ago)
- Topics: check-constraints, database, django, django-models
- Language: Python
- Homepage: https://pypi.org/project/django-enforced-choices/
- Size: 40 KB
- Stars: 8
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# Django-enforced-choices
[](https://pypi.python.org/pypi/django-enforced-choices/)
[](http://django-enforced-choices.readthedocs.io/?badge=latest)
[](https://pypi.python.org/pypi/django-enforced-choices/)
[](https://github.com/psf/black)
Django does *not* enforce choices at the model level: yes a `ModelForm` can validate these, but unfortunately people oftend define form fields themselves or even worse don't work with any `Form` or `Serializer` at all. So this means that if we define a model:
```
class Movie(models.Model):
genre = models.CharField(max_length=1, choices=[('d', 'drama'), ('h', 'horror')])
```
The database will *not* enforce that one can only pick `'d'` or `'h'` as genre. A simple solution might be to encapsulate the item in a separate model, and then the `FOREIGN KEY` constraint will take care of this:
```
class Genre(models.Model):
id = models.CharField(max_length=1, primary_key=True)
name = models.CharField(max_length=128)
class Movie(models.Model):
genre = models.ForeignKey(Genre, on_delete=models.PROTECT)
```
we can then fill the table with a data migration for example. This also makes some sense: a quote I once heared is that there should only be three constants in programming: zero, one and infinity, meaning that typically one should not restrict the number of genres.
But regardless, people often use the former model. This is good if we work with `Form`s, or `Serializer`s, but models don't (eagerly) validate data, and even if they did, the database does not know about the choices, and thus will happily accept `'s'` as value (for example for science fiction).
## What does this package provide?
This package provides two mixins:
- `ChoicesConstraintModelMixin` which checks on the `choices=` parameter of the fields of that model, and based on that, creates a constraint to enforce the choices at the datbase side; and
- `RangeConstraintModelMixin`, which checks the `validators=` parameter of the fields of that model, and based on that, creates a constraint to enforce that at the database side.
You can combine the mixins that are defined with `FullChoicesConstraintModelMixin`, it is probably advisable to use `FullChoicesConstraintModelMixin` over the mixins defined above, since as more validators are enforced ,
it will automatically add more constraints for these models, whereas `ChoicesConstraintModelMixin` for example, will only limit itself to choices.
One can exclude certain fields with the `exclude_choice_check_fields` and `exclude_range_check_fields` attributes that you can alter in the model. These need to provide a collection of strings that contain the *name* of the field.
Another option is to import the correspond field from the `django_enforced_choices.fields` module, or `django_enforced_choices.fields.postgres` for PostgreSQL-specific fields. This will, by default, also check if the fields have choices, but we do *not* recommend to use these, since this means the field has for example as type `ChoiceCharField`, and certain Django functionalities (and packages) sometimes check full type equality to determine a widget, not through an `instanceof`. This thus means that certain functionalities might no longer work as intended.
### Usage
One can import the `ChoicesConstraintModelMixin` and mix it into a model, like:
```
from django.core.validators import MaxValuevalidator, MinValueValidator
from django_enforced_choices.models import FullChoicesConstraintModelMixin
class Movie(FullChoicesConstraintModelMixin, models.Model):
genre = models.CharField(max_length=1, choices=[('d', 'drama'), ('h', 'horror')])
year = models.IntegerField(validators=[MinValueValidator(1888)])
```
this will then add `CheckConstraint`s to the model to enforce that `genre` only can contain `'d'` and `'h'` at the database side, and that the `year` is greater than or equal to [1888](https://en.wikipedia.org/wiki/Roundhay_Garden_Scene).
## How does the package work?
For the fields defined, it will check if the `choices` and `validators` are defined. If that is the case, it will create a `CheckConstraint` with `fieldname__in` with the keys in the choices for choices, and `__range`, `__lte` or `__gte` for ranges depending on what values are picked.
If the field is NULLable, it will also allow `NULL`/`None` to be used.