├── djchoices ├── tests │ ├── __init__.py │ ├── test_private_api.py │ ├── test_native_choices.py │ └── test_choices.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── generate_native_django_choices.py ├── __init__.py └── choices.py ├── MANIFEST.in ├── .coveragerc ├── .gitignore ├── .readthedocs.yaml ├── setup.cfg ├── CONTRIBUTORS.md ├── .github └── workflows │ ├── code_quality.yml │ └── ci.yml ├── runtests.py ├── tox.ini ├── LICENSE ├── setup.py ├── docs ├── contributing.rst ├── migrating.rst ├── index.rst ├── choices.rst ├── Makefile ├── make.bat └── conf.py ├── CONTRIBUTING.md ├── README.rst └── Changelog.rst /djchoices/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djchoices/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | 3 | -------------------------------------------------------------------------------- /djchoices/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = djchoices 4 | 5 | [report] 6 | omit = */tests/* 7 | exclude_lines = 8 | pragma: no cover 9 | 10 | [html] 11 | directory = cover 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *egg-info 4 | .eggs/ 5 | *.pyc 6 | .project 7 | .pydevproject 8 | .tox 9 | 10 | # coverage 11 | htmlcov 12 | .coverage 13 | 14 | # docs 15 | docs/_build 16 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | sphinx: 8 | configuration: docs/conf.py 9 | 10 | build: 11 | os: 'ubuntu-20.04' 12 | tools: 13 | python: '3.8' 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | combine_as_imports = true 3 | default_section = THIRDPARTY 4 | include_trailing_comma = true 5 | line_length = 88 6 | multi_line_output = 3 7 | skip = .tox,env 8 | known_django=django 9 | known_first_party=djchoices 10 | sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 11 | -------------------------------------------------------------------------------- /djchoices/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from pkg_resources import get_distribution 4 | 5 | from djchoices.choices import C, ChoiceItem, DjangoChoices 6 | 7 | __version__ = get_distribution("django-choices").version 8 | 9 | __all__ = ["ChoiceItem", "DjangoChoices", "C"] 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Many thanks to the various contributors: 2 | 3 | * https://github.com/bigjason (author) 4 | * https://github.com/crisisking 5 | * https://github.com/dokterbob 6 | * https://github.com/ebertti 7 | * https://github.com/eliostvs 8 | * https://github.com/JostCrow 9 | * [Sergei Maertens](https://github.com/sergei-maertens) (sergeimaertens@gmail.com, maintainer) 10 | * https://github.com/soulne4ny 11 | * https://github.com/vaad2 12 | * https://github.com/WoLpH 13 | * https://github.com/ashwch 14 | * [Alexander Kavanaugh](https://github.com/kavdev) 15 | * https://github.com/awais786 16 | * [Irtaza Akram](https://github.com/irtazaakram) 17 | -------------------------------------------------------------------------------- /djchoices/tests/test_private_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from djchoices import C, ChoiceItem, DjangoChoices # noqa 4 | 5 | 6 | class PrivateAPITests(unittest.TestCase): 7 | def test_labels_dict(self): 8 | """ 9 | Ref #33: IPython reads the Labels.keys() for pretty printing. 10 | """ 11 | 12 | class Choices(DjangoChoices): 13 | a = ChoiceItem("a", "A") 14 | b = ChoiceItem("b", "B") 15 | 16 | self.assertEqual(Choices.labels.a, "A") 17 | self.assertEqual(Choices.labels.b, "B") 18 | with self.assertRaises(AttributeError): 19 | Choices.labels.c 20 | 21 | keys = Choices.labels.keys() 22 | self.assertEqual(set(keys), set(["a", "b"])) 23 | -------------------------------------------------------------------------------- /.github/workflows/code_quality.yml: -------------------------------------------------------------------------------- 1 | name: Code quality checks 2 | 3 | # Run this workflow every time a new commit pushed to your repository 4 | on: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - '*' 10 | pull_request: 11 | workflow_dispatch: 12 | 13 | jobs: 14 | linting: 15 | name: Code-quality checks 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | toxenv: [isort, black, docs] 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.8' 25 | - name: Install dependencies 26 | run: pip install tox 27 | - run: tox 28 | env: 29 | TOXENV: ${{ matrix.toxenv }} 30 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from os import path 3 | from sys import stdout 4 | 5 | from django.conf import settings 6 | 7 | 8 | def get_suite(): 9 | disc_folder = path.abspath(path.dirname(__file__)) 10 | 11 | settings.configure(SECRET_KEY="dummy") 12 | 13 | stdout.write("Discovering tests in '%s'..." % disc_folder) 14 | suite = unittest.TestSuite() 15 | loader = unittest.loader.defaultTestLoader 16 | suite.addTest(loader.discover(disc_folder, pattern="test*.py")) 17 | stdout.write("Done.\n") 18 | return suite 19 | 20 | 21 | def run_tests(): 22 | suite = get_suite() 23 | stdout.write("Running tests...\n") 24 | runner = unittest.TextTestRunner() 25 | runner.verbosity = 2 26 | runner.run(suite.run) 27 | 28 | 29 | if __name__ == "__main__": 30 | run_tests() 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{38,39,310}-django{32,41,42} 4 | py310-django{41,42} 5 | docs 6 | black 7 | isort 8 | skip_missing_interpreters = true 9 | 10 | [gh-actions] 11 | python = 12 | 3.8: py38 13 | 3.9: py39 14 | 3.10: py310 15 | 3.11: py311 16 | 17 | [gh-actions:env] 18 | DJANGO = 19 | 3.2: django30 20 | 4.1: django41 21 | 4.2: django42 22 | 23 | [testenv] 24 | deps= 25 | django32: Django~=3.2.0 26 | django41: Django~=4.1.0 27 | django42: Django~=4.2.0 28 | coverage 29 | commands= 30 | coverage run \ 31 | --rcfile={toxinidir}/.coveragerc \ 32 | {toxinidir}/runtests.py 33 | 34 | [testenv:docs] 35 | basepython=python 36 | changedir=docs 37 | skipsdist=true 38 | deps= 39 | sphinx 40 | sphinx_rtd_theme 41 | commands= 42 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 43 | 44 | [testenv:isort] 45 | deps = isort 46 | skipsdist = True 47 | commands = isort --check-only --diff djchoices 48 | 49 | [testenv:black] 50 | deps = black 51 | skipsdist = True 52 | commands = black --check djchoices docs setup.py runtests.py 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2015 Jason Webb 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /djchoices/tests/test_native_choices.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.db.models import TextChoices 4 | 5 | from djchoices import ChoiceItem, DjangoChoices 6 | 7 | 8 | class ToMigrate(DjangoChoices): 9 | option_1 = ChoiceItem("option_1") 10 | option_2 = ChoiceItem("option_2", "Option 2") 11 | 12 | 13 | class Native(TextChoices): 14 | option_1 = "option_1", "option 1" 15 | option_2 = "option_2", "Option 2" 16 | 17 | 18 | class NativeChoicesEquivalenceTests(unittest.TestCase): 19 | def test_labels(self): 20 | labels = ToMigrate.labels 21 | native = dict(zip(Native.names, Native.labels)) 22 | 23 | self.assertEqual(native, labels) 24 | 25 | def test_values(self): 26 | values = ToMigrate.values 27 | native = dict(zip(Native.values, Native.labels)) 28 | 29 | self.assertEqual(native, values) 30 | 31 | def test_attributes(self): 32 | attributes = ToMigrate.attributes 33 | native = dict(zip(Native.values, Native.names)) 34 | 35 | self.assertEqual(native, attributes) 36 | 37 | def test_get_choice(self): 38 | a_choice = ToMigrate.get_choice(ToMigrate.option_2) 39 | native = Native[Native.option_2] 40 | 41 | self.assertEqual(native.value, a_choice.value) 42 | self.assertEqual(native.label, a_choice.label) 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open(path.join(path.dirname(__file__), "README.rst")) as f: 6 | readme = f.read() 7 | 8 | setup( 9 | name="django-choices", 10 | version="2.0.0", 11 | license="MIT", 12 | description="Sanity for the django choices functionality.", 13 | long_description=readme, 14 | install_requires=["Django>=3.2"], 15 | url="https://github.com/bigjason/django-choices", 16 | author="Jason Webb", 17 | author_email="bigjasonwebb@gmail.com,sergeimaertens@gmail.com", 18 | packages=find_packages(), 19 | include_package_data=True, 20 | classifiers=[ 21 | "Development Status :: 5 - Production/Stable", 22 | "Operating System :: OS Independent", 23 | "License :: OSI Approved :: MIT License", 24 | "Intended Audience :: Developers", 25 | "Framework :: Django", 26 | "Framework :: Django :: 3.2", 27 | "Framework :: Django :: 4.1", 28 | "Framework :: Django :: 4.2", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: Implementation :: PyPy", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | Contributing 4 | ============ 5 | 6 | To get up and running quickly, fork the github repository and make all 7 | your changes in your local clone. 8 | 9 | Git-flow is prefered as git workflow, but as long as you make pull requests 10 | against the `develop` branch, all should be well. Pull requests should 11 | always have tests, and if relevant, documentation updates. 12 | 13 | Feel free to create unfinished pull-requests to get the tests to build 14 | and get work going, someone else might always want to pick up the tests 15 | and/or documentation. 16 | 17 | 18 | Testing 19 | ------- 20 | For testing the standard unittest2 library is used, contrary to Django's 21 | test framework. The reason is simple: speed. Django choices doesn't touch 22 | the database in any way, so the standard library unit tests are fine. 23 | 24 | To run the tests in your (virtual) environment, simple execute 25 | 26 | .. code-block:: sh 27 | 28 | python runtests.py 29 | 30 | This will run the tests with the current python version and Django version. 31 | 32 | To run the tests on all supported python/Django versions, use tox_. 33 | 34 | .. code-block:: sh 35 | 36 | pip install tox 37 | tox 38 | 39 | If you want to speed this up, you can also use detox_. This library will 40 | run as much in parallel as possible. 41 | 42 | 43 | Documentation 44 | ------------- 45 | 46 | The documentation is built with Sphinx. Run `make` to build the documentation: 47 | 48 | .. code-block: sh 49 | 50 | cd docs/ 51 | make html 52 | 53 | You can now open `_build/index.html`. 54 | 55 | 56 | Coding style 57 | ------------ 58 | Please stick to PEP8, and use pylint or similar tools to check the code style. 59 | 60 | 61 | .. _tox: https://testrun.org/tox/latest/ 62 | .. _detox: https://pypi.python.org/pypi/detox/ 63 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run CI 2 | 3 | # Run this workflow every time a new commit pushed to your repository 4 | on: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - '*' 10 | pull_request: 11 | workflow_dispatch: 12 | 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python: 19 | - '3.8' 20 | - '3.9' 21 | - '3.10' 22 | - '3.11' 23 | django: 24 | - '3.2' 25 | - '4.1' 26 | - '4.2' 27 | exclude: 28 | - python: '3.11' 29 | django: '3.2' 30 | 31 | name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }}) 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-python@v2 36 | with: 37 | python-version: ${{ matrix.python }} 38 | 39 | - name: Install dependencies 40 | run: pip install tox tox-gh-actions 41 | 42 | - name: Run tests 43 | run: tox 44 | env: 45 | PYTHON_VERSION: ${{ matrix.python }} 46 | DJANGO: ${{ matrix.django }} 47 | 48 | - name: Publish coverage report 49 | uses: codecov/codecov-action@v3 50 | 51 | # publish: 52 | # name: Publish package to PyPI 53 | # runs-on: ubuntu-latest 54 | # needs: tests 55 | 56 | # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 57 | 58 | # steps: 59 | # - uses: actions/checkout@v2 60 | # - uses: actions/setup-python@v2 61 | # with: 62 | # python-version: '3.8' 63 | 64 | # - name: Build sdist and wheel 65 | # run: | 66 | # pip install pip setuptools wheel --upgrade 67 | # python setup.py sdist bdist_wheel 68 | 69 | # - name: Publish a Python distribution to PyPI 70 | # uses: pypa/gh-action-pypi-publish@v1.4.1 71 | # with: 72 | # user: __token__ 73 | # password: ${{ secrets.PYPI_TOKEN }} 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Open source projects shine when anyone can contribute, and this project is no different. However, there are some guidelines 4 | to adhere to in order to get your contribution merged in. In general, if you adhere to the Django contributing 5 | guidelines, all is well: https://docs.djangoproject.com/en/dev/internals/contributing/ 6 | 7 | First, fork the repository and then clone it: 8 | 9 | git glone git@github.com:your-username/django-choices.git 10 | 11 | Make sure you have Django installed (a virtualenv is recommended), and the test dependencies: 12 | 13 | pip install Django tox 14 | 15 | ## Tests 16 | 17 | All changes should be accompanied by a test that either tests the new behaviour, or tests the regression. 18 | Make sure all the tests still pass after your changes - for your current Django and Python version this 19 | can be done by running: 20 | 21 | python runtests.py 22 | 23 | And to run the entire matrix of Django and tox versions: 24 | 25 | tox 26 | 27 | ## Documentation 28 | 29 | When behaviour changes or gets added, check whether the documentation needs updates. If so, please 30 | submit a draft or final version. 31 | 32 | ## Pull requests 33 | 34 | When you think the patch is ready, submit a pull request to the `develop` branch. If it's a bug fix, 35 | the maintainer(s) will take care of bumping the version and uploading to PyPI. Feel free to add 36 | yourself to the CONTRIBUTORS.md file. 37 | 38 | ## Smaller style guidelines 39 | 40 | ### Commit(s) 41 | 42 | Try to keep commits as atomic as possible. It's fine to do many small commits before submitting the PR, 43 | you can always rebase your branch to make a nice commit history. A commit that adds the test, and 44 | then a different commit that fixes the issue/feature is reasonable, combining them is fine as well. 45 | 46 | ### Code style 47 | 48 | * Stick to PEP8, with the exclusion of the 80-char max line length. 80 columns is a guideline, 120 is the 49 | upper limit. 50 | * Use 4 spaces instead of tabs. 51 | * Follow https://docs.djangoproject.com/en/1.9/internals/contributing/writing-code/coding-style/ 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Django-Choices 3 | ============== 4 | 5 | |build-status| |code-quality| |coverage| |docs| |black| |pypi| |python-versions| |django-versions| 6 | 7 | Order and sanity for django model choices. 8 | ------------------------------------------ 9 | 10 | **DISCLAIMER** 11 | 12 | New projects should not use this package. Existing users can follow the migration guide 13 | in the `documentation`_. 14 | 15 | **Note:** Django 3.0 added `enumeration types `__. 16 | This feature mostly replaces the need for Django-Choices. 17 | See also `Adam Johnson's post on using them `__. 18 | 19 | **Introduction** 20 | 21 | Django choices provides a declarative way of using the choices_ option on django_ 22 | fields. 23 | 24 | See the `documentation`_ on ReadTheDocs on how to use this library. 25 | 26 | ------- 27 | License 28 | ------- 29 | 30 | Licensed under the `MIT License`_. 31 | 32 | ----------- 33 | Source Code 34 | ----------- 35 | 36 | The source code can be found on github_. 37 | 38 | .. |build-status| image:: https://github.com/bigjason/django-choices/actions/workflows/ci.yml/badge.svg 39 | :alt: Build status 40 | :target: https://github.com/bigjason/django-choices/actions/workflows/ci.yml 41 | 42 | .. |code-quality| image:: https://github.com/bigjason/django-choices/actions//workflows/code_quality.yml/badge.svg 43 | :alt: Code quality checks 44 | :target: https://github.com/bigjason/django-choices/actions//workflows/code_quality.yml 45 | 46 | .. |coverage| image:: https://codecov.io/gh/bigjason/django-choices/branch/master/graph/badge.svg?token=pcbBUCju0B 47 | :alt: Code coverage 48 | :target: https://codecov.io/gh/bigjason/django-choices 49 | 50 | .. |docs| image:: https://readthedocs.org/projects/django-choices/badge/?version=latest 51 | :target: http://django-choices.readthedocs.io/en/latest/ 52 | :alt: Documentation Status 53 | 54 | .. |pypi| image:: https://img.shields.io/pypi/v/django-choices.svg 55 | :target: https://pypi.python.org/pypi/django-choices 56 | 57 | .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-choices.svg 58 | 59 | .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-choices.svg 60 | 61 | .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 62 | :target: https://github.com/psf/black 63 | 64 | .. _choices: https://docs.djangoproject.com/en/stable/ref/models/fields/#choices 65 | .. _MIT License: https://en.wikipedia.org/wiki/MIT_License 66 | .. _django: https://www.djangoproject.com/ 67 | .. _github: https://github.com/bigjason/django-choices 68 | .. _PyPi: https://pypi.org/project/django-choices/ 69 | .. _documentation: https://django-choices.readthedocs.io/en/latest/ 70 | -------------------------------------------------------------------------------- /djchoices/management/commands/generate_native_django_choices.py: -------------------------------------------------------------------------------- 1 | from textwrap import indent 2 | 3 | from django.core.management.base import BaseCommand, no_translations 4 | 5 | from djchoices import DjangoChoices 6 | 7 | 8 | def get_subclasses(cls): 9 | for subclass in cls.__subclasses__(): 10 | yield from get_subclasses(subclass) 11 | yield subclass 12 | 13 | 14 | def iter_django_choices(): 15 | for cls in get_subclasses(DjangoChoices): 16 | yield cls 17 | 18 | 19 | BASE_CLS = { 20 | str: "models.TextChoices", 21 | int: "models.IntegerChoices", 22 | } 23 | 24 | 25 | class Command(BaseCommand): 26 | help = "Introspect DjangoChoices subclasses and generate equivalent native Django choices code." 27 | 28 | def add_arguments(self, parser): 29 | parser.add_argument( 30 | "--no-wrap-gettext", 31 | dest="wrap_gettext", 32 | action="store_false", 33 | help="Do not wrap labels in gettext/gettext_lazy calls. See also --gettext-alias.", 34 | ) 35 | parser.add_argument( 36 | "--gettext-alias", 37 | default="_", 38 | help="Alias/function name for the gettext(_lazy) wrapper. Defaults to '_'.", 39 | ) 40 | 41 | @no_translations 42 | def handle(self, **options): 43 | wrap_gettext = options["wrap_gettext"] 44 | gettext_alias = options["gettext_alias"] 45 | 46 | def wrap_label(label): 47 | if not wrap_gettext: 48 | return label 49 | return f"{gettext_alias}({label})" 50 | 51 | for cls in iter_django_choices(): 52 | full_path = f"{cls.__module__}.{cls.__qualname__}" 53 | 54 | value_types = set( 55 | [type(choice_item.value) for choice_item in cls._fields.values()] 56 | ) 57 | if len(value_types) != 1: 58 | self.stdout.write( 59 | f" Choices do not have consistent value types: {value_types}" 60 | ) 61 | continue 62 | 63 | value_type = list(value_types)[0] 64 | base = BASE_CLS.get(value_type) 65 | if not base: 66 | self.stdout.write( 67 | indent( 68 | f"No builtin enum for type '{value_type}'. You may need to define your own." 69 | "See: https://docs.djangoproject.com/en/3.2/ref/models/fields/#enumeration-types", 70 | prefix=" ", 71 | ) 72 | ) 73 | continue 74 | 75 | lines = [ 76 | f"# {full_path}", 77 | f"class {cls.__name__}({base}):", 78 | ] 79 | for field_name, choice_item in cls._fields.items(): 80 | label = wrap_label(f'"{choice_item.label}"') 81 | lines.append( 82 | indent( 83 | f"{field_name} = {repr(choice_item.value)}, {label}", 84 | prefix=" ", 85 | ) 86 | ) 87 | snippet = indent("\n".join(lines), prefix=" ") 88 | self.stdout.write("\n") 89 | self.stdout.write(snippet) 90 | self.stdout.write("\n") 91 | -------------------------------------------------------------------------------- /Changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 2.0.0 (2023-07-24) 6 | ------------------ 7 | 8 | This will (likely) be the final release, ever. Django has been shipping native choices 9 | support since version 3.0, so we are sunsetting this library. Many thanks to every 10 | contributor in any matter - documentation, issues, ideas, patches... You have all 11 | made valuable contribution. 12 | 13 | That said, this final (hopefully) release aims to assist in taking those last hurdles. 14 | We've provide some tooling to help you migrate (see the "migrating" documentation) and 15 | updated the project to work on supported Django and Python versions. 16 | 17 | Breaking changes 18 | ---------------- 19 | 20 | * Drop support for Django 1.11, 2.2, 3.0, 3.1 (EOL) 21 | * Drop support for Python 2.7, 3.5, 3.6, 3.7 (EOL) 22 | 23 | Older Django versions and Python 3.6+ likely still work, but they are not tested in CI 24 | anymore. 25 | 26 | Other changes 27 | ------------- 28 | 29 | * Confirm support for Django 4.1 & 4.2 30 | * Confirm support for Python 3.10 and 3.11 31 | * Added a management command to generate equivalent native django choice definitions 32 | * Documented how to migrate to native choices 33 | * Documented the (approximately) equivalent usage of Django Choices/native choices. 34 | 35 | 1.7.2 (2021-07-12) 36 | ------------------ 37 | 38 | CI maintenance release. There are no actual Python code changes in this release. 39 | 40 | * Added explicit support for Django 3.1 and 3.2 41 | * Added explicit support for Python 3.8 and 3.9 42 | * Migrated from Travis CI to Github Actions 43 | * Added Black as code formatter 44 | * Dropped Python 3.4 support since it's not available on Github Actions. 45 | 46 | 1.7.1 (2019-12-08) 47 | ------------------ 48 | 49 | * Added support for Django 3.0 50 | * Added dependency on non-vendored six 51 | 52 | 1.7.0 (2019-05-02) 53 | ------------------ 54 | 55 | * Added ``DjangoChoices.get_order_expression`` class method 56 | * Added support for Python 3.7 and Django 2.2 57 | * Dropped support for Django 1.8, Django 1.9 and Django 1.10 58 | 59 | 1.6.2 (2019-01-24) 60 | ------------------ 61 | 62 | * documentation code blocks are now syntax highlighted (@bashu in #55) 63 | * ``DjangoChoices`` subclasses are now directly iterable (yielding the choice 64 | tuples) (@brianjbuck in #53) 65 | * documentation of ``DjangoChoices.labels`` is fixed (#54) 66 | * typo fixed in docs (@nielznl in #57) 67 | 68 | 1.6.0 69 | ----- 70 | 71 | * Added support for custom attributes to ``ChoiceItem``. 72 | * Added ``DjangoChoices.get_choice`` as public API to retrieve a ``ChoiceItem`` 73 | instance. 74 | 75 | See the docs for example usage. 76 | 77 | 1.5.1 78 | ----- 79 | 80 | * Fixed inability to set custom order to 0 (#50), thanks to @kavdev for the 81 | patch and @robinramael for the report 82 | * Added API to get the attribute name from a value (#48), thanks to @jaseemabid 83 | for the report 84 | 85 | 1.5.0 86 | ----- 87 | 88 | * Dropped support for old Python/Django versions. 89 | * Added support for ``NullBooleanField`` -- thanks to @ashwch 90 | * Added retention of choices order in ``DjangoChoices.values`` -- thanks to @merwok 91 | 92 | .. warning:: 93 | Dropped support for Python versions < 2.7 and 3.3, and Django < 1.8. If you 94 | need explicit support for these versions, you should stick to version 1.4.4. 95 | 96 | 1.4.4 97 | ----- 98 | 99 | * Bugfix for better IPython support (125d523e1c94e4edb344e3bb3ea1eab6f7d073ed) 100 | 101 | 1.4.3 102 | ----- 103 | 104 | Fixed a bug in the validator error message output - thanks to Sobolev Nikita 105 | 106 | 1.4 to 1.4.1 107 | ------------ 108 | This is a small release that fixes some ugliness. In Django 1.7 and up, the 109 | validator can now be used as documented in the readme. Before this version, the 110 | error:: 111 | 112 | ValueError: Cannot serialize: > 113 | 114 | would be raised, and a workaround was to define a validator function calling the 115 | choices' validator, requiring everything to be defined in the module scope. 116 | 117 | This is now fixed by moving to a class based validator which is deconstructible. 118 | 119 | 120 | 1.3 to 1.4 121 | ---------- 122 | * Added support for upcoming Django 1.9, by preferring stlib SortedDict over 123 | Django's OrderedDict 124 | * Added pypy to the build matrix 125 | * Added coverage to the Travis set-up 126 | -------------------------------------------------------------------------------- /docs/migrating.rst: -------------------------------------------------------------------------------- 1 | .. _migration: 2 | 3 | Migrating to native Django Choices 4 | ================================== 5 | 6 | Since version 3.0, Django offers native choices enums (mostly equivalent) to the 7 | functionality that this library offers. See the `django docs`_ for more details. 8 | 9 | We provide some automated tooling to facilitate migrating and instructions for possible 10 | hurdles. 11 | 12 | Generating equivalent native code 13 | --------------------------------- 14 | 15 | For trivial usage where you just define the choices as a constant, there is a management 16 | command since version 2.0 to generate the equivalent code. It supports ``str`` and ``int`` 17 | for values types. 18 | 19 | #. Ensure you have ``djchoices`` added to your ``INSTALLED_APPS`` setting. 20 | #. Run the command ``python manage.py generate_native_django_choices`` 21 | 22 | The command essentially discovers all subclasses of ``DjangoChoices`` and introspects 23 | them to generate the equivalent native choices code. This depends on the classes being 24 | imported during Django's initialization phase. This should be the case for almost all 25 | usages, as the choices need to be imported to be picked up by models. 26 | 27 | Possible options for the command: 28 | 29 | * ``--no-wrap-gettext``: do not wrap the choice labels in a function call to mark them 30 | translatable. 31 | 32 | * ``--gettext-alias``: when wrapping the labels, you can specify the name of the 33 | function call/alias to wrap with, e.g. ``gettext_lazy``. It defaults to the common 34 | pattern of ``_``. You need to ensure the necessary imports are present in the module. 35 | 36 | Public API 37 | ---------- 38 | 39 | * The ``choices`` class attribute behaves the same in native choices. 40 | 41 | Migrating non-trivial usage 42 | --------------------------- 43 | 44 | Django-choices offered some class attributes that need to be updated too when migrating 45 | to native choices. 46 | 47 | 48 | ``DjangoChoices.labels`` 49 | ^^^^^^^^^^^^^^^^^^^^^^^^ 50 | 51 | This is roughly equivalent to: 52 | 53 | .. code-block:: python 54 | 55 | dict(zip(Native.names, Native.labels)) 56 | 57 | Notable differences: 58 | 59 | * for a value like ``'option1'`` without explicit label, Django Choices produces 60 | ``'option1'`` as label, while Django produces ``'option 1'``. A value like 61 | ``'option_1'`` results in the same label. 62 | * It may not play nice with the empty option in native choices. 63 | 64 | 65 | ``ChoiceItem.order`` 66 | ^^^^^^^^^^^^^^^^^^^^ 67 | 68 | The management command emits the generated native choices in the configured order. 69 | 70 | If you need access to the order, you can leverage ``enumerate(Native.values)`` to loop 71 | over tuples of ``(order, value)``. 72 | 73 | ``DjangoChoices.values`` 74 | ^^^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | This is equivalent to: 77 | 78 | .. code-block:: python 79 | 80 | dict(zip(Native.values, Native.labels)) 81 | 82 | 83 | ``DjangoChoices.validator`` 84 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 85 | 86 | Django has been performing this out of the box on model fields since at least 87 | Django 1.3 - you don't need it. 88 | 89 | ``DjangoChoices.attributes`` 90 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 91 | 92 | This is roughly equivalent to: 93 | 94 | .. code-block:: python 95 | 96 | native = dict(zip(Native.values, Native.names)) 97 | 98 | Remarks: 99 | 100 | * It may not play nice with the empty option in native choices. 101 | 102 | ``DjangoChoices.get_choice`` 103 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 104 | 105 | There is no direct equivalent, however you can access the enum instance and look up 106 | properties: 107 | 108 | .. code-block:: python 109 | 110 | an_enum_value = Native[Native.some_value] 111 | print(an_enum_value.value) 112 | print(an_enum_value.label) 113 | 114 | ``DjangoChoices.get_order_expression`` 115 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 116 | 117 | There is no equivalent, but you should easily be able to add this as your own 118 | class method/mixin: 119 | 120 | .. code-block:: python 121 | 122 | from django.db.models import Case, IntegerField, Value, When 123 | 124 | @classmethod 125 | def get_order_expression(cls, field_name): 126 | whens = [] 127 | for order, value in enumerate(cls.values()): 128 | whens.append( 129 | When(**{field_name: value, "then": Value(order)}) 130 | ) 131 | return Case(*whens, output_field=IntegerField()) 132 | 133 | Custom attributes 134 | ^^^^^^^^^^^^^^^^^ 135 | 136 | It's recommended to keep a separate dictionary with a mapping of choice values to the 137 | additional attributes. You could consider dataclasses to model this too. 138 | 139 | 140 | .. _django docs: https://docs.djangoproject.com/en/3.2/ref/models/fields/#enumeration-types 141 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django-Choices documentation master file, created by 2 | sphinx-quickstart on Thu Sep 10 22:03:38 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | 8 | Django-Choices 9 | ============== 10 | 11 | .. rubric:: Order and sanity for django model choices. 12 | 13 | |build-status| |code-quality| |coverage| |docs| |black| |pypi| |python-versions| |django-versions| 14 | 15 | Contents: 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | migrating 21 | choices 22 | contributing 23 | 24 | Overview 25 | -------- 26 | 27 | .. warning:: 28 | 29 | We **strongly** recommend migrating to the native functionality and not use 30 | django-choices for new projects. See the :ref:`migration`. This library will not be 31 | actively developed any longer. 32 | 33 | Django choices provides a declarative way of using the choices_ option on django_ fields. 34 | 35 | **Note:** Django 3.0 added `enumeration types `__. 36 | This feature mostly replaces the need for Django-Choices. 37 | See also `Adam Johnson's post on using them `__. 38 | 39 | 40 | Requirements 41 | ------------ 42 | 43 | Django choices is fairly simple, so most Python and Django 44 | versions should work. It is tested against Python 3.8+ and Django 3.2+. 45 | 46 | 47 | Quick-start 48 | ----------- 49 | 50 | Install like any other library: 51 | 52 | .. code-block:: sh 53 | 54 | pip install django-choices 55 | 56 | There is no need to add it in your installed apps. 57 | 58 | To use it, you write a choices class, and use it in your model fields: 59 | 60 | 61 | .. code-block:: python 62 | 63 | from djchoices import ChoiceItem, DjangoChoices 64 | 65 | 66 | class Book(models.Model): 67 | 68 | class BookType(DjangoChoices): 69 | short_story = ChoiceItem('short', 'Short story') 70 | novel = ChoiceItem('novel', 'Novel') 71 | non_fiction = ChoiceItem('non_fiction', 'Non fiction') 72 | 73 | 74 | author = models.ForeignKey('Author') 75 | book_type = models.CharField( 76 | max_length=20, choices=BookType.choices, 77 | default=BookType.novel 78 | ) 79 | 80 | 81 | You can then use the available choices in other modules, e.g.: 82 | 83 | 84 | .. code-block:: python 85 | 86 | from .models import Book 87 | 88 | Person.objects.create(author=my_author, type=Book.BookTypes.short_story) 89 | 90 | 91 | The ``DjangoChoices`` classes can be located anywhere you want, 92 | for example you can put them outside of the model declaration if you have a 93 | 'common' set of choices for different models. Any place is valid though, 94 | you can group them all together in ``choices.py`` if you want. 95 | 96 | 97 | License 98 | ------- 99 | Licensed under the `MIT License`_. 100 | 101 | Source Code and contributing 102 | ---------------------------- 103 | The source code can be found on github_. 104 | 105 | Bugs can also be reported on the github_ repository, and pull requests 106 | are welcome. See :ref:`contributing` for more details. 107 | 108 | 109 | Indices and tables 110 | ================== 111 | 112 | * :ref:`genindex` 113 | * :ref:`modindex` 114 | * :ref:`search` 115 | 116 | .. |build-status| image:: https://github.com/bigjason/django-choices/actions/workflows/ci.yml/badge.svg 117 | :alt: Build status 118 | :target: https://github.com/bigjason/django-choices/actions/workflows/ci.yml 119 | 120 | .. |code-quality| image:: https://github.com/bigjason/django-choices/actions//workflows/code_quality.yml/badge.svg 121 | :alt: Code quality checks 122 | :target: https://github.com/bigjason/django-choices/actions//workflows/code_quality.yml 123 | 124 | .. |coverage| image:: https://codecov.io/gh/bigjason/django-choices/branch/master/graph/badge.svg?token=pcbBUCju0B 125 | :alt: Code coverage 126 | :target: https://codecov.io/gh/bigjason/django-choices 127 | 128 | .. |docs| image:: https://readthedocs.org/projects/django-choices/badge/?version=latest 129 | :target: http://django-choices.readthedocs.io/en/latest/ 130 | :alt: Documentation Status 131 | 132 | .. |pypi| image:: https://img.shields.io/pypi/v/django-choices.svg 133 | :target: https://pypi.python.org/pypi/django-choices 134 | 135 | .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-choices.svg 136 | 137 | .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-choices.svg 138 | 139 | .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 140 | :target: https://github.com/psf/black 141 | 142 | .. _django: http://www.djangoproject.com/ 143 | .. _choices: https://docs.djangoproject.com/en/dev/ref/models/fields/#choices 144 | .. _MIT License: http://en.wikipedia.org/wiki/MIT_License 145 | .. _github: https://github.com/bigjason/django-choices 146 | -------------------------------------------------------------------------------- /docs/choices.rst: -------------------------------------------------------------------------------- 1 | Choice items 2 | ============ 3 | 4 | The ``ChoiceItem`` class is what drives the choices. Each instance 5 | corresponds to a possible choice for your field. 6 | 7 | 8 | Basic usage 9 | ----------- 10 | 11 | .. code-block:: python 12 | 13 | class MyChoices(DjangoChoices): 14 | my_choice = ChoiceItem(1, 'label 1') 15 | 16 | 17 | The first argument for ``ChoiceItem`` is the value, as it will be stored 18 | in the database. ``ChoiceItem`` values can be any type, as long as it matches 19 | the field where the choices are defined, e.g.: 20 | 21 | String type: 22 | 23 | .. code-block:: python 24 | 25 | class Strings(DjangoChoices): 26 | one = ChoiceItem('one', 'one') 27 | 28 | 29 | class Model1(models.Model): 30 | field = models.CharField(max_length=10, choices=Strings.choices) 31 | 32 | 33 | or integer: 34 | 35 | .. code-block:: python 36 | 37 | class Ints(DjangoChoices): 38 | one = ChoiceItem(1, 'one') 39 | 40 | 41 | class Model2(models.Model): 42 | field = models.IntegerField(choices=Ints.choices) 43 | 44 | 45 | There is also a 'short name'. You can import ``C`` instead of `ChoiceItem`, if 46 | you're into that. 47 | 48 | Custom attributes 49 | +++++++++++++++++ 50 | 51 | Any additional (custom) keyword arguments passed to the constructor, are made 52 | available as custom attributes: 53 | 54 | .. code-block:: python 55 | 56 | >>> choice = ChoiceItem('excellent', limit_to=['US', 'CA', 'CN']) 57 | >>> choice.limit_to 58 | ['US', 'CA', "CN"] 59 | 60 | To obtain the ``ChoiceItem`` instance, see :ref:`djangochoices.get_choice` 61 | 62 | 63 | Labels 64 | ------ 65 | The second argument to the `ChoiceItem` class is the label. 66 | It's recommended to specify this explicitly if you use 67 | internationalization, e.g.: 68 | 69 | 70 | .. code-block:: python 71 | 72 | from django.utils.translation import ugettext_lazy as _ 73 | 74 | 75 | class MyChoices(DjangoChoices): 76 | one = ChoiceItem(1, _('one')) 77 | 78 | 79 | If the label is not provided, it will be automatically determined from 80 | the class property, and underscores are translated to spaces. So, the 81 | following example yields: 82 | 83 | .. code-block:: python 84 | 85 | >>> class MyChoices(DjangoChoices): 86 | ... first_choice = ChoiceItem(1) 87 | 88 | >>> MyChoices.choices 89 | ((1, 'first choice'),) 90 | 91 | 92 | Ordering 93 | -------- 94 | 95 | `ChoiceItem` objects also support ordering. If not provided, the choices are 96 | returned in order of declaration. 97 | 98 | 99 | .. code-block:: python 100 | 101 | >>> class MyChoices(DjangoChoices): 102 | ... first = ChoiceItem(1, order=20) 103 | ... second = ChoiceItem(2, order=10) 104 | 105 | >>> MyChoices.choices 106 | ( 107 | (2, 'second'), 108 | (1, 'first'), 109 | ) 110 | 111 | 112 | Values 113 | ------ 114 | If you really want to use the minimal amount of code, you can leave off the 115 | value as well, and it will be determined from the label. 116 | 117 | .. code-block:: python 118 | 119 | >>> class Sample(DjangoChoices): 120 | ... OptionA = ChoiceItem() 121 | ... OptionB = ChoiceItem() 122 | 123 | >>> Sample.choices 124 | ( 125 | ('OptionA', 'OptionA'), 126 | ('OptionB', 'OptionB'), 127 | ) 128 | 129 | 130 | ``DjangoChoices`` class attributes 131 | ---------------------------------- 132 | 133 | The choices class itself has a few useful attributes. Most notably `choices`, 134 | which returns the choices as a tuple. 135 | 136 | 137 | choices 138 | +++++++ 139 | 140 | .. code-block:: python 141 | 142 | >>> class Sample(DjangoChoices): 143 | ... OptionA = ChoiceItem() 144 | ... OptionB = ChoiceItem() 145 | 146 | >>> Sample.choices 147 | ( 148 | ('OptionA', 'OptionA'), 149 | ('OptionB', 'OptionB'), 150 | ) 151 | 152 | 153 | labels 154 | ++++++ 155 | 156 | Returns a dictionary with a mapping from attribute to the human-readable 157 | label: 158 | 159 | .. code-block:: python 160 | 161 | >>> class MyChoices(DjangoChoices): 162 | ... first_choice = ChoiceItem(1) 163 | ... second_choice = ChoiceItem(2) 164 | 165 | >>> MyChoices.labels 166 | {'first_choice': 1, 'second_choice': 2} 167 | >>> MyChoices.labels.first_choice 168 | "first choice" 169 | 170 | 171 | values 172 | ++++++ 173 | 174 | Returns a dictionary with a mapping from value to label: 175 | 176 | .. code-block:: python 177 | 178 | >>> class MyChoices(DjangoChoices): 179 | ... first_choice = ChoiceItem(1, 'label 1') 180 | ... second_choice = ChoiceItem(2, 'label 2') 181 | 182 | >>> MyChoices.values 183 | {1: 'label 1', '2': 'label 2'} 184 | 185 | 186 | validator 187 | +++++++++ 188 | 189 | .. note:: 190 | 191 | At least since Django 1.3, there is model and form-level validation of the 192 | choices. Unless you have a reason to explicitly specify/override the validator, 193 | you can skip specifying this validator. 194 | 195 | 196 | Returns a validator that can be used in your model field. This validator checks 197 | that the value passed to the field is indeed a value specified in your choices 198 | class. 199 | 200 | attributes 201 | ++++++++++ 202 | 203 | Returns an ``OrderedDict`` with the mapping from choice value -> attribute 204 | on the choices class. 205 | 206 | .. code-block:: python 207 | 208 | >>> class MyChoices(DjangoChoices): 209 | ... first_choice = ChoiceItem(1, 'label 1') 210 | ... second_choice = ChoiceItem(2, 'label 2') 211 | 212 | >>> MyChoices.attributes 213 | OrderedDict([(1, 'first_choice'), (2, 'second_choice')]) 214 | 215 | 216 | .. _djangochoices.get_choice: 217 | 218 | get_choice 219 | ++++++++++ 220 | 221 | Returns the actual ``ChoiceItem`` instance for a given value: 222 | 223 | .. code-block:: python 224 | 225 | >>> class MyChoices(DjangoChoices): 226 | ... first_choice = ChoiceItem(1, 'label 1') 227 | ... second_choice = ChoiceItem(2, 'label 2') 228 | 229 | >>> MyChoices.get_choice(MyChoices.second_choice) 230 | 231 | 232 | This allows you to inspect any ``ChoiceItem`` attributes. 233 | 234 | get_order_expression 235 | ++++++++++++++++++++ 236 | 237 | Build the ``Case``/``When`` statement to use in queryset annotations. 238 | 239 | Choices defined get an implicit or explicit order, which can have semantic 240 | value. This ORM expression allows you to map choice values to the order value, 241 | as an integer, on the database level. 242 | 243 | It is then available for subsequent filtering, allowing more work to be done 244 | in the database instead of in Python. 245 | 246 | .. code-block:: python 247 | 248 | >>> class MyChoices(DjangoChoices): 249 | ... first = ChoiceItem('first') 250 | ... second = ChoiceItem('second') 251 | 252 | >>> order = MyChoices.get_order_expression('some_field') 253 | >>> queryset = Model.objects.annotate(some_field_order=order) 254 | >>> for item in queryset: 255 | ... print(item.some_field) 256 | ... print(item.some_field_order) 257 | # first_ 258 | # 1 259 | # second 260 | # 2 261 | -------------------------------------------------------------------------------- /djchoices/choices.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | 4 | from django.core.exceptions import ValidationError 5 | from django.db.models import Case, IntegerField, Value, When 6 | from django.utils.deconstruct import deconstructible 7 | 8 | __all__ = ["ChoiceItem", "DjangoChoices", "C"] 9 | 10 | 11 | # Support Functionality (Not part of public API) 12 | 13 | 14 | class Labels(dict): 15 | def __getattr__(self, name): 16 | result = dict.get(self, name, None) 17 | if result is not None: 18 | return result 19 | else: 20 | raise AttributeError("Label for field %s was not found." % name) 21 | 22 | def __setattr__(self, name, value): 23 | self[name] = value 24 | 25 | 26 | class StaticProp(object): 27 | def __init__(self, value): 28 | self.value = value 29 | 30 | def __get__(self, obj, objtype): 31 | return self.value 32 | 33 | 34 | class Attributes(object): 35 | def __init__(self, attrs, fields): 36 | self.attrs = attrs 37 | self.fields = fields 38 | 39 | def __get__(self, obj, objtype): 40 | if len(self.attrs) != len(self.fields): 41 | raise ValueError( 42 | "Not all values are unique, it's not possible to map all " 43 | "values to the right attribute" 44 | ) 45 | return self.attrs 46 | 47 | 48 | # End Support Functionality 49 | 50 | 51 | sentinel = object() 52 | 53 | 54 | class ChoiceItem(object): 55 | """ 56 | Describes a choice item. 57 | 58 | The label is usually the field name so label can normally be left blank. 59 | Set a label if you need characters that are illegal in a python identifier 60 | name (ie: "DVD/Movie"). 61 | """ 62 | 63 | order = 0 64 | 65 | def __init__(self, value=sentinel, label=None, order=None, **extra): 66 | self.value = value 67 | self.label = label 68 | self._extra = extra 69 | 70 | if order is not None: 71 | self.order = order 72 | else: 73 | ChoiceItem.order += 1 74 | self.order = ChoiceItem.order 75 | 76 | def __repr__(self): 77 | extras = " ".join( 78 | [ 79 | "{key}={value!r}".format(key=key, value=value) 80 | for key, value in self._extra.items() 81 | ] 82 | ) 83 | 84 | return "<{} value={!r} label={!r} order={!r}{extras}>".format( 85 | self.__class__.__name__, 86 | self.value, 87 | self.label, 88 | self.order, 89 | extras=" " + extras if extras else "", 90 | ) 91 | 92 | def __getattr__(self, name): 93 | try: 94 | return self._extra[name] 95 | except KeyError: 96 | raise AttributeError( 97 | "{!r} object has no attribute {!r}".format(self.__class__, name) 98 | ) 99 | 100 | 101 | # Shorter convenience alias. 102 | C = ChoiceItem # noqa 103 | 104 | 105 | class DjangoChoicesMeta(type): 106 | """ 107 | Metaclass that writes the choices class. 108 | """ 109 | 110 | name_clean = re.compile(r"_+") 111 | 112 | def __iter__(self): 113 | for choice in self.choices: 114 | yield choice 115 | 116 | def __len__(self): 117 | return len(self.choices) 118 | 119 | def __new__(cls, name, bases, attrs): 120 | fields = {} 121 | labels = Labels() 122 | values = OrderedDict() 123 | attributes = OrderedDict() 124 | choices = [] 125 | 126 | # Get all the fields from parent classes. 127 | parents = [b for b in bases if isinstance(b, DjangoChoicesMeta)] 128 | for kls in parents: 129 | for field_name in kls._fields: 130 | fields[field_name] = kls._fields[field_name] 131 | 132 | # Get all the fields from this class. 133 | for field_name in attrs: 134 | val = attrs[field_name] 135 | if isinstance(val, ChoiceItem): 136 | fields[field_name] = val 137 | 138 | fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order)) 139 | 140 | for field_name in fields: 141 | val = fields[field_name] 142 | if isinstance(val, ChoiceItem): 143 | if val.label is not None: 144 | label = val.label 145 | else: 146 | # TODO: mark translatable by default? 147 | label = cls.name_clean.sub(" ", field_name) 148 | 149 | val0 = label if val.value is sentinel else val.value 150 | choices.append((val0, label)) 151 | attrs[field_name] = StaticProp(val0) 152 | setattr(labels, field_name, label) 153 | values[val0] = label 154 | attributes[val0] = field_name 155 | else: 156 | choices.append((field_name, val.choices)) 157 | 158 | attrs["choices"] = StaticProp(tuple(choices)) 159 | attrs["labels"] = labels 160 | attrs["values"] = values 161 | attrs["_fields"] = fields 162 | attrs["validator"] = ChoicesValidator(values) 163 | attrs["attributes"] = Attributes(attributes, fields) 164 | 165 | return super(DjangoChoicesMeta, cls).__new__(cls, name, bases, attrs) 166 | 167 | 168 | @deconstructible 169 | class ChoicesValidator(object): 170 | def __init__(self, values): 171 | self.values = values 172 | 173 | def __call__(self, value): 174 | if value not in self.values: 175 | raise ValidationError( 176 | "Select a valid choice. %s is not " 177 | "one of the available choices." % value 178 | ) 179 | 180 | def __eq__(self, other): 181 | return isinstance(other, ChoicesValidator) and self.values == other.values 182 | 183 | def __ne__(self, other): 184 | return not (self == other) 185 | 186 | 187 | class DjangoChoices(metaclass=DjangoChoicesMeta): 188 | order = 0 189 | choices = () 190 | labels = Labels() 191 | values = {} 192 | validator = None 193 | 194 | @classmethod 195 | def get_choice(cls, value): 196 | """ 197 | Return the underlying :class:`ChoiceItem` for a given value. 198 | """ 199 | attribute_for_value = cls.attributes[value] 200 | return cls._fields[attribute_for_value] 201 | 202 | @classmethod 203 | def get_order_expression(cls, field_name): 204 | """ 205 | Build the Case/When to annotate objects with the choice item order 206 | 207 | Useful if choices represent some access-control mechanism, for example. 208 | 209 | Usage:: 210 | 211 | >>> order = MyChoices.get_order_expression('some_field') 212 | >>> queryset = Model.objects.annotate(some_field_order=order) 213 | >>> for item in queryset: 214 | ... print(item.some_field) 215 | ... print(item.some_field_order) 216 | # first_choice 217 | # 1 218 | # second_choice 219 | # 2 220 | """ 221 | whens = [] 222 | for choice_item in cls._fields.values(): 223 | whens.append( 224 | When( 225 | **{field_name: choice_item.value, "then": Value(choice_item.order)} 226 | ) 227 | ) 228 | return Case(*whens, output_field=IntegerField()) 229 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-Choices.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-Choices.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Django-Choices" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-Choices" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Django-Choices.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Django-Choices.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /djchoices/tests/test_choices.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.db.models import Case, IntegerField, Value, When 4 | 5 | from djchoices import C, ChoiceItem, DjangoChoices 6 | 7 | 8 | class NumericTestClass(DjangoChoices): 9 | Item_0 = C(0) 10 | Item_1 = C(1) 11 | Item_2 = C(2) 12 | Item_3 = C(3) 13 | 14 | 15 | class StringTestClass(DjangoChoices): 16 | empty = ChoiceItem("", "") 17 | One = ChoiceItem("O") 18 | Two = ChoiceItem("T") 19 | Three = ChoiceItem("H") 20 | 21 | 22 | class SubClass1(NumericTestClass): 23 | Item_4 = C(4) 24 | Item_5 = C(5) 25 | 26 | 27 | class SubClass2(SubClass1): 28 | Item_6 = C(6) 29 | Item_7 = C(7) 30 | 31 | 32 | class EmptyValueClass(DjangoChoices): 33 | Option1 = ChoiceItem() 34 | Option2 = ChoiceItem() 35 | Option3 = ChoiceItem() 36 | 37 | 38 | class NullBooleanValueClass(DjangoChoices): 39 | Option1 = ChoiceItem(None, "Pending") 40 | Option2 = ChoiceItem(True, "Successful") 41 | Option3 = ChoiceItem(False, "Failed") 42 | 43 | 44 | class DuplicateValuesClass(DjangoChoices): 45 | Option1 = ChoiceItem("a") 46 | Option2 = ChoiceItem("a") 47 | 48 | 49 | class OrderedChoices(DjangoChoices): 50 | Option1 = ChoiceItem("a", order=1) 51 | Option2 = ChoiceItem("b", order=0) 52 | 53 | 54 | class ExtraAttributeChoices(DjangoChoices): 55 | Option1 = ChoiceItem(0, help_text="Option1 help text") 56 | Option2 = ChoiceItem( 57 | 1, help_text="Option2 help text", validator_class_name="RegexValidator" 58 | ) 59 | 60 | 61 | class DjangoChoices(unittest.TestCase): 62 | def setUp(self): 63 | pass 64 | 65 | def tearDown(self): 66 | pass 67 | 68 | def test_numeric_class_values(self): 69 | self.assertEqual(NumericTestClass.Item_0, 0) 70 | self.assertEqual(NumericTestClass.Item_1, 1) 71 | self.assertEqual(NumericTestClass.Item_2, 2) 72 | self.assertEqual(NumericTestClass.Item_3, 3) 73 | 74 | def test_class_labels(self): 75 | self.assertEqual(StringTestClass.labels.empty, "") 76 | self.assertEqual(NumericTestClass.labels.Item_1, "Item 1") 77 | self.assertEqual(NumericTestClass.labels.Item_2, "Item 2") 78 | self.assertEqual(NumericTestClass.labels.Item_3, "Item 3") 79 | 80 | def test_class_labels_inherited(self): 81 | self.assertEqual(SubClass2.labels.Item_2, "Item 2") 82 | self.assertEqual(SubClass2.labels.Item_6, "Item 6") 83 | 84 | def test_class_values(self): 85 | self.assertEqual(SubClass1.values[SubClass1.Item_1], "Item 1") 86 | self.assertEqual(SubClass1.values[SubClass1.Item_4], "Item 4") 87 | self.assertEqual(SubClass1.values[SubClass1.Item_5], "Item 5") 88 | 89 | def test_class_values_order(self): 90 | self.assertEqual(list(StringTestClass.values), ["", "O", "T", "H"]) 91 | 92 | def test_numeric_class_order(self): 93 | choices = NumericTestClass.choices 94 | self.assertEqual(choices[0][0], 0) 95 | self.assertEqual(choices[1][0], 1) 96 | self.assertEqual(choices[2][0], 2) 97 | self.assertEqual(choices[3][0], 3) 98 | 99 | def test_string_class_values(self): 100 | self.assertEqual(StringTestClass.One, "O") 101 | self.assertEqual(StringTestClass.Two, "T") 102 | self.assertEqual(StringTestClass.Three, "H") 103 | 104 | def test_string_class_order(self): 105 | choices = StringTestClass.choices 106 | self.assertEqual(choices[0][0], "") 107 | self.assertEqual(choices[1][0], "O") 108 | self.assertEqual(choices[2][0], "T") 109 | self.assertEqual(choices[3][0], "H") 110 | 111 | def test_sub_class_level_1_choices(self): 112 | choices = SubClass1.choices 113 | self.assertEqual(choices[0][0], 0) 114 | self.assertEqual(choices[4][0], 4) 115 | self.assertEqual(choices[5][0], 5) 116 | 117 | def test_sub_class_level_1_values(self): 118 | self.assertEqual(SubClass1.Item_1, 1) 119 | self.assertEqual(SubClass1.Item_4, 4) 120 | self.assertEqual(SubClass1.Item_5, 5) 121 | 122 | def test_sub_class_level_2_choices(self): 123 | choices = SubClass2.choices 124 | self.assertEqual(choices[0][0], 0) 125 | self.assertEqual(choices[4][0], 4) 126 | self.assertEqual(choices[6][0], 6) 127 | self.assertEqual(choices[7][0], 7) 128 | 129 | def test_sub_class_level_2_values(self): 130 | self.assertEqual(SubClass2.Item_1, 1) 131 | self.assertEqual(SubClass2.Item_5, 5) 132 | self.assertEqual(SubClass2.Item_6, 6) 133 | self.assertEqual(SubClass2.Item_7, 7) 134 | 135 | def test_sub_class_name(self): 136 | self.assertEqual(NumericTestClass.__name__, "NumericTestClass") 137 | self.assertEqual(SubClass2.__name__, "SubClass2") 138 | 139 | def test_numeric_class_validator(self): 140 | from django.core.exceptions import ValidationError 141 | 142 | self.assertEqual(None, NumericTestClass.validator(1)) 143 | self.assertEqual(None, NumericTestClass.validator(2)) 144 | self.assertEqual(None, NumericTestClass.validator(3)) 145 | 146 | self.assertRaises(ValidationError, NumericTestClass.validator, 4) 147 | self.assertRaises(ValidationError, NumericTestClass.validator, 5) 148 | self.assertRaises(ValidationError, NumericTestClass.validator, 6) 149 | self.assertRaises(ValidationError, NumericTestClass.validator, 7) 150 | 151 | def test_validation_error_message(self): 152 | from django.core.exceptions import ValidationError 153 | 154 | message = "Select a valid choice. 4 is not " "one of the available choices." 155 | 156 | self.assertRaisesRegexp(ValidationError, message, NumericTestClass.validator, 4) 157 | 158 | def test_subclass1_validator(self): 159 | from django.core.exceptions import ValidationError 160 | 161 | self.assertEqual(None, SubClass1.validator(1)) 162 | self.assertEqual(None, SubClass1.validator(2)) 163 | self.assertEqual(None, SubClass1.validator(3)) 164 | self.assertEqual(None, SubClass1.validator(4)) 165 | self.assertEqual(None, SubClass1.validator(5)) 166 | 167 | self.assertRaises(ValidationError, SubClass1.validator, 6) 168 | self.assertRaises(ValidationError, SubClass1.validator, 7) 169 | 170 | def test_subclass_2_validator(self): 171 | from django.core.exceptions import ValidationError 172 | 173 | self.assertEqual(None, SubClass2.validator(1)) 174 | self.assertEqual(None, SubClass2.validator(2)) 175 | self.assertEqual(None, SubClass2.validator(3)) 176 | self.assertEqual(None, SubClass2.validator(4)) 177 | self.assertEqual(None, SubClass2.validator(5)) 178 | self.assertEqual(None, SubClass2.validator(6)) 179 | self.assertEqual(None, SubClass2.validator(7)) 180 | 181 | self.assertRaises(ValidationError, SubClass2.validator, 8) 182 | 183 | def test_empty_value_class(self): 184 | choices = EmptyValueClass.choices 185 | self.assertEqual(choices[0][0], "Option1") 186 | self.assertEqual(choices[1][0], "Option2") 187 | self.assertEqual(choices[2][0], "Option3") 188 | 189 | def test_null_boolean_value_class(self): 190 | choices = NullBooleanValueClass.choices 191 | self.assertEqual(choices[0][0], None) 192 | self.assertEqual(choices[1][0], True) 193 | self.assertEqual(choices[2][0], False) 194 | self.assertEqual(choices[0][1], "Pending") 195 | self.assertEqual(choices[1][1], "Successful") 196 | self.assertEqual(choices[2][1], "Failed") 197 | 198 | def test_deconstructible_validator(self): 199 | deconstructed = NumericTestClass.validator.deconstruct() 200 | self.assertEqual( 201 | deconstructed, 202 | ("djchoices.choices.ChoicesValidator", (NumericTestClass.values,), {}), 203 | ) 204 | 205 | def test_attribute_from_value(self): 206 | attributes = NumericTestClass.attributes 207 | self.assertEqual(attributes[0], "Item_0") 208 | self.assertEqual(attributes[1], "Item_1") 209 | self.assertEqual(attributes[2], "Item_2") 210 | self.assertEqual(attributes[3], "Item_3") 211 | 212 | def test_attribute_from_value_duplicates(self): 213 | with self.assertRaises(ValueError): 214 | DuplicateValuesClass.attributes 215 | 216 | def test_choice_item_order(self): 217 | choices = OrderedChoices.choices 218 | self.assertEqual(choices[0][0], "b") 219 | self.assertEqual(choices[1][0], "a") 220 | 221 | def test_get_choices(self): 222 | choices_class = NullBooleanValueClass 223 | 224 | self.assertEqual("Pending", choices_class.get_choice(None).label) 225 | self.assertEqual("Successful", choices_class.get_choice(True).label) 226 | self.assertEqual("Failed", choices_class.get_choice(False).label) 227 | 228 | def test_get_extra_attributes(self): 229 | choices_class = ExtraAttributeChoices 230 | 231 | self.assertEqual( 232 | "Option1 help text", 233 | choices_class.get_choice(choices_class.Option1).help_text, 234 | ) 235 | 236 | self.assertEqual( 237 | "Option2 help text", 238 | choices_class.get_choice(choices_class.Option2).help_text, 239 | ) 240 | 241 | self.assertEqual( 242 | "RegexValidator", 243 | choices_class.get_choice(choices_class.Option2).validator_class_name, 244 | ) 245 | 246 | def test_get_extra_attributes_unknown_attribute_throws_error(self): 247 | choices_class = ExtraAttributeChoices 248 | 249 | with self.assertRaises(AttributeError): 250 | choices_class.get_choice(choices_class.Option1).unknown_attribute 251 | 252 | def test_repr(self): 253 | choices_class = ExtraAttributeChoices 254 | repr_string = repr(choices_class.get_choice(choices_class.Option2)) 255 | 256 | self.assertIn(" v documentation". 124 | # html_title = None 125 | 126 | # A shorter title for the navigation bar. Default is the same as html_title. 127 | # html_short_title = None 128 | 129 | # The name of an image file (relative to this directory) to place at the top 130 | # of the sidebar. 131 | # html_logo = None 132 | 133 | # The name of an image file (within the static path) to use as favicon of the 134 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 135 | # pixels large. 136 | # html_favicon = None 137 | 138 | # Add any paths that contain custom static files (such as style sheets) here, 139 | # relative to this directory. They are copied after the builtin static files, 140 | # so a file named "default.css" will overwrite the builtin "default.css". 141 | # html_static_path = ['_static'] 142 | 143 | # Add any extra paths that contain custom files (such as robots.txt or 144 | # .htaccess) here, relative to this directory. These files are copied 145 | # directly to the root of the documentation. 146 | # html_extra_path = [] 147 | 148 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 149 | # using the given strftime format. 150 | # html_last_updated_fmt = '%b %d, %Y' 151 | 152 | # If true, SmartyPants will be used to convert quotes and dashes to 153 | # typographically correct entities. 154 | # html_use_smartypants = True 155 | 156 | # Custom sidebar templates, maps document names to template names. 157 | # html_sidebars = {} 158 | 159 | # Additional templates that should be rendered to pages, maps page names to 160 | # template names. 161 | # html_additional_pages = {} 162 | 163 | # If false, no module index is generated. 164 | # html_domain_indices = True 165 | 166 | # If false, no index is generated. 167 | # html_use_index = True 168 | 169 | # If true, the index is split into individual pages for each letter. 170 | # html_split_index = False 171 | 172 | # If true, links to the reST sources are added to the pages. 173 | # html_show_sourcelink = True 174 | 175 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 176 | # html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 179 | # html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages will 182 | # contain a tag referring to it. The value of this option must be the 183 | # base URL from which the finished HTML is served. 184 | # html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | # html_file_suffix = None 188 | 189 | # Language to be used for generating the HTML full-text search index. 190 | # Sphinx supports the following languages: 191 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 192 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 193 | # html_search_language = 'en' 194 | 195 | # A dictionary with options for the search language support, empty by default. 196 | # Now only 'ja' uses this config value 197 | # html_search_options = {'type': 'default'} 198 | 199 | # The name of a javascript file (relative to the configuration directory) that 200 | # implements a search results scorer. If empty, the default will be used. 201 | # html_search_scorer = 'scorer.js' 202 | 203 | # Output file base name for HTML help builder. 204 | htmlhelp_basename = "Django-Choicesdoc" 205 | 206 | # -- Options for LaTeX output --------------------------------------------- 207 | 208 | latex_elements = { 209 | # The paper size ('letterpaper' or 'a4paper'). 210 | #'papersize': 'letterpaper', 211 | # The font size ('10pt', '11pt' or '12pt'). 212 | #'pointsize': '10pt', 213 | # Additional stuff for the LaTeX preamble. 214 | #'preamble': '', 215 | # Latex figure (float) alignment 216 | #'figure_align': 'htbp', 217 | } 218 | 219 | # Grouping the document tree into LaTeX files. List of tuples 220 | # (source start file, target name, title, 221 | # author, documentclass [howto, manual, or own class]). 222 | latex_documents = [ 223 | ( 224 | master_doc, 225 | "Django-Choices.tex", 226 | "Django-Choices Documentation", 227 | "Jason Webb, Sergei Maertens", 228 | "manual", 229 | ), 230 | ] 231 | 232 | # The name of an image file (relative to this directory) to place at the top of 233 | # the title page. 234 | # latex_logo = None 235 | 236 | # For "manual" documents, if this is true, then toplevel headings are parts, 237 | # not chapters. 238 | # latex_use_parts = False 239 | 240 | # If true, show page references after internal links. 241 | # latex_show_pagerefs = False 242 | 243 | # If true, show URL addresses after external links. 244 | # latex_show_urls = False 245 | 246 | # Documents to append as an appendix to all manuals. 247 | # latex_appendices = [] 248 | 249 | # If false, no module index is generated. 250 | # latex_domain_indices = True 251 | 252 | 253 | # -- Options for manual page output --------------------------------------- 254 | 255 | # One entry per manual page. List of tuples 256 | # (source start file, name, description, authors, manual section). 257 | man_pages = [ 258 | (master_doc, "django-choices", "Django-Choices Documentation", [author], 1) 259 | ] 260 | 261 | # If true, show URL addresses after external links. 262 | # man_show_urls = False 263 | 264 | 265 | # -- Options for Texinfo output ------------------------------------------- 266 | 267 | # Grouping the document tree into Texinfo files. List of tuples 268 | # (source start file, target name, title, author, 269 | # dir menu entry, description, category) 270 | texinfo_documents = [ 271 | ( 272 | master_doc, 273 | "Django-Choices", 274 | "Django-Choices Documentation", 275 | author, 276 | "Django-Choices", 277 | "One line description of project.", 278 | "Miscellaneous", 279 | ), 280 | ] 281 | 282 | # Documents to append as an appendix to all manuals. 283 | # texinfo_appendices = [] 284 | 285 | # If false, no module index is generated. 286 | # texinfo_domain_indices = True 287 | 288 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 289 | # texinfo_show_urls = 'footnote' 290 | 291 | # If true, do not generate a @detailmenu in the "Top" node's menu. 292 | # texinfo_no_detailmenu = False 293 | 294 | 295 | # on_rtd is whether we are on readthedocs.org 296 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 297 | 298 | if not on_rtd: # only import and set the theme if we're building docs locally 299 | import sphinx_rtd_theme 300 | 301 | html_theme = "sphinx_rtd_theme" 302 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 303 | 304 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 305 | --------------------------------------------------------------------------------