{"id":22927196,"url":"https://github.com/hapytex/django-enforced-choices","last_synced_at":"2025-05-13T01:50:04.716Z","repository":{"id":195172981,"uuid":"691113344","full_name":"hapytex/django-enforced-choices","owner":"hapytex","description":"Enforce the list of possible values at the database level.","archived":false,"fork":false,"pushed_at":"2024-04-04T18:54:39.000Z","size":41,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-13T01:49:57.603Z","etag":null,"topics":["check-constraints","database","django","django-models"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/django-enforced-choices/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hapytex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null},"funding":{"custom":"https://www.buymeacoffee.com/hapytex"}},"created_at":"2023-09-13T14:22:37.000Z","updated_at":"2025-01-12T17:32:51.000Z","dependencies_parsed_at":"2024-04-04T12:45:18.126Z","dependency_job_id":null,"html_url":"https://github.com/hapytex/django-enforced-choices","commit_stats":null,"previous_names":["hapytex/django-enforced-choices"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hapytex%2Fdjango-enforced-choices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hapytex%2Fdjango-enforced-choices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hapytex%2Fdjango-enforced-choices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hapytex%2Fdjango-enforced-choices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hapytex","download_url":"https://codeload.github.com/hapytex/django-enforced-choices/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253856616,"owners_count":21974576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["check-constraints","database","django","django-models"],"created_at":"2024-12-14T09:13:54.144Z","updated_at":"2025-05-13T01:50:04.690Z","avatar_url":"https://github.com/hapytex.png","language":"Python","funding_links":["https://www.buymeacoffee.com/hapytex"],"categories":[],"sub_categories":[],"readme":"# Django-enforced-choices\n\n[![PyPi version](https://badgen.net/pypi/v/django-enforced-choices/)](https://pypi.python.org/pypi/django-enforced-choices/)\n[![Documentation Status](https://readthedocs.org/projects/django-enforced-choices/badge/?version=latest)](http://django-enforced-choices.readthedocs.io/?badge=latest)\n[![PyPi license](https://badgen.net/pypi/license/django-enforced-choices/)](https://pypi.python.org/pypi/django-enforced-choices/)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\nDjango 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:\n\n```\nclass Movie(models.Model):\n    genre = models.CharField(max_length=1, choices=[('d', 'drama'), ('h', 'horror')])\n```\n\nThe 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:\n\n```\nclass Genre(models.Model):\n  id = models.CharField(max_length=1, primary_key=True)\n  name = models.CharField(max_length=128)\n\n\nclass Movie(models.Model):\n  genre = models.ForeignKey(Genre, on_delete=models.PROTECT)\n```\n\nwe 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.\n\nBut 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).\n\n## What does this package provide?\n\nThis package provides two mixins:\n\n - `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\n - `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.\n\nYou 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 ,\nit will automatically add more constraints for these models, whereas `ChoicesConstraintModelMixin` for example, will only limit itself to choices.\n\nOne 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.\n\nAnother 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.\n\n### Usage\n\nOne can import the `ChoicesConstraintModelMixin` and mix it into a model, like:\n\n```\nfrom django.core.validators import MaxValuevalidator, MinValueValidator \nfrom django_enforced_choices.models import FullChoicesConstraintModelMixin\n\nclass Movie(FullChoicesConstraintModelMixin, models.Model):\n    genre = models.CharField(max_length=1, choices=[('d', 'drama'), ('h', 'horror')])\n    year = models.IntegerField(validators=[MinValueValidator(1888)])\n```\n\nthis 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).\n\n## How does the package work?\n\nFor 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.\n\nIf the field is NULLable, it will also allow `NULL`/`None` to be used.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhapytex%2Fdjango-enforced-choices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhapytex%2Fdjango-enforced-choices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhapytex%2Fdjango-enforced-choices/lists"}