├── .codecov.yml ├── .github ├── dependabot.yml └── workflows │ ├── debug.yml │ ├── lint.yml │ ├── release.yml │ ├── scorecard.yml │ ├── test.yml │ └── zizmor.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── benchmarks.json ├── doc ├── .readthedocs.yaml └── source │ ├── changelog.rst │ ├── conf.py │ ├── eccentric.rst │ ├── howto │ ├── admin.rst │ ├── external.rst │ ├── flags.rst │ ├── forms.rst │ ├── index.rst │ ├── integrations.rst │ ├── migrations.rst │ ├── options.rst │ └── urls.rst │ ├── index.rst │ ├── performance.rst │ ├── plots │ ├── FlagSizeBenchmark.png │ ├── IndexedExactQueryPerformance_postgres.png │ ├── NoIndexQueryPerformance_postgres.png │ └── QueryPerformance_postgresql.png │ ├── reference │ ├── DRF.rst │ ├── choices.rst │ ├── fields.rst │ ├── filters.rst │ ├── forms.rst │ ├── index.rst │ ├── query.rst │ ├── urls.rst │ ├── utils.rst │ └── widgets.rst │ ├── refs.rst │ ├── tutorials │ ├── flags.rst │ ├── index.rst │ └── properties.rst │ └── widgets │ ├── FlagCheckbox.png │ ├── FlagCheckbox_nulls.png │ ├── FlagSelectMultiple.png │ ├── NonStrictFlagCheckbox.png │ ├── NonStrictFlagSelectMultiple.png │ ├── NonStrictRadioSelect.png │ ├── NonStrictSelect.png │ ├── RadioSelect.png │ ├── RadioSelect_nulls.png │ └── Select.png ├── justfile ├── plot_benchmarks.py ├── pyproject.toml ├── src └── django_enum │ ├── __init__.py │ ├── choices.py │ ├── drf.py │ ├── fields.py │ ├── filters.py │ ├── forms.py │ ├── py.typed │ ├── query.py │ ├── urls.py │ └── utils.py ├── tests ├── __init__.py ├── benchmark │ ├── __init__.py │ ├── apps.py │ ├── enums.py │ ├── migrations │ │ └── __init__.py │ └── models.py ├── benchmarks.py ├── constraints │ ├── __init__.py │ ├── apps.py │ ├── enums.py │ ├── migrations │ │ └── __init__.py │ └── models.py ├── converters │ ├── __init__.py │ ├── apps.py │ └── urls.py ├── db_default │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ └── models.py ├── djenum │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── enums.py │ ├── forms.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── urls.py │ └── views.py ├── edit_tests │ ├── __init__.py │ ├── apps.py │ ├── edits │ │ ├── _1.py │ │ ├── _10.py │ │ ├── _2.py │ │ ├── _3.py │ │ ├── _4.py │ │ ├── _5.py │ │ ├── _6.py │ │ ├── _7.py │ │ ├── _8.py │ │ └── _9.py │ ├── migrations │ │ └── __init__.py │ └── models.py ├── enum_prop │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── enums.py │ ├── forms.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── urls.py │ └── views.py ├── examples │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── basic_example.py │ ├── checkboxes_form_howto.py │ ├── choice_form_howto.py │ ├── custom_value_example.py │ ├── drf_modelserializer_howto.py │ ├── drf_serializer_howto.py │ ├── enums │ │ ├── __init__.py │ │ ├── color.py │ │ └── permissions.py │ ├── equivalency_howto.py │ ├── extern_howto.py │ ├── filterfield_howto.py │ ├── filterset_howto.py │ ├── flag_example.py │ ├── flag_form_howto.py │ ├── flag_howto.py │ ├── flagfilterfield_howto.py │ ├── gnss_tutorial.py │ ├── gnss_vanilla_tutorial.py │ ├── hash_equivalency_howto.py │ ├── mapbox_tutorial.py │ ├── migrations │ │ └── __init__.py │ ├── mixed_value_example.py │ ├── models │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── custom_value.py │ │ ├── equivalency.py │ │ ├── extern.py │ │ ├── flag.py │ │ ├── flag_howto.py │ │ ├── gnss.py │ │ ├── gnss_vanilla.py │ │ ├── hash_equivalency.py │ │ ├── mapbox.py │ │ ├── mixed_value.py │ │ ├── no_coerce.py │ │ ├── path_value.py │ │ ├── properties.py │ │ ├── properties_choices.py │ │ ├── strict.py │ │ ├── text_choices.py │ │ └── widgets.py │ ├── no_coerce_howto.py │ ├── path_value_example.py │ ├── properties_example.py │ ├── radio_form_howto.py │ ├── strict_howto.py │ ├── templates │ │ └── tests_examples │ │ │ ├── choice_form_howto.html │ │ │ ├── flag_form_howto.html │ │ │ └── textchoicesexample_filter.html │ ├── text_choices_howto.py │ ├── url_converter_howto.py │ ├── urls.py │ ├── urls_forms.py │ └── urls_howto.py ├── flag_constraints │ ├── __init__.py │ ├── apps.py │ ├── enums.py │ ├── migrations │ │ └── __init__.py │ └── models.py ├── oracle_patch.py ├── settings.py ├── test_admin.py ├── test_admin_ep.py ├── test_bulk.py ├── test_bulk_ep.py ├── test_choices.py ├── test_choices_ep.py ├── test_constraints.py ├── test_converter.py ├── test_db_defaults.py ├── test_eccentric.py ├── test_enum_props.py ├── test_errors.py ├── test_examples.py ├── test_external.py ├── test_field_types.py ├── test_field_types_ep.py ├── test_flags.py ├── test_flags_ep.py ├── test_forms.py ├── test_forms_ep.py ├── test_migrations.py ├── test_name_override.py ├── test_queries.py ├── test_queries_ep.py ├── test_requests.py ├── test_requests_ep.py ├── test_requests_flags.py ├── test_requests_flags_ep.py ├── test_utils.py ├── test_validation.py ├── test_verify_environment.py ├── tmpls │ ├── __init__.py │ ├── apps.py │ ├── templates │ │ ├── base.html │ │ ├── enumtester_confirm_delete.html │ │ ├── enumtester_detail.html │ │ ├── enumtester_form.html │ │ ├── enumtester_list.html │ │ ├── flagtester_confirm_delete.html │ │ ├── flagtester_detail.html │ │ ├── flagtester_form.html │ │ └── flagtester_list.html │ └── templatetags │ │ ├── __init__.py │ │ └── test_tags.py ├── urls.py └── utils.py └── uv.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/codecov/codecov-python/issues/136 2 | coverage: 3 | fixes: 4 | - "__init__.py::django_enum/__init__.py" 5 | - "utils.py::django_enum/utils.py" 6 | - "forms.py::django_enum/forms.py" 7 | - "urls.py::django_enum/urls.py" 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | commit-message: 14 | prefix: ⬆ 15 | # Python 16 | - package-ecosystem: "pip" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | commit-message: 21 | prefix: ⬆ 22 | -------------------------------------------------------------------------------- /.github/workflows/debug.yml: -------------------------------------------------------------------------------- 1 | name: Debug 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | python-version: 10 | description: 'Python Version' 11 | required: true 12 | type: choice 13 | default: '3.13' 14 | options: 15 | - '3.9' 16 | - '3.10' 17 | - '3.11' 18 | - '3.12' 19 | - '3.13' 20 | 21 | jobs: 22 | 23 | debug: 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Python ${{ github.event.inputs.python-version }} 33 | uses: actions/setup-python@v5 34 | id: sp 35 | with: 36 | python-version: ${{ github.event.inputs.python-version }} 37 | - name: Install uv 38 | uses: astral-sh/setup-uv@v6 39 | with: 40 | enable-cache: true 41 | - name: Install Just 42 | uses: extractions/setup-just@v3 43 | - name: Install Dependencies 44 | run: | 45 | just setup ${{ steps.sp.outputs.python-path }} 46 | - name: Install Emacs 47 | run: | 48 | sudo apt install emacs 49 | - name: Setup tmate session 50 | uses: mxschmitt/action-tmate@v3.22 51 | with: 52 | detached: true 53 | timeout-minutes: 60 54 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | tags-ignore: 9 | - '*' 10 | branches: 11 | - '*' 12 | pull_request: 13 | workflow_call: 14 | workflow_dispatch: 15 | inputs: 16 | debug: 17 | description: 'Open ssh debug session.' 18 | required: true 19 | default: false 20 | type: boolean 21 | 22 | jobs: 23 | 24 | lint: 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: read 28 | strategy: 29 | matrix: 30 | # run static analysis on bleeding and trailing edges 31 | python-version: [ '3.9', '3.13' ] 32 | django-version: 33 | - '3.2' # LTS April 2024 34 | - '4.2' # LTS April 2026 35 | - '5.2' # LTS April 2028 36 | exclude: 37 | - python-version: '3.9' 38 | django-version: '4.2' 39 | - python-version: '3.13' 40 | django-version: '3.2' 41 | - python-version: '3.9' 42 | django-version: '5.2' 43 | 44 | env: 45 | RDBMS: sqlite 46 | TEST_PYTHON_VERSION: ${{ matrix.python-version }} 47 | TEST_DJANGO_VERSION: ${{ matrix.django-version }} 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v5 53 | id: sp 54 | with: 55 | python-version: ${{ matrix.python-version }} 56 | - name: Install uv 57 | uses: astral-sh/setup-uv@v6 58 | with: 59 | enable-cache: true 60 | - name: Install Just 61 | uses: extractions/setup-just@v3 62 | - name: Install Dependencies 63 | run: | 64 | just setup ${{ steps.sp.outputs.python-path }} 65 | just test-lock Django~=${{ matrix.django-version }}.0 66 | just install-docs 67 | - name: Install Emacs 68 | if: ${{ github.event.inputs.debug == 'true' }} 69 | run: | 70 | sudo apt install emacs 71 | - name: Setup tmate session 72 | if: ${{ github.event.inputs.debug == 'true' }} 73 | uses: mxschmitt/action-tmate@v3.22 74 | with: 75 | detached: true 76 | timeout-minutes: 60 77 | - name: Run Static Analysis 78 | run: | 79 | just manage makemigrations 80 | just test ./tests/test_verify_environment.py 81 | just check-lint 82 | just check-format 83 | just check-types 84 | just check-package 85 | just check-readme 86 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: OpenSSF Scorecard 2 | on: 3 | # For Branch-Protection check. Only the default branch is supported. See 4 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 5 | branch_protection_rule: 6 | # To guarantee Maintained check is occasionally updated. See 7 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 8 | push: 9 | branches: [ main ] 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecard analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | security-events: write 19 | id-token: write 20 | 21 | steps: 22 | - name: "Checkout code" 23 | uses: actions/checkout@v4 24 | with: 25 | persist-credentials: false 26 | 27 | - name: "Run analysis" 28 | uses: ossf/scorecard-action@v2.4.2 29 | with: 30 | results_file: results.sarif 31 | results_format: sarif 32 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 33 | # - you want to enable the Branch-Protection check on a *public* repository, or 34 | # - you are installing Scorecard on a *private* repository 35 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 36 | repo_token: ${{ secrets.SCORECARD_TOKEN }} 37 | 38 | # Public repositories: 39 | # - Publish results to OpenSSF REST API for easy access by consumers 40 | # - Allows the repository to include the Scorecard badge. 41 | # - See https://github.com/ossf/scorecard-action#publishing-results. 42 | # For private repositories: 43 | # - `publish_results` will always be set to `false`, regardless 44 | # of the value entered here. 45 | publish_results: true 46 | 47 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 48 | # format to the repository Actions tab. 49 | - name: "Upload artifact" 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: SARIF file 53 | path: results.sarif 54 | retention-days: 5 55 | 56 | # Upload the results to GitHub's code scanning dashboard (optional). 57 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 58 | - name: "Upload to code-scanning" 59 | uses: github/codeql-action/upload-sarif@v3 60 | with: 61 | sarif_file: results.sarif 62 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: Zizmor 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Run weekly 10 | - cron: '0 0 * * 0' 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | zizmor-analysis: 18 | name: Run Zizmor 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | security-events: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Set up Rust 31 | uses: actions-rust-lang/setup-rust-toolchain@v1 32 | 33 | - name: Install Zizmor 34 | run: | 35 | cargo install zizmor 36 | 37 | - name: Run Zizmor analysis 38 | run: | 39 | zizmor --format sarif .github/workflows/ > results.sarif 40 | 41 | - name: Upload analysis results 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: zizmor-results 45 | path: results.sarif 46 | retention-days: 7 47 | 48 | - name: Upload to code-scanning 49 | uses: github/codeql-action/upload-sarif@v3 50 | with: 51 | sarif_file: results.sarif 52 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: lint 5 | name: Lint 6 | entry: just lint 7 | language: system 8 | pass_filenames: false 9 | - id: format 10 | name: Format 11 | entry: just format 12 | language: system 13 | pass_filenames: false 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 Brian Kohan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | [![CodeQL](https://github.com/django-commons/django-enum/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/django-commons/django-enum/actions/workflows/github-code-scanning/codeql?query=branch:main) 4 | [![Zizmor](https://github.com/django-commons/django-enum/actions/workflows/zizmor.yml/badge.svg?branch=main)](https://woodruffw.github.io/zizmor) 5 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/django-commons/django-enum/badge)](https://securityscorecards.dev/viewer/?uri=github.com/django-commons/django-enum) 6 | 7 | ## Supported Versions 8 | 9 | Only the latest version [![PyPI version](https://badge.fury.io/py/django-enum.svg)](https://pypi.python.org/pypi/django-enum) is supported. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you think you have found a vulnerability, and even if you are not sure, please [report it to us in private](https://github.com/django-commons/django-enum/security/advisories/new). We will review it and get back to you. Please refrain from public discussions of the issue. 14 | -------------------------------------------------------------------------------- /doc/.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 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | jobs: 14 | post_install: 15 | - pip install uv 16 | - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: doc/source/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | formats: 24 | - pdf 25 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import os 3 | import sys 4 | from pathlib import Path 5 | from sphinx.ext.autodoc import between 6 | 7 | sys.path.append(str(Path(__file__).parent.parent.parent)) 8 | import django_enum 9 | 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 11 | 12 | # Configuration file for the Sphinx documentation builder. 13 | # 14 | # This file only contains a selection of the most common options. For a full 15 | # list see the documentation: 16 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 17 | 18 | # -- Path setup -------------------------------------------------------------- 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | # 24 | # import os 25 | # import sys 26 | # sys.path.insert(0, os.path.abspath('.')) 27 | 28 | 29 | # -- Project information ----------------------------------------------------- 30 | 31 | project = django_enum.__title__ 32 | copyright = django_enum.__copyright__ 33 | author = django_enum.__author__ 34 | release = django_enum.__version__ 35 | 36 | 37 | # -- General configuration --------------------------------------------------- 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinxcontrib_django', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.todo', 47 | 'sphinx_tabs.tabs', 48 | "sphinx.ext.viewcode", 49 | ] 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # List of patterns, relative to source directory, that match files and 55 | # directories to ignore when looking for source files. 56 | # This pattern also affects html_static_path and html_extra_path. 57 | exclude_patterns = [] 58 | 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | 62 | # The theme to use for HTML and HTML Help pages. See the documentation for 63 | # a list of builtin themes. 64 | # 65 | html_theme = 'furo' 66 | html_theme_options = { 67 | "source_repository": "https://github.com/django-commons/django-enum/", 68 | "source_branch": "main", 69 | "source_directory": "doc/source", 70 | } 71 | 72 | # Add any paths that contain custom static files (such as style sheets) here, 73 | # relative to this directory. They are copied after the builtin static files, 74 | # so a file named "default.css" will overwrite the builtin "default.css". 75 | html_static_path = [] 76 | 77 | todo_include_todos = True 78 | 79 | intersphinx_mapping = { 80 | "django": ( 81 | "https://docs.djangoproject.com/en/stable", 82 | "https://docs.djangoproject.com/en/stable/_objects/", 83 | ), 84 | "enum-properties": ("https://enum-properties.readthedocs.io/en/stable", None), 85 | "django-render-static": ("https://django-render-static.readthedocs.io/en/stable", None), 86 | "django-filter": ("https://django-filter.readthedocs.io/en/stable", None), 87 | "python": ('https://docs.python.org/3', None) 88 | } 89 | 90 | 91 | def setup(app): 92 | # Register a sphinx.ext.autodoc.between listener to ignore everything 93 | # between lines that contain the word IGNORE 94 | app.connect( 95 | 'autodoc-process-docstring', 96 | between('^.*[*]{79}.*$', exclude=True) 97 | ) 98 | return app 99 | -------------------------------------------------------------------------------- /doc/source/eccentric.rst: -------------------------------------------------------------------------------- 1 | .. include:: refs.rst 2 | 3 | .. _eccentric: 4 | 5 | =============== 6 | Eccentric Enums 7 | =============== 8 | 9 | Python's :class:`enum.Enum` type is extremely lenient. Enumeration values may be any hashable type 10 | and values of the same enumeration may be of different types. 11 | 12 | .. tip:: 13 | 14 | We define an eccentric enumeration to be any enumeration where the value type is not a simple 15 | string or integer or where the enumeration values are not all of the same type. 16 | 17 | For use in databases it is recommended to use more strict enumeration types that only allow a single 18 | value type of either string or integer. If additional properties need to be associated with 19 | enumeration values, a library like :doc:`enum-properties:index` should be used to store them on the 20 | enumeration value classes. 21 | 22 | However, the goal of django-enum_ is to provide as complete of a bridge as possible between Python 23 | and the database so eccentric enumerations are supported with caveats. The following enumeration 24 | value types are supported out of the box, and map to the obvious 25 | :ref:`model field type `. 26 | 27 | * :class:`int` 28 | * :class:`str` 29 | * :class:`float` 30 | * :class:`datetime.date` 31 | * :class:`datetime.datetime` 32 | * :class:`datetime.time` 33 | * :class:`datetime.timedelta` 34 | * :class:`decimal.Decimal` 35 | 36 | You should avoid eccentric enums if possible, but there may be some compelling reasons to use them. 37 | For example, for unusual data types it may make sense in situations where the database will be used 38 | in a non-Python context and the enumeration values need to retain their native meaning. Or you may 39 | not have direct control over the enumeration you want to store. 40 | 41 | Mixed Value Enumerations 42 | ======================== 43 | 44 | Mixed value enumerations are supported. For example: 45 | 46 | .. literalinclude:: ../../tests/examples/models/mixed_value.py 47 | :language: python 48 | 49 | 50 | :class:`~django_enum.fields.EnumField` will determine the most appropriate database column type to 51 | store the enumeration by trying each of the supported primitive types in order and selecting the 52 | first one that is symmetrically coercible to and from each enumeration value. ``None`` values are 53 | allowed and do not take part in the primitive type selection. In the above example, the database 54 | column type would default to a string. 55 | 56 | .. note:: 57 | 58 | If none of the supported primitive types are symmetrically coercible 59 | :class:`~django_enum.fields.EnumField` will not be able to determine an appropriate column 60 | type and a :exc:`ValueError` will be raised. 61 | 62 | In these cases, or to override the primitive type selection made by 63 | :class:`~django_enum.fields.EnumField`, pass the ``primitive`` parameter. It may be necessary to 64 | extend one of the supported primitives to make it coercible. It may also be necessary 65 | to override the :class:`enum.Enum` class's :meth:`~enum.Enum._missing_` method: 66 | 67 | .. literalinclude:: ../../tests/examples/mixed_value_example.py 68 | :language: python 69 | :lines: 4- 70 | 71 | In the above case since ``None`` is an enumeration value, :class:`~django_enum.fields.EnumField` 72 | will automatically set null=True on the model field. 73 | 74 | The above yields:: 75 | 76 | obj.eccentric_str= 77 | obj.eccentric_float= 78 | obj.eccentric_str= 79 | obj.eccentric_float= 80 | obj.eccentric_str= 81 | obj.eccentric_float= 82 | obj.eccentric_str= 83 | obj.eccentric_float= 84 | obj.eccentric_str= 85 | obj.eccentric_float= 86 | 87 | Custom Enum Value Types 88 | ======================= 89 | 90 | .. warning:: 91 | There is almost certainly a better way to do what you might be trying to do by writing a custom 92 | enumeration value - for example consider using :doc:`enum-properties:index` to make your 93 | enumeration types more robust by pushing more of this functionality on the :class:`enum.Enum` 94 | class itself. 95 | 96 | If you must use a custom value type, you can by specifying a symmetrically coercible primitive type. 97 | For example Path is already symmetrically coercible to str so this works: 98 | 99 | .. literalinclude:: ../../tests/examples/models/path_value.py 100 | :language: python 101 | 102 | 103 | A fully custom value might look like the following contrived example: 104 | 105 | .. literalinclude:: ../../tests/examples/models/custom_value.py 106 | :language: python 107 | -------------------------------------------------------------------------------- /doc/source/howto/admin.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _admin: 4 | 5 | ================ 6 | Use Django Admin 7 | ================ 8 | 9 | :class:`~django_enum.fields.EnumField` will mostly just work in the Django 10 | :mod:`~django.contrib.admin`. There is 11 | `one issue `_ where :ref:`enums that are 12 | not hash equivalent ` will not render value labels correctly in the 13 | :class:`~django.contrib.admin.ModelAdmin` :attr:`~django.contrib.admin.ModelAdmin.list_display`. 14 | -------------------------------------------------------------------------------- /doc/source/howto/external.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _external: 4 | 5 | ================== 6 | Use External Enums 7 | ================== 8 | 9 | :class:`enum.Enum` classes defined externally to your code base or enum classes that otherwise do 10 | not inherit from Django's :ref:`field-choices-enum-types`, are supported. When no choices are 11 | present on an :class:`enum.Enum` type, :class:`~django_enum.fields.EnumField` will attempt to use 12 | the ``label`` member on each enumeration value if it is present, otherwise the labels will be based 13 | off the enumeration name. Choices can also be overridden at the 14 | :class:`~django_enum.fields.EnumField` declaration. 15 | 16 | :class:`~django_enum.fields.EnumField` should work with any subclass of :class:`enum.Enum`. 17 | 18 | .. literalinclude:: ../../../tests/examples/models/extern.py 19 | 20 | The list of choice tuples for each field are: 21 | 22 | .. literalinclude:: ../../../tests/examples/extern_howto.py 23 | :lines: 3- 24 | 25 | .. warning:: 26 | 27 | One nice feature of Django's :ref:`field-choices-enum-types` are that they disable 28 | :class:`enum.auto` on :class:`enum.Enum` fields. :class:`enum.auto` can be dangerous because the 29 | values assigned depend on the order of declaration. This means that if the order changes 30 | existing database values will no longer align with the enumeration values. When control over the 31 | values is not certain it is a good idea to add integration tests that look for value changes. 32 | 33 | .. _hash_equivalency: 34 | 35 | Hash Equivalency 36 | ---------------- 37 | 38 | .. tip:: 39 | 40 | It is a good idea to make sure your enumeration instances are hash equivalent to their 41 | primitive values. You can do this simply by inheriting from their primitive value 42 | (e.g. ``class MyEnum(str, Enum):``) or by using :class:`~enum.StrEnum` and 43 | :class:`~enum.IntEnum` types. Any enumeration defined using :doc:`enum-properties:index` 44 | will be hash equivalent to its values by default. 45 | 46 | :class:`~django_enum.fields.EnumField` automatically sets the choices tuple on the field. Django_ 47 | has logic in a number of places that handles fields with choices in a special way 48 | (e.g. :ref:`in the admin `). For example, the choices may be converted to a dictionary 49 | mapping values to labels. The values will be the primitive values of the enumeration not 50 | enumeration instances and the current value of the field which may be an enumeration instance will 51 | be searched for in the dictionary. This will fail if the enumeration instance is not hash 52 | equivalent to its value. 53 | 54 | To control the hashing behavior of an object, you must override its :meth:`~object.__hash__` and 55 | :meth:`~object.__eq__` methods. 56 | 57 | For example: 58 | 59 | .. literalinclude:: ../../../tests/examples/models/hash_equivalency.py 60 | 61 | .. literalinclude:: ../../../tests/examples/hash_equivalency_howto.py 62 | :lines: 3- 63 | -------------------------------------------------------------------------------- /doc/source/howto/flags.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _flag_enums: 4 | 5 | ===================== 6 | Use Flags (BitFields) 7 | ===================== 8 | 9 | Python supports `bit fields `_ through the 10 | :class:`enum.Flag` extension to :class:`enum.Enum`. 11 | 12 | These enumerations are fully supported and will render as multi select form fields by default. For 13 | example: 14 | 15 | .. _group_permissions_ex: 16 | 17 | .. literalinclude:: ../../../tests/examples/models/flag_howto.py 18 | :lines: 2- 19 | 20 | .. literalinclude:: ../../../tests/examples/flag_howto.py 21 | :lines: 14-22 22 | 23 | **Two new field lookups are provided for flag enumerations:** :ref:`has_any` **and** :ref:`has_all`. 24 | 25 | .. _has_any: 26 | 27 | has_any 28 | ------- 29 | 30 | The :ref:`has_any` lookup will return any object that has at least one of the flags in the 31 | referenced enumeration. For example: 32 | 33 | .. literalinclude:: ../../../tests/examples/flag_howto.py 34 | :lines: 23-30 35 | 36 | .. _has_all: 37 | 38 | has_all 39 | ------- 40 | 41 | The :ref:`has_all` lookup will return any object that has at least all of the flags in the 42 | referenced enumeration. For example: 43 | 44 | .. literalinclude:: ../../../tests/examples/flag_howto.py 45 | :lines: 32- 46 | 47 | **There are performance considerations when using a bit mask like a Flag enumeration instead of 48 | multiple boolean columns.** See :ref:`flag performance ` for discussion and 49 | benchmarks. 50 | 51 | .. _large_flags: 52 | 53 | Flags with more than 64 bits 54 | ---------------------------- 55 | 56 | Flag enumerations of arbitrary size are supported, however if the enum has more than 64 flags it 57 | will be stored as a :class:`~django.db.models.BinaryField`. It is therefore strongly recommended to 58 | keep your :class:`enum.IntFlag` enumerations at 64 bits or less. 59 | 60 | .. warning:: 61 | 62 | Support for extra large flag fields is experimental. :ref:`has_any` and :ref:`has_all` do not 63 | work. Most RDBMS systems do not support bitwise operations on binary fields. Future work may 64 | involve exploring support for this as a Postgres extension. 65 | -------------------------------------------------------------------------------- /doc/source/howto/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | ====== 4 | How To 5 | ====== 6 | 7 | :class:`~django_enum.fields.EnumField` infers the primitive enumeration type and maps to the most 8 | appropriate Django_ field type. For example :class:`enum.StrEnum` types would become 9 | :class:`~django.db.models.CharField` and :class:`enum.IntEnum` types would become 10 | :class:`~django.db.models.PositiveSmallIntegerField` or 11 | :class:`~django.db.models.PositiveIntegerField` depending on the maximum enumeration value. 12 | 13 | This means that :class:`~django_enum.fields.EnumField` columns will behave as expected and integrate 14 | broadly with third party libraries. When issues arise it tends to be because the primitive type was 15 | marshalled into an :class:`enum.Enum` instance. :ref:`integrations` with some popular third party 16 | libraries are provided. 17 | 18 | For example: 19 | 20 | .. literalinclude:: ../../../tests/examples/models/equivalency.py 21 | 22 | 23 | ``txt_enum`` and ``txt_choices`` fields are equivalent in all ways with the 24 | following exceptions: 25 | 26 | .. literalinclude:: ../../../tests/examples/equivalency_howto.py 27 | :lines: 5- 28 | 29 | 30 | :class:`~django.forms.ModelForm` classes, DRF_ serializers and filters will behave the same way 31 | with ``txt_enum`` and ``txt_choices``. A few types are provided for deeper integration with forms 32 | and django-filter_ but their usage is optional. See :ref:`forms` and :ref:`filtering`. 33 | 34 | Very rich enumeration fields that encapsulate much more functionality in a simple declarative syntax 35 | are possible with :class:`~django_enum.fields.EnumField`. See :ref:`enum_props`. 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | :caption: How Tos: 40 | 41 | external 42 | options 43 | flags 44 | forms 45 | integrations 46 | migrations 47 | urls 48 | admin 49 | -------------------------------------------------------------------------------- /doc/source/howto/migrations.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _migrations: 4 | 5 | ================ 6 | Write Migrations 7 | ================ 8 | 9 | .. important:: 10 | 11 | There is one rule for writing custom migration files for EnumFields: 12 | **Never reference or import your enumeration classes in a migration file, 13 | work with the primitive values instead**. If your :class:`~enum.Enum` class 14 | changes over time, this can break older migration files. Always working with 15 | primitive values in migrations files will ensure that the migration will be 16 | valid to the data as it existed when the migration was generated. 17 | 18 | The deconstructed :class:`~django_enum.fields.EnumField` only include the choices tuple in the 19 | migration files. This is because :class:`enum.Enum` classes may come and go or be 20 | altered but the earlier migration files must still work. Simply treat any 21 | custom migration routines as if they were operating on a normal model field 22 | with choices. 23 | 24 | :class:`~django_enum.fields.EnumField` in migration files will not resolve the field values to 25 | enumeration types. The fields will be the primitive enumeration values as they 26 | are with any field with choices. 27 | 28 | 29 | Using :class:`enum.auto` 30 | ------------------------ 31 | 32 | If your :class:`~django_enum.fields.EnumField` is storing the value as the database column 33 | (default) it is best to avoid the usage of :class:`enum.auto` because the value for each 34 | enumerated instance may change which would bring your database out of sync with your 35 | codebase. 36 | 37 | If you have to use :class:`enum.auto` it is best to add integration tests to check for value 38 | changes. 39 | -------------------------------------------------------------------------------- /doc/source/howto/options.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _options: 4 | 5 | ==================== 6 | Configure EnumFields 7 | ==================== 8 | 9 | All parameters available to the equivalent model field with choices may be set directly in the 10 | :class:`~django_enum.fields.EnumField` instantiation. If not provided 11 | :class:`~django_enum.fields.EnumField` will set ``choices`` and ``max_length`` automatically. 12 | 13 | The following :class:`~django_enum.fields.EnumField` specific parameters are available: 14 | 15 | ``strict`` 16 | ---------- 17 | 18 | By default all :class:`~django_enum.fields.EnumField` are ``strict``. This means a 19 | :exc:`~django.core.exceptions.ValidationError` will be thrown anytime 20 | :meth:`~django.db.models.Model.full_clean` is run on a model and a value is set for the field that 21 | can not be coerced to its native :class:`~enum.Enum` type. To allow the field to store values that 22 | are not present in the fields :class:`~enum.Enum` type we can pass `strict=False`. 23 | 24 | Non-strict fields will be instances of the enumeration where a valid :class:`~enum.Enum` value is 25 | present and the plain old data where no :class:`~enum.Enum` type coercion is possible. 26 | 27 | .. literalinclude:: ../../../tests/examples/models/strict.py 28 | 29 | .. literalinclude:: ../../../tests/examples/strict_howto.py 30 | :lines: 4- 31 | 32 | 33 | ``constrained`` 34 | --------------- 35 | 36 | By default all strict :class:`~django_enum.fields.EnumField` are ``constrained``. This means that 37 | :doc:`CheckConstraints ` will be generated at the database level 38 | to ensure that the column will reject any value that is not present in the enumeration. This is a 39 | good idea for most use cases, but it can be turned off by setting ``constrained`` to ``False``. 40 | 41 | .. note:: 42 | 43 | This is new in version 2.0. If you are upgrading from a previous version, you may set 44 | this parameter to ``False`` to maintain the previous behavior. 45 | 46 | ``primitive`` 47 | ------------- 48 | 49 | :class:`~django_enum.fields.EnumField` dynamically determines the database column type by 50 | determining the most appropriate primitive type for the enumeration based on the enumeration 51 | values. You may override the primitive determined by :class:`~django_enum.fields.EnumField` by 52 | passing a type to the ``primitive`` parameter. You will likely not need to do this unless your 53 | enumeration is :ref:`eccentric ` in some way. 54 | 55 | ``coerce`` 56 | ---------- 57 | 58 | Setting this parameter to ``False`` will turn off the automatic conversion to 59 | the field's :class:`~enum.Enum` type while leaving all validation checks in place. It will 60 | still be possible to set the field directly as an :class:`~enum.Enum` instance and to 61 | filter by :class:`~enum.Enum` instance or any symmetric value: 62 | 63 | .. literalinclude:: ../../../tests/examples/models/no_coerce.py 64 | :lines: 13- 65 | 66 | .. literalinclude:: ../../../tests/examples/no_coerce_howto.py 67 | :lines: 6- 68 | -------------------------------------------------------------------------------- /doc/source/howto/urls.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _urls_howto: 4 | 5 | ================== 6 | Use Enums in URLs 7 | ================== 8 | 9 | django-enum_ provides a :ref:`converter ` that can be used to register enum url parameters 10 | with the Django_ path resolver. 11 | 12 | .. literalinclude:: ../../../tests/examples/urls.py 13 | 14 | By default the converter will use the value property of the enumeration to resolve the enumeration, 15 | but this can be overridden by passing the `prop` parameter, so we could for example use the 16 | name or label instead. 17 | 18 | The reversals for the above paths would look like this: 19 | 20 | .. literalinclude:: ../../../tests/examples/urls_howto.py 21 | :lines: 3- 22 | -------------------------------------------------------------------------------- /doc/source/plots/FlagSizeBenchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/plots/FlagSizeBenchmark.png -------------------------------------------------------------------------------- /doc/source/plots/IndexedExactQueryPerformance_postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/plots/IndexedExactQueryPerformance_postgres.png -------------------------------------------------------------------------------- /doc/source/plots/NoIndexQueryPerformance_postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/plots/NoIndexQueryPerformance_postgres.png -------------------------------------------------------------------------------- /doc/source/plots/QueryPerformance_postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/plots/QueryPerformance_postgresql.png -------------------------------------------------------------------------------- /doc/source/reference/DRF.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _drf_ref: 4 | 5 | === 6 | DRF 7 | === 8 | 9 | .. automodule:: django_enum.drf 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | :private-members: 14 | -------------------------------------------------------------------------------- /doc/source/reference/choices.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _choices_ref: 4 | 5 | ======= 6 | Choices 7 | ======= 8 | 9 | .. automodule:: django_enum.choices 10 | :members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /doc/source/reference/fields.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _fields: 4 | 5 | ====== 6 | Fields 7 | ====== 8 | 9 | .. note:: 10 | 11 | :class:`~django_enum.fields.EnumField` automatically determines the most appropriate database 12 | column type based on the :class:`~enum.Enum` subclass it is assigned to. 13 | 14 | .. automodule:: django_enum.fields 15 | :members: 16 | :show-inheritance: 17 | -------------------------------------------------------------------------------- /doc/source/reference/filters.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _filters: 4 | 5 | ======= 6 | Filters 7 | ======= 8 | 9 | .. automodule:: django_enum.filters 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | :private-members: 14 | 15 | -------------------------------------------------------------------------------- /doc/source/reference/forms.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _forms_ref: 4 | 5 | =========== 6 | Form Fields 7 | =========== 8 | 9 | .. autoclass:: django_enum.forms.ChoiceFieldMixin 10 | :members: 11 | :show-inheritance: 12 | 13 | .. autoclass:: django_enum.forms.EnumChoiceField 14 | :members: 15 | :show-inheritance: 16 | 17 | .. autoclass:: django_enum.forms.EnumFlagField 18 | :members: 19 | :show-inheritance: 20 | 21 | .. autoclass:: django_enum.forms.EnumMultipleChoiceField 22 | :members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /doc/source/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _reference: 4 | 5 | ========= 6 | Reference 7 | ========= 8 | 9 | .. automodule:: django_enum 10 | 11 | | 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Contents: 16 | 17 | fields 18 | choices 19 | filters 20 | forms 21 | widgets 22 | query 23 | DRF 24 | urls 25 | utils 26 | -------------------------------------------------------------------------------- /doc/source/reference/query.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _query: 4 | 5 | ===== 6 | Query 7 | ===== 8 | 9 | .. automodule:: django_enum.query 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | :private-members: 14 | -------------------------------------------------------------------------------- /doc/source/reference/urls.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _urls: 4 | 5 | ==== 6 | URLS 7 | ==== 8 | 9 | .. automodule:: django_enum.urls 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | :private-members: 14 | -------------------------------------------------------------------------------- /doc/source/reference/utils.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _utilities: 4 | 5 | ===== 6 | Utils 7 | ===== 8 | 9 | .. automodule:: django_enum.utils 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | :private-members: 14 | -------------------------------------------------------------------------------- /doc/source/reference/widgets.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _widgets_ref: 4 | 5 | ======= 6 | Widgets 7 | ======= 8 | 9 | The widgets below provide example renderings using these enums: 10 | 11 | .. _color_ex: 12 | .. literalinclude:: ../../../tests/examples/enums/color.py 13 | :lines: 5- 14 | 15 | .. _permissions_ex: 16 | .. literalinclude:: ../../../tests/examples/enums/permissions.py 17 | :lines: 5- 18 | 19 | 20 | Django's builtin :class:`~django.forms.Select` widget is the default widget used for 21 | :class:`~django_enum.fields.EnumField` fields. It renders a simple drop down select box. 22 | For example: 23 | 24 | .. code-block:: Python 25 | 26 | class Model(models.Model): 27 | color = EnumField(Color, default=Color.RED) 28 | 29 | Model.objects.create() 30 | 31 | .. image:: ../widgets/Select.png 32 | :alt: Select widget 33 | 34 | .. autoclass:: django_enum.forms.NonStrictSelect 35 | :members: 36 | :show-inheritance: 37 | 38 | .. autoclass:: django_enum.forms.NonStrictSelectMultiple 39 | :members: 40 | :show-inheritance: 41 | 42 | .. autoclass:: django_enum.forms.FlagSelectMultiple 43 | :members: 44 | :show-inheritance: 45 | 46 | .. autoclass:: django_enum.forms.FlagCheckbox 47 | :members: 48 | :show-inheritance: 49 | 50 | .. autoclass:: django_enum.forms.NonStrictFlagSelectMultiple 51 | :members: 52 | :show-inheritance: 53 | 54 | .. autoclass:: django_enum.forms.NonStrictFlagCheckbox 55 | :members: 56 | :show-inheritance: 57 | 58 | .. autoclass:: django_enum.forms.NonStrictRadioSelect 59 | :members: 60 | :show-inheritance: 61 | 62 | 63 | Mixins 64 | ------ 65 | 66 | .. autoclass:: django_enum.forms.NonStrictMixin 67 | :members: 68 | 69 | .. autoclass:: django_enum.forms.FlagMixin 70 | :members: 71 | 72 | .. autoclass:: django_enum.forms.NonStrictFlagMixin 73 | :members: 74 | -------------------------------------------------------------------------------- /doc/source/refs.rst: -------------------------------------------------------------------------------- 1 | .. _Python: https://www.python.org 2 | .. _Django: https://www.djangoproject.com 3 | .. _GitHub: https://github.com/django-commons/django-enum 4 | .. _PyPI: https://pypi.python.org/pypi/django-enum 5 | .. _PEP435: https://peps.python.org/pep-0435 6 | .. _DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself 7 | .. _DRF: https://www.django-rest-framework.org 8 | .. _django-enum: https://pypi.org/project/django-enum 9 | .. _django-filter: https://pypi.org/project/django-filter 10 | .. _djangorestframework: https://pypi.org/project/djangorestframework 11 | .. _PostgreSQL: https://www.postgresql.org 12 | .. _MySQL: https://www.mysql.com 13 | .. _MariaDB: https://mariadb.org 14 | .. _Oracle: https://www.oracle.com/database 15 | .. _SQLite: https://www.sqlite.org 16 | .. _cx-Oracle: https://pypi.org/project/cx-Oracle 17 | .. _psycopg2: https://pypi.org/project/psycopg2 18 | .. _psycopg3: https://pypi.org/project/psycopg 19 | .. _mysqlclient: https://pypi.org/project/mysqlclient 20 | -------------------------------------------------------------------------------- /doc/source/tutorials/flags.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _flags_bitfields: 4 | 5 | ================= 6 | Flags (BitFields) 7 | ================= 8 | 9 | There are many different Global Satellite Navigation Systems (GNSS) in operation today: 10 | 11 | * `GPS `_ 12 | * `GLONASS `_ 13 | * `Galileo `_ 14 | * `BeiDou `_ 15 | * `QZSS `_ 16 | * `IRNSS `_ 17 | 18 | GNSS receivers may understand or be configured to track one or more of these systems. If we wanted 19 | to build a data model of a GNSS receiver we would want to know which systems it can track. In 20 | Django_ we might do this using a collection of boolean fields like this: 21 | 22 | .. literalinclude:: ../../../tests/examples/models/gnss_vanilla.py 23 | 24 | Which would allow us to check for receiver compatibility and filter requirements like this: 25 | 26 | .. literalinclude:: ../../../tests/examples/gnss_vanilla_tutorial.py 27 | :lines: 3- 28 | 29 | This works pretty well. As our data scales though the waste of using an entire column for each 30 | boolean can add up. We can do better by using a single column as a bit field. 31 | 32 | Python has a built-in :class:`enum.IntFlag` type that is used to represent bit fields. Bit fields 33 | are useful for storing multiple boolean values in a single column. This is :ref:`much more space 34 | efficient `. :class:`~django_enum.fields.EnumField` supports :class:`enum.IntFlag` 35 | types out of the box. We could rewrite our GNSS receiver model like this: 36 | 37 | .. literalinclude:: ../../../tests/examples/models/gnss.py 38 | :lines: 2- 39 | 40 | And use it like this: 41 | 42 | .. literalinclude:: ../../../tests/examples/gnss_tutorial.py 43 | :lines: 3- 44 | 45 | The bit field model is much more compact and it does better in 46 | :ref:`space efficiency and query performance `. We also get some additional lookup 47 | types for :class:`~django_enum.fields.EnumField` that represent flags: :ref:`has_all` and 48 | :ref:`has_any`. 49 | 50 | The defined indexes are not necessarily used in our examples. They may be partially engaged for the 51 | :ref:`has_all` lookups but not for :ref:`has_any`. Our flag lookups perform optimized bit mask 52 | operations in the database. The bit field example will out perform the boolean example because it 53 | consumes much less memory while doing table scans with particular improvements in hard to index 54 | :ref:`has_any` queries. 55 | 56 | -------------------------------------------------------------------------------- /doc/source/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _tutorials: 4 | 5 | ========= 6 | Tutorials 7 | ========= 8 | 9 | Enumerations in Python can provide rich class based interfaces well suited to many use cases. We 10 | present several real world scenarios here that demonstrate the capability of django-enum_ to get 11 | your :class:`~django_enum.fields.EnumField` to do more work. 12 | 13 | In the :ref:`properties ` tutorial, we leverage :doc:`enum-properties:index` to 14 | encapsulate more information onto our :class:`~enum.Enum` values so that any information needed in 15 | different contexts is readily available without brittle mapping boilerplate. We also demonstrate 16 | symmetric properties that are comparison equivalent to our enumeration values. 17 | 18 | In the :ref:`flags ` tutorial, we demonstrate how to use :class:`enum.Flag` 19 | enumerations to represent bitfields in a database. This is a common pattern for storing multiple 20 | boolean values in a single column. 21 | 22 | | 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :caption: Tutorials: 27 | 28 | properties 29 | flags 30 | -------------------------------------------------------------------------------- /doc/source/tutorials/properties.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../refs.rst 2 | 3 | .. _properties: 4 | 5 | ========== 6 | Properties 7 | ========== 8 | 9 | To run this example, we'll need to install django-enum_ with 10 | :doc:`property support `: 11 | 12 | .. code-block:: bash 13 | 14 | > pip install "django-enum[properties]" 15 | 16 | MapBox Styles 17 | ------------- 18 | 19 | `MapBox `_ is a leading web mapping platform. It comes with a handful of default 20 | `map styles `_. An enumeration is a 21 | natural choice to represent these styles but the styles are complicated by versioning and are 22 | identified by different properties depending on context. When used as a parameter in the MapBox API 23 | they are in URI format, but in our interface we would prefer a more human friendly label, and in 24 | code we prefer the brevity and reliability of an :class:`~enum.Enum` value attribute. 25 | 26 | Each MapBox style enumeration is therefore composed of 4 primary properties: 27 | 28 | 1) A a human friendly label for the style 29 | 2) A name slug used in the URI 30 | 3) A version number for the style 31 | 4) The full URI specification of the style. 32 | 33 | Leveraging :class:`~enum_properties.IntEnumProperties` We might implement our style enumeration like 34 | so: 35 | 36 | .. literalinclude:: ../../../tests/examples/models/mapbox.py 37 | :language: python 38 | :lines: 2- 39 | 40 | We've used a small integer as the value of the enumeration to save storage space. We've also added a 41 | symmetric case insensitive slug and a version property. 42 | 43 | The version numbers will increment over time, but we're only concerned with the most recent 44 | versions, so we'll increment their values in this enumeration as they change. Any version number 45 | updates exist only in code and will be picked up as those persisted values are re-instantiated as 46 | ``MapBoxStyle`` enumerations. 47 | 48 | The last property we've added is the ``uri`` property. We've added it as concrete property on the 49 | class because it can be created from the slug and version. We could have specified it in the value 50 | tuple but that would be very verbose and less 51 | `DRY `_. To make this property symmetric we 52 | decorated it with :func:`~enum_properties.symmetric`. 53 | 54 | We can use our enumeration like so: 55 | 56 | .. literalinclude:: ../../../tests/examples/mapbox_tutorial.py 57 | :language: python 58 | :lines: 3- 59 | -------------------------------------------------------------------------------- /doc/source/widgets/FlagCheckbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/FlagCheckbox.png -------------------------------------------------------------------------------- /doc/source/widgets/FlagCheckbox_nulls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/FlagCheckbox_nulls.png -------------------------------------------------------------------------------- /doc/source/widgets/FlagSelectMultiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/FlagSelectMultiple.png -------------------------------------------------------------------------------- /doc/source/widgets/NonStrictFlagCheckbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/NonStrictFlagCheckbox.png -------------------------------------------------------------------------------- /doc/source/widgets/NonStrictFlagSelectMultiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/NonStrictFlagSelectMultiple.png -------------------------------------------------------------------------------- /doc/source/widgets/NonStrictRadioSelect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/NonStrictRadioSelect.png -------------------------------------------------------------------------------- /doc/source/widgets/NonStrictSelect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/NonStrictSelect.png -------------------------------------------------------------------------------- /doc/source/widgets/RadioSelect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/RadioSelect.png -------------------------------------------------------------------------------- /doc/source/widgets/RadioSelect_nulls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/RadioSelect_nulls.png -------------------------------------------------------------------------------- /doc/source/widgets/Select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/doc/source/widgets/Select.png -------------------------------------------------------------------------------- /src/django_enum/__init__.py: -------------------------------------------------------------------------------- 1 | r""" 2 | :: 3 | 4 | ____ _ _____ 5 | | _ \(_) __ _ _ __ __ _ ___ | ____|_ __ _ _ _ __ ___ 6 | | | | | |/ _` | '_ \ / _` |/ _ \ | _| | '_ \| | | | '_ ` _ \ 7 | | |_| | | (_| | | | | (_| | (_) | | |___| | | | |_| | | | | | | 8 | |____// |\__,_|_| |_|\__, |\___/ |_____|_| |_|\__,_|_| |_| |_| 9 | |__/ |___/ 10 | 11 | 12 | Full and natural support for enumerations as Django model fields. 13 | """ 14 | 15 | from django_enum.fields import EnumField 16 | 17 | __all__ = ["EnumField"] 18 | 19 | VERSION = (2, 2, 3) 20 | 21 | __title__ = "Django Enum" 22 | __version__ = ".".join(str(i) for i in VERSION) 23 | __author__ = "Brian Kohan" 24 | __license__ = "MIT" 25 | __copyright__ = "Copyright 2022-2025 Brian Kohan" 26 | -------------------------------------------------------------------------------- /src/django_enum/choices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for symmetrical property enumeration types derived from Django choice 3 | types. These choices types are drop in replacements for the Django 4 | IntegerChoices and TextChoices. 5 | """ 6 | 7 | import enum 8 | import typing as t 9 | 10 | from django import VERSION as django_version 11 | from django.db.models import Choices 12 | from django.db.models import IntegerChoices as DjangoIntegerChoices 13 | from django.db.models import TextChoices as DjangoTextChoices 14 | from django.db.models import enums as model_enums 15 | from enum_properties import DecomposeMixin, EnumPropertiesMeta, SymmetricMixin 16 | 17 | from django_enum.utils import choices, names 18 | 19 | ChoicesType = ( 20 | model_enums.ChoicesType 21 | if django_version[0:2] >= (5, 0) 22 | else model_enums.ChoicesMeta 23 | ) 24 | 25 | DEFAULT_BOUNDARY = getattr(enum, "KEEP", None) 26 | 27 | 28 | class DjangoEnumPropertiesMeta(EnumPropertiesMeta, ChoicesType): # type: ignore 29 | """ 30 | A composite meta class that combines Django's Choices metaclass with 31 | enum-properties metaclass. This metaclass will add Django's expected 32 | choices attribute and label properties to enumerations and 33 | enum-properties' generic property support. 34 | """ 35 | 36 | def __new__(mcs, classname, bases, classdict, **kwargs): 37 | cls = super().__new__(mcs, classname, bases, classdict, **kwargs) 38 | # choices does not allow duplicates, but base class construction breaks 39 | # this member, so we alias it here to stay compatible with enum-properties 40 | # interface 41 | # TODO - is this a fixable bug in ChoicesType? 42 | cls._member_names_ = ( 43 | list(classdict._member_names.keys()) 44 | if isinstance(classdict._member_names, dict) # changes based on py ver 45 | else classdict._member_names 46 | ) 47 | cls.__first_class_members__ = cls._member_names_ 48 | return cls 49 | 50 | @property 51 | def names(self) -> t.List[str]: 52 | """ 53 | For some eccentric enums list(Enum) is empty, so we override names 54 | if empty. 55 | 56 | :returns: list of enum value names 57 | """ 58 | return super().names or names(self, override=True) 59 | 60 | @property 61 | def choices(self) -> t.List[t.Tuple[t.Any, str]]: 62 | """ 63 | For some eccentric enums list(Enum) is empty, so we override 64 | choices if empty 65 | 66 | :returns: list of enum value choices 67 | """ 68 | return super().choices or choices(self, override=True) 69 | 70 | 71 | class DjangoSymmetricMixin(SymmetricMixin): 72 | """ 73 | An enumeration mixin that makes Django's Choices type label field 74 | symmetric. 75 | """ 76 | 77 | _symmetric_builtins_ = ["name", "label"] 78 | 79 | 80 | class TextChoices( 81 | DjangoSymmetricMixin, DjangoTextChoices, metaclass=DjangoEnumPropertiesMeta 82 | ): 83 | """ 84 | A character enumeration type that extends Django's TextChoices and 85 | accepts enum-properties property lists. 86 | """ 87 | 88 | def __hash__(self): 89 | return DjangoTextChoices.__hash__(self) 90 | 91 | 92 | class IntegerChoices( 93 | DjangoSymmetricMixin, DjangoIntegerChoices, metaclass=DjangoEnumPropertiesMeta 94 | ): 95 | """ 96 | An integer enumeration type that extends Django's IntegerChoices and 97 | accepts enum-properties property lists. 98 | """ 99 | 100 | def __hash__(self): 101 | return DjangoIntegerChoices.__hash__(self) 102 | 103 | 104 | class FloatChoices( 105 | DjangoSymmetricMixin, float, Choices, metaclass=DjangoEnumPropertiesMeta 106 | ): 107 | """ 108 | A floating point enumeration type that accepts enum-properties 109 | property lists. 110 | """ 111 | 112 | def __hash__(self): 113 | return float.__hash__(self) 114 | 115 | def __str__(self): 116 | return str(self.value) 117 | 118 | 119 | # mult inheritance type hint bug 120 | class FlagChoices( # type: ignore 121 | DecomposeMixin, 122 | DjangoSymmetricMixin, 123 | enum.IntFlag, 124 | Choices, 125 | metaclass=DjangoEnumPropertiesMeta, 126 | # default boundary argument gets lost in the inheritance when choices 127 | # is included if it is not explicitly specified 128 | **({"boundary": DEFAULT_BOUNDARY} if DEFAULT_BOUNDARY is not None else {}), 129 | ): 130 | """ 131 | An integer flag enumeration type that accepts enum-properties property 132 | lists. 133 | """ 134 | 135 | def __hash__(self): 136 | return enum.IntFlag.__hash__(self) 137 | -------------------------------------------------------------------------------- /src/django_enum/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/src/django_enum/py.typed -------------------------------------------------------------------------------- /src/django_enum/query.py: -------------------------------------------------------------------------------- 1 | """ 2 | Specialized has_any and has_all query lookups for flag enumerations. 3 | """ 4 | 5 | # from django.core.exceptions import FieldError 6 | from django.db.models.lookups import Exact 7 | 8 | # from django_enum.utils import get_set_bits 9 | 10 | 11 | class HasAllFlagsLookup(Exact): 12 | """ 13 | Extend Exact lookup to support lookup on has all flags. This lookup bitwise 14 | ANDs the column with the lookup value and checks that the result is equal 15 | to the lookup value. 16 | """ 17 | 18 | lookup_name = "has_all" 19 | 20 | def process_lhs(self, compiler, connection, lhs=None): 21 | lhs_sql, lhs_params = super().process_lhs(compiler, connection, lhs) 22 | rhs_sql, rhs_params = super().process_rhs(compiler, connection) 23 | if self.rhs: 24 | return ( 25 | "BITAND(%s, %s)" if connection.vendor == "oracle" else "%s & %s" 26 | ) % (lhs_sql, rhs_sql), [*lhs_params, *rhs_params] 27 | return lhs_sql, lhs_params 28 | 29 | def get_rhs_op(self, connection, rhs): 30 | return connection.operators["exact"] % rhs 31 | 32 | 33 | # class ExtraBigFlagMixin: 34 | # 35 | # def get_prep_lookup(self): 36 | # return self.lhs.output_field.to_python(super().get_prep_lookup()) 37 | # 38 | # def get_rhs_op(self, connection, rhs): 39 | # if connection.vendor == 'postgresql': 40 | # return connection.operators['exact'] % '1' 41 | # raise FieldError( 42 | # f'{connection.vendor} does not support {self.lookup_name} on ' 43 | # f'ExtraBigIntegerFlagFields.' 44 | # ) 45 | 46 | 47 | # class HasAllFlagsExtraBigLookup( 48 | # ExtraBigFlagMixin, 49 | # HasAllFlagsLookup 50 | # ): 51 | # """ 52 | # Support for bitwise has_all lookup on extra big integers (>64 bits) 53 | # stored as binary columns. 54 | # 55 | # get_bit(, 0) AND get_bit(, 7) = 1; 56 | # """ 57 | # 58 | # def process_lhs(self, compiler, connection, lhs=None): 59 | # lhs_sql, lhs_params = Exact.process_lhs( 60 | # self, 61 | # compiler, 62 | # connection, 63 | # lhs 64 | # ) 65 | # rhs_sql, rhs_params = Exact.process_rhs(self, compiler, connection) 66 | # bits = get_set_bits(rhs_params[0]) 67 | # if self.rhs: 68 | # ret = ' AND '.join( 69 | # [ 70 | # f'get_bit({lhs_sql}, %s)' for _ in range(len(bits)) 71 | # ] 72 | # ), bits 73 | # print(ret) 74 | # return ret 75 | # return lhs_sql, lhs_params 76 | 77 | 78 | class HasAnyFlagsLookup(HasAllFlagsLookup): 79 | """ 80 | Extend Exact lookup to support lookup on has any flags. This bitwise ANDs 81 | the column with the lookup value and checks that the result is greater 82 | than zero. 83 | """ 84 | 85 | lookup_name = "has_any" 86 | 87 | def process_rhs(self, compiler, connection): 88 | rhs_sql, rhs_params = super().process_rhs(compiler, connection) 89 | if rhs_params: 90 | rhs_params[0] = 0 91 | else: 92 | rhs_sql = "0" 93 | return rhs_sql, rhs_params 94 | 95 | def get_rhs_op(self, connection, rhs): 96 | return connection.operators["gt" if self.rhs else "exact"] % rhs 97 | 98 | 99 | # class HasAnyFlagsExtraBigLookup( 100 | # ExtraBigFlagMixin, 101 | # HasAnyFlagsLookup 102 | # ): 103 | # """ 104 | # Support for bitwise has_any lookup on extra big integers (>64 bits) 105 | # stored as binary columns. 106 | # """ 107 | # 108 | # def process_lhs(self, compiler, connection, lhs=None): 109 | # lhs_sql, lhs_params = Exact.process_lhs( 110 | # self, 111 | # compiler, 112 | # connection, 113 | # lhs 114 | # ) 115 | # rhs_sql, rhs_params = Exact.process_rhs(self, compiler, connection) 116 | # bits = get_set_bits(rhs_params[0]) 117 | # if self.rhs: 118 | # ret = ' OR '.join( 119 | # [ 120 | # f'get_bit({lhs_sql}, %s)' for _ in range(len(bits)) 121 | # ] 122 | # ), [*bits, 1] 123 | # print(ret) 124 | # return ret 125 | # return lhs_sql, lhs_params 126 | # 127 | # def process_rhs(self, compiler, connection): 128 | # rhs_sql, rhs_params = Exact.process_rhs(self, compiler, connection) 129 | # rhs_params[0] = 0 130 | # return rhs_sql, rhs_params 131 | -------------------------------------------------------------------------------- /src/django_enum/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | A metaclass and converter for Django's URL dispatcher to use with Python's 3 | Enum class. 4 | """ 5 | 6 | from enum import Enum 7 | from typing import Dict, Type 8 | 9 | from django.urls.converters import register_converter 10 | 11 | from django_enum.utils import determine_primitive 12 | 13 | __all__ = ["register_enum_converter"] 14 | 15 | 16 | class _EnumConverter: 17 | enum: Type[Enum] 18 | prop: str = "value" 19 | primitive: type 20 | 21 | _lookup_: Dict[str, Enum] 22 | 23 | def to_python(self, value: str) -> Enum: 24 | """ 25 | Convert the string representation of the enum into an instance of it. 26 | """ 27 | return self._lookup_[value] 28 | 29 | def to_url(self, value): 30 | """ 31 | Convert the given enumeration value to its url string. 32 | 33 | :param value: The enumeration value 34 | :return: the string representation of the enumeration value 35 | """ 36 | return str(getattr(value, self.prop)) 37 | 38 | 39 | def register_enum_converter(enum: Type[Enum], type_name="", prop="value"): 40 | """ 41 | Register an enum converter for Django's URL dispatcher. 42 | 43 | :param enum: The enumeration type to register. 44 | :param type_name: the name to use for the converter, defaults to the enum class 45 | name: 46 | 47 | .. code-block:: python 48 | 49 | path("", view, view_name) 50 | 51 | :param prop: The property name to use in the urls - by default the value is used. 52 | """ 53 | register_converter( 54 | type( 55 | f"{enum.__name__}Converter", 56 | (_EnumConverter,), 57 | { 58 | "enum": enum, 59 | "prop": prop, 60 | "primitive": determine_primitive(enum), 61 | "regex": "|".join([str(getattr(env, prop)) for env in enum]), 62 | "_lookup_": {str(getattr(env, prop)): env for env in enum}, 63 | }, 64 | ), 65 | type_name or enum.__name__, 66 | ) 67 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/__init__.py -------------------------------------------------------------------------------- /tests/benchmark/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/benchmark/__init__.py -------------------------------------------------------------------------------- /tests/benchmark/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BenchmarkConfig(AppConfig): 5 | name = "tests.benchmark" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/benchmark/enums.py: -------------------------------------------------------------------------------- 1 | from enum import IntFlag 2 | 3 | enums = [] 4 | 5 | for num_flags in range(0, 63): 6 | enums.append( 7 | IntFlag( 8 | f"Flags{num_flags:03d}", 9 | {f"FLG_{flg}": 2**flg for flg in range(0, num_flags + 1)}, 10 | ) 11 | ) 12 | 13 | 14 | class Index16(IntFlag): 15 | FLG_0 = 1 16 | FLG_1 = 2 17 | FLG_2 = 4 18 | FLG_3 = 8 19 | FLG_4 = 16 20 | FLG_5 = 32 21 | FLG_6 = 64 22 | FLG_7 = 128 23 | FLG_8 = 256 24 | FLG_9 = 512 25 | FLG_10 = 1024 26 | FLG_11 = 2048 27 | FLG_12 = 4096 28 | FLG_13 = 8192 29 | FLG_14 = 16384 30 | FLG_15 = 32768 31 | -------------------------------------------------------------------------------- /tests/benchmark/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/benchmark/migrations/__init__.py -------------------------------------------------------------------------------- /tests/benchmark/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import BooleanField, Index, Model 2 | 3 | from django_enum import EnumField 4 | from tests.benchmark.enums import Index16, enums 5 | 6 | 7 | def chop(original_list, limit=32): 8 | return [original_list[i : i + limit] for i in range(0, len(original_list), limit)] 9 | 10 | 11 | for num_flags in range(0, 63): 12 | globals()[f"FlagTester{num_flags:03d}"] = type( 13 | f"FlagTester{num_flags:03d}", 14 | (Model,), 15 | { 16 | f"flags": EnumField(enums[num_flags]), 17 | "__module__": "tests.benchmark.models", 18 | "FLAG": True, 19 | "num_flags": num_flags + 1, 20 | }, 21 | ) 22 | 23 | globals()[f"BoolTester{num_flags:03d}"] = type( 24 | f"BoolTester{num_flags:03d}", 25 | (Model,), 26 | { 27 | **{ 28 | f"flg_{flg}": BooleanField(default=False) 29 | for flg in range(0, num_flags + 1) 30 | }, 31 | "__module__": "tests.benchmark.models", 32 | "BOOL": True, 33 | "num_flags": num_flags + 1, 34 | # 'Meta': type( 35 | # 'Meta', 36 | # (), 37 | # { 38 | # 'indexes': [ 39 | # Index(fields=[ 40 | # f'flg_{flg}' for flg in flgs 41 | # ]) for flgs in chop(range(num_flags+1)) 42 | # ] 43 | # }) 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/constraints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/constraints/__init__.py -------------------------------------------------------------------------------- /tests/constraints/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ConstraintsConfig(AppConfig): 5 | name = "tests.constraints" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/constraints/enums.py: -------------------------------------------------------------------------------- 1 | from enum import IntFlag 2 | 3 | 4 | class IntFlagEnum(IntFlag): 5 | VAL1 = 2**12 6 | VAL2 = 2**13 7 | VAL3 = 2**14 8 | -------------------------------------------------------------------------------- /tests/constraints/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/constraints/migrations/__init__.py -------------------------------------------------------------------------------- /tests/constraints/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_enum import EnumField 4 | from tests.constraints.enums import IntFlagEnum 5 | 6 | 7 | class FlagConstraintTestModel(models.Model): 8 | flag_field = EnumField(IntFlagEnum, null=True, default=None, blank=True) 9 | 10 | flag_field_non_strict = EnumField( 11 | IntFlagEnum, null=True, default=None, blank=True, strict=False 12 | ) 13 | -------------------------------------------------------------------------------- /tests/converters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/converters/__init__.py -------------------------------------------------------------------------------- /tests/converters/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class Converters(AppConfig): 5 | name = "tests.converters" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/converters/urls.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from django.http import HttpResponse 4 | from django.urls import path 5 | 6 | from django_enum.urls import register_enum_converter 7 | from tests.djenum.enums import Constants, DecimalEnum 8 | 9 | 10 | class TestEnum(IntEnum): 11 | A = 1 12 | B = 2 13 | 14 | 15 | register_enum_converter(TestEnum) 16 | register_enum_converter(DecimalEnum, "decimal_enum") 17 | register_enum_converter(Constants, prop="label") 18 | 19 | record = [] 20 | 21 | 22 | def enum_converter_view(request, enum): 23 | record.append(enum) 24 | return HttpResponse(status=200) 25 | 26 | 27 | urlpatterns = [ 28 | path("", enum_converter_view, name="enum1_view"), 29 | path("", enum_converter_view, name="decimal_enum_view"), 30 | path("", enum_converter_view, name="constants_view"), 31 | ] 32 | -------------------------------------------------------------------------------- /tests/db_default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/db_default/__init__.py -------------------------------------------------------------------------------- /tests/db_default/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DBDefaultConfig(AppConfig): 5 | name = "tests.db_default" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/db_default/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/db_default/migrations/__init__.py -------------------------------------------------------------------------------- /tests/db_default/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.expressions import Value, F 3 | from django.db.models.functions import Concat 4 | from django.urls import reverse 5 | 6 | from django_enum import EnumField 7 | from tests.djenum.enums import ( 8 | BigIntEnum, 9 | BigPosIntEnum, 10 | Constants, 11 | DJIntEnum, 12 | DJTextEnum, 13 | ExternEnum, 14 | IntEnum, 15 | PosIntEnum, 16 | SmallIntEnum, 17 | SmallPosIntEnum, 18 | TextEnum, 19 | ) 20 | 21 | 22 | class DBDefaultTester(models.Model): 23 | small_pos_int = EnumField(SmallPosIntEnum, null=True, db_default=None, blank=True) 24 | small_int = EnumField( 25 | SmallIntEnum, null=False, db_default=SmallIntEnum.VAL3, blank=True 26 | ) 27 | 28 | small_int_shadow = EnumField( 29 | SmallIntEnum, null=False, db_default=Value(SmallIntEnum.VAL3.value), blank=True 30 | ) 31 | 32 | pos_int = EnumField(PosIntEnum, db_default=2147483647, blank=True) 33 | int = EnumField(IntEnum, null=True, db_default=IntEnum.VALn1, blank=True) 34 | 35 | big_pos_int = EnumField(BigPosIntEnum, null=True, db_default=None, blank=True) 36 | big_int = EnumField(BigIntEnum, db_default=-2147483649, blank=True) 37 | 38 | constant = EnumField( 39 | Constants, null=True, db_default=Constants.GOLDEN_RATIO, blank=True 40 | ) 41 | 42 | text = EnumField(TextEnum, db_default="", blank=True, strict=False) 43 | doubled_text = EnumField( 44 | TextEnum, 45 | default="", 46 | db_default=Concat(Value("db"), Value("_default")), 47 | blank=True, 48 | max_length=10, 49 | strict=False, 50 | ) 51 | doubled_text_strict = EnumField( 52 | TextEnum, 53 | default=TextEnum.DEFAULT, 54 | db_default=TextEnum.VALUE2, 55 | blank=True, 56 | max_length=10, 57 | ) 58 | 59 | char_field = models.CharField(db_default="db_default", blank=True, max_length=10) 60 | doubled_char_field = models.CharField( 61 | default="default", db_default="db_default", blank=True, max_length=10 62 | ) 63 | 64 | extern = EnumField(ExternEnum, null=True, db_default=ExternEnum.THREE, blank=True) 65 | 66 | dj_int_enum = EnumField(DJIntEnum, db_default=DJIntEnum.ONE) 67 | dj_text_enum = EnumField(DJTextEnum, db_default="A") 68 | 69 | # Non-strict 70 | non_strict_int = EnumField( 71 | SmallPosIntEnum, strict=False, null=True, db_default=5, blank=True 72 | ) 73 | 74 | non_strict_text = EnumField( 75 | TextEnum, 76 | max_length=12, 77 | strict=False, 78 | null=False, 79 | db_default="arbitrary", 80 | blank=True, 81 | ) 82 | 83 | no_coerce = EnumField( 84 | SmallPosIntEnum, 85 | coerce=False, 86 | null=True, 87 | db_default=SmallPosIntEnum.VAL2, 88 | blank=True, 89 | ) 90 | 91 | no_coerce_value = EnumField( 92 | SmallPosIntEnum, 93 | coerce=False, 94 | null=True, 95 | db_default=SmallPosIntEnum.VAL3.value, 96 | blank=True, 97 | ) 98 | 99 | no_coerce_none = EnumField( 100 | SmallPosIntEnum, coerce=False, null=True, db_default=None, blank=True 101 | ) 102 | 103 | class Meta: 104 | ordering = ("id",) 105 | -------------------------------------------------------------------------------- /tests/djenum/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/djenum/__init__.py -------------------------------------------------------------------------------- /tests/djenum/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django.forms import ModelForm, RadioSelect 4 | from django_enum.forms import ( 5 | NonStrictRadioSelect, 6 | FlagCheckbox, 7 | NonStrictFlagCheckbox, 8 | ) 9 | from django_enum.utils import decompose 10 | 11 | from tests.djenum.enums import TextEnum, GNSSConstellation 12 | from tests.djenum.models import ( 13 | AdminDisplayBug35, 14 | EnumTester, 15 | NullBlankFormTester, 16 | NullableBlankFormTester, 17 | Bug53Tester, 18 | NullableStrFormTester, 19 | AltWidgetTester, 20 | ) 21 | 22 | admin.site.register(EnumTester) 23 | admin.site.register(NullBlankFormTester) 24 | admin.site.register(NullableBlankFormTester) 25 | admin.site.register(Bug53Tester) 26 | admin.site.register(NullableStrFormTester) 27 | 28 | 29 | class AdminDisplayBug35Admin(admin.ModelAdmin): 30 | list_display = ( 31 | "text_enum", 32 | "int_enum", 33 | "status_basic", 34 | "status_basic_int", 35 | "status_str", 36 | "status_int", 37 | ) 38 | readonly_fields = ("text_enum", "int_enum", "blank_int", "blank_txt") 39 | 40 | 41 | admin.site.register(AdminDisplayBug35, AdminDisplayBug35Admin) 42 | 43 | 44 | class AltWidgetAdminForm(ModelForm): 45 | class Meta: 46 | model = AltWidgetTester 47 | fields = "__all__" 48 | widgets = { 49 | "text": RadioSelect, 50 | "text_null": RadioSelect, 51 | "text_non_strict": NonStrictRadioSelect, 52 | "constellation": FlagCheckbox, 53 | "constellation_null": FlagCheckbox, 54 | "constellation_non_strict": NonStrictFlagCheckbox, 55 | } 56 | 57 | 58 | class AltWidgetAdmin(admin.ModelAdmin): 59 | form = AltWidgetAdminForm 60 | list_display = ( 61 | "text", 62 | "text_null", 63 | "text_non_strict", 64 | "constellations", 65 | "constellations_null", 66 | "constellations_non_strict", 67 | ) 68 | 69 | def constellations(self, obj): 70 | return ", ".join([str(c.name) for c in decompose(obj.constellation)]) 71 | 72 | def constellations_null(self, obj): 73 | if obj.constellation_null is None: 74 | return "None" 75 | return ", ".join([str(c.name) for c in decompose(obj.constellation_null)]) 76 | 77 | def constellations_non_strict(self, obj): 78 | return ", ".join([str(c.name) for c in decompose(obj.constellation_non_strict)]) 79 | 80 | 81 | admin.site.register(AltWidgetTester, AltWidgetAdmin) 82 | -------------------------------------------------------------------------------- /tests/djenum/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DJEnumConfig(AppConfig): 5 | name = "tests.djenum" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/djenum/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm, Form 2 | from django_enum.forms import EnumMultipleChoiceField 3 | from tests.djenum.models import EnumTester 4 | from tests.djenum.enums import ( 5 | SmallPosIntEnum, 6 | SmallIntEnum, 7 | PosIntEnum, 8 | IntEnum, 9 | BigPosIntEnum, 10 | BigIntEnum, 11 | Constants, 12 | TextEnum, 13 | ExternEnum, 14 | DJIntEnum, 15 | DJTextEnum, 16 | DateEnum, 17 | DateTimeEnum, 18 | DecimalEnum, 19 | TimeEnum, 20 | DurationEnum, 21 | ) 22 | 23 | 24 | class EnumTesterForm(ModelForm): 25 | class Meta: 26 | model = EnumTester 27 | fields = "__all__" 28 | 29 | 30 | class EnumTesterMultipleChoiceForm(Form): 31 | small_pos_int = EnumMultipleChoiceField(SmallPosIntEnum) 32 | small_int = EnumMultipleChoiceField(SmallIntEnum) 33 | pos_int = EnumMultipleChoiceField(PosIntEnum) 34 | int = EnumMultipleChoiceField(IntEnum) 35 | big_pos_int = EnumMultipleChoiceField(BigPosIntEnum) 36 | big_int = EnumMultipleChoiceField(BigIntEnum) 37 | constant = EnumMultipleChoiceField(Constants) 38 | text = EnumMultipleChoiceField(TextEnum) 39 | extern = EnumMultipleChoiceField(ExternEnum) 40 | 41 | # Non-strict 42 | non_strict_int = EnumMultipleChoiceField(SmallPosIntEnum, strict=False) 43 | non_strict_text = EnumMultipleChoiceField(TextEnum, strict=False) 44 | no_coerce = EnumMultipleChoiceField(SmallPosIntEnum, strict=False) 45 | 46 | # eccentric enums 47 | date_enum = EnumMultipleChoiceField(DateEnum) 48 | datetime_enum = EnumMultipleChoiceField(DateTimeEnum) 49 | time_enum = EnumMultipleChoiceField(TimeEnum) 50 | duration_enum = EnumMultipleChoiceField(DurationEnum) 51 | decimal_enum = EnumMultipleChoiceField(DecimalEnum) 52 | -------------------------------------------------------------------------------- /tests/djenum/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/djenum/migrations/__init__.py -------------------------------------------------------------------------------- /tests/djenum/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from tests.djenum.models import EnumTester 4 | from tests.djenum.views import ( 5 | EnumTesterCreateView, 6 | EnumTesterDeleteView, 7 | EnumTesterDetailView, 8 | EnumTesterListView, 9 | EnumTesterUpdateView, 10 | FlagTesterCreateView, 11 | FlagTesterDeleteView, 12 | FlagTesterDetailView, 13 | FlagTesterListView, 14 | FlagTesterUpdateView, 15 | ) 16 | 17 | app_name = "tests_djenum" 18 | 19 | 20 | urlpatterns = [ 21 | path("enum/", EnumTesterDetailView.as_view(), name="enum-detail"), 22 | path("enum/list/", EnumTesterListView.as_view(), name="enum-list"), 23 | path("enum/add/", EnumTesterCreateView.as_view(), name="enum-add"), 24 | path("enum//", EnumTesterUpdateView.as_view(), name="enum-update"), 25 | path("enum//delete/", EnumTesterDeleteView.as_view(), name="enum-delete"), 26 | path("flag/", FlagTesterDetailView.as_view(), name="flag-detail"), 27 | path("flag/list/", FlagTesterListView.as_view(), name="flag-list"), 28 | path("flag/add/", FlagTesterCreateView.as_view(), name="flag-add"), 29 | path("flag//", FlagTesterUpdateView.as_view(), name="flag-update"), 30 | path("flag//delete/", FlagTesterDeleteView.as_view(), name="flag-delete"), 31 | ] 32 | 33 | 34 | try: 35 | from rest_framework import routers 36 | 37 | from tests.djenum.views import DRFView, DRFFlagView 38 | 39 | router = routers.DefaultRouter() 40 | router.register(r"enumtesters", DRFView) 41 | router.register(r"flagtesters", DRFFlagView) 42 | urlpatterns.append(path("drf/", include(router.urls))) 43 | 44 | except ImportError: # pragma: no cover 45 | pass 46 | 47 | 48 | try: 49 | from django_filters.views import FilterView 50 | 51 | from tests.djenum.views import ( 52 | EnumTesterFilterViewSet, 53 | EnumTesterFilterExcludeViewSet, 54 | EnumTesterMultipleFilterViewSet, 55 | EnumTesterMultipleFilterExcludeViewSet, 56 | FlagTesterFilterViewSet, 57 | FlagTesterFilterExcludeViewSet, 58 | FlagTesterFilterConjoinedViewSet, 59 | FlagTesterFilterConjoinedExcludeViewSet, 60 | ) 61 | 62 | urlpatterns.extend( 63 | [ 64 | path( 65 | "enum/filter/", 66 | FilterView.as_view( 67 | model=EnumTester, 68 | filterset_fields="__all__", 69 | template_name="enumtester_list.html", 70 | ), 71 | name="enum-filter", 72 | ), 73 | path( 74 | "enum/filter/viewset/", 75 | EnumTesterFilterViewSet.as_view(), 76 | name="enum-filter-viewset", 77 | ), 78 | path( 79 | "enum/filter/viewset/exclude", 80 | EnumTesterFilterExcludeViewSet.as_view(), 81 | name="enum-filter-viewset-exclude", 82 | ), 83 | path( 84 | "enum/filter/multiple/", 85 | EnumTesterMultipleFilterViewSet.as_view(), 86 | name="enum-filter-multiple", 87 | ), 88 | path( 89 | "enum/filter/multiple/exclude/", 90 | EnumTesterMultipleFilterExcludeViewSet.as_view(), 91 | name="enum-filter-multiple-exclude", 92 | ), 93 | path("flag/filter/", FlagTesterFilterViewSet.as_view(), name="flag-filter"), 94 | path( 95 | "flag/filter/exclude", 96 | FlagTesterFilterExcludeViewSet.as_view(), 97 | name="flag-filter-exclude", 98 | ), 99 | path( 100 | "flag/filter/conjoined", 101 | FlagTesterFilterConjoinedViewSet.as_view(), 102 | name="flag-filter-conjoined", 103 | ), 104 | path( 105 | "flag/filter/conjoined/exclude", 106 | FlagTesterFilterConjoinedExcludeViewSet.as_view(), 107 | name="flag-filter-conjoined-exclude", 108 | ), 109 | ] 110 | ) 111 | except ImportError: # pragma: no cover 112 | pass 113 | -------------------------------------------------------------------------------- /tests/edit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/edit_tests/__init__.py -------------------------------------------------------------------------------- /tests/edit_tests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EditTestsConfig(AppConfig): 5 | name = "tests.edit_tests" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_1.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | class IntEnum(models.IntegerChoices): 13 | ONE = 0, "One" 14 | TWO = ( 15 | 1, 16 | "Two", 17 | ) 18 | THREE = 2, "Three" 19 | 20 | class Color(TextChoices): 21 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 22 | hex: Annotated[str, Symmetric(case_fold=True)] 23 | 24 | RED = "R", "Red", (1, 0, 0), "ff0000" 25 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 26 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 27 | BLACK = "K", "Black", (0, 0, 0), "000000" 28 | 29 | int_enum = EnumField(IntEnum) 30 | color = EnumField(Color) 31 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_10.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | class IntEnum(models.TextChoices): 13 | A = "A", "One" 14 | B = ( 15 | "B", 16 | "Two", 17 | ) 18 | C = "C", "Three" 19 | 20 | class Color(TextChoices): 21 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 22 | hex: Annotated[str, Symmetric(case_fold=True)] 23 | 24 | RED = "R", "Red", (1, 0, 0), "ff0000" 25 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 26 | 27 | # change meaning of default indirectly 28 | BLUE = "B", "Blue", (0, 0, 1), "000000" 29 | BLACK = "K", "Black", (0, 0, 0), "0000ff" 30 | 31 | int_enum = EnumField(IntEnum, null=True, default=None) 32 | 33 | # default value unchanged 34 | color = EnumField(Color, default="000000") 35 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_2.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # alter values (incr by 1) 13 | class IntEnum(models.IntegerChoices): 14 | ONE = 1, "One" 15 | TWO = ( 16 | 2, 17 | "Two", 18 | ) 19 | THREE = 3, "Three" 20 | 21 | # unchanged 22 | class Color(TextChoices): 23 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 24 | hex: Annotated[str, Symmetric(case_fold=True)] 25 | 26 | RED = "R", "Red", (1, 0, 0), "ff0000" 27 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 28 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 29 | BLACK = "K", "Black", (0, 0, 0), "000000" 30 | 31 | int_enum = EnumField(IntEnum) 32 | color = EnumField(Color) 33 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_3.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # unchanged 13 | class IntEnum(models.IntegerChoices): 14 | ONE = 1, "One" 15 | TWO = ( 16 | 2, 17 | "Two", 18 | ) 19 | THREE = 3, "Three" 20 | 21 | # remove black 22 | class Color(TextChoices): 23 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 24 | hex: Annotated[str, Symmetric(case_fold=True)] 25 | 26 | # name value label rgb hex 27 | RED = "R", "Red", (1, 0, 0), "ff0000" 28 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 29 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 30 | 31 | int_enum = EnumField(IntEnum) 32 | color = EnumField(Color) 33 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_4.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # unchanged 13 | class IntEnum(models.IntegerChoices): 14 | ONE = 1, "One" 15 | TWO = ( 16 | 2, 17 | "Two", 18 | ) 19 | THREE = 3, "Three" 20 | 21 | # change enumeration names 22 | class Color(TextChoices): 23 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 24 | hex: Annotated[str, Symmetric(case_fold=True)] 25 | 26 | # name value label rgb hex 27 | RD = "R", "Red", (1, 0, 0), "ff0000" 28 | GR = "G", "Green", (0, 1, 0), "00ff00" 29 | BL = "B", "Blue", (0, 0, 1), "0000ff" 30 | 31 | # change strict and leave constrain on - should not generate a migration 32 | int_enum = EnumField(IntEnum, strict=False, constrained=True) 33 | color = EnumField(Color) 34 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_5.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # unchanged 13 | class IntEnum(models.IntegerChoices): 14 | ONE = 1, "One" 15 | TWO = ( 16 | 2, 17 | "Two", 18 | ) 19 | THREE = 3, "Three" 20 | 21 | # change enumeration names 22 | class Color(TextChoices): 23 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 24 | hex: Annotated[str, Symmetric(case_fold=True)] 25 | 26 | # name value label rgb hex 27 | RD = "R", "Red", (1, 0, 0), "ff0000" 28 | GR = "G", "Green", (0, 1, 0), "00ff00" 29 | BL = "B", "Blue", (0, 0, 1), "0000ff" 30 | 31 | # should remove the check constraint 32 | int_enum = EnumField(IntEnum, strict=False) 33 | color = EnumField(Color) 34 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_6.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # unchanged 13 | class IntEnum(models.IntegerChoices): 14 | ONE = 1, "One" 15 | TWO = ( 16 | 2, 17 | "Two", 18 | ) 19 | THREE = 3, "Three" 20 | FOUR = 32768, "Four" # force column size to increase 21 | 22 | # change enumeration names 23 | class Color(TextChoices): 24 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 25 | hex: Annotated[str, Symmetric(case_fold=True)] 26 | 27 | # name value label rgb hex 28 | RD = "R", "Red", (1, 0, 0), "ff0000" 29 | GR = "G", "Green", (0, 1, 0), "00ff00" 30 | BL = "B", "Blue", (0, 0, 1), "0000ff" 31 | 32 | # should remove the check constraint 33 | int_enum = EnumField(IntEnum, strict=False) 34 | color = EnumField(Color) 35 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_7.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # remove enumeration 13 | 14 | # no change 15 | class Color(TextChoices): 16 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 17 | hex: Annotated[str, Symmetric(case_fold=True)] 18 | 19 | # name value label rgb hex 20 | RD = "R", "Red", (1, 0, 0), "ff0000" 21 | GR = "G", "Green", (0, 1, 0), "00ff00" 22 | BL = "B", "Blue", (0, 0, 1), "0000ff" 23 | 24 | color = EnumField(Color) 25 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_8.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | # add enum back w/ same name but different type 13 | class IntEnum(models.TextChoices): 14 | A = "A", "One" 15 | B = ( 16 | "B", 17 | "Two", 18 | ) 19 | C = "C", "Three" 20 | 21 | class Color(TextChoices): 22 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 23 | hex: Annotated[str, Symmetric(case_fold=True)] 24 | 25 | RED = "R", "Red", (1, 0, 0), "ff0000" 26 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 27 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 28 | BLACK = "K", "Black", (0, 0, 0), "000000" 29 | 30 | int_enum = EnumField(IntEnum, null=True, default=None) 31 | color = EnumField(Color) 32 | -------------------------------------------------------------------------------- /tests/edit_tests/edits/_9.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from typing_extensions import Annotated 3 | 4 | from django.db import models 5 | from enum_properties import Symmetric 6 | 7 | from django_enum import EnumField 8 | from django_enum.choices import TextChoices 9 | 10 | 11 | class MigrationTester(models.Model): 12 | class IntEnum(models.TextChoices): 13 | A = "A", "One" 14 | B = ( 15 | "B", 16 | "Two", 17 | ) 18 | C = "C", "Three" 19 | 20 | class Color(TextChoices): 21 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 22 | hex: Annotated[str, Symmetric(case_fold=True)] 23 | 24 | RED = "R", "Red", (1, 0, 0), "ff0000" 25 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 26 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 27 | BLACK = "K", "Black", (0, 0, 0), "000000" 28 | 29 | int_enum = EnumField(IntEnum, null=True, default=None) 30 | 31 | # set default 32 | color = EnumField(Color, default="000000") 33 | -------------------------------------------------------------------------------- /tests/edit_tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/edit_tests/migrations/__init__.py -------------------------------------------------------------------------------- /tests/edit_tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/edit_tests/models.py -------------------------------------------------------------------------------- /tests/enum_prop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/enum_prop/__init__.py -------------------------------------------------------------------------------- /tests/enum_prop/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from tests.enum_prop.models import ( 4 | AdminDisplayBug35, 5 | BitFieldModel, 6 | EnumTester, 7 | ) 8 | 9 | 10 | class AdminDisplayBug35Admin(admin.ModelAdmin): 11 | list_display = ("text_enum", "int_enum") 12 | readonly_fields = ("text_enum", "int_enum", "blank_int", "blank_txt") 13 | 14 | 15 | admin.site.register(EnumTester) 16 | admin.site.register(BitFieldModel) 17 | admin.site.register(AdminDisplayBug35, AdminDisplayBug35Admin) 18 | -------------------------------------------------------------------------------- /tests/enum_prop/apps.py: -------------------------------------------------------------------------------- 1 | import enum_properties 2 | from django.apps import AppConfig 3 | 4 | 5 | class EnumPropConfig(AppConfig): 6 | name = "tests.enum_prop" 7 | label = name.replace(".", "_") 8 | -------------------------------------------------------------------------------- /tests/enum_prop/forms.py: -------------------------------------------------------------------------------- 1 | from django.db.models import BLANK_CHOICE_DASH 2 | from django.forms import ModelForm 3 | 4 | from django_enum.forms import EnumChoiceField 5 | from tests.enum_prop.enums import SmallPosIntEnum, TextEnum 6 | from tests.enum_prop.models import EnumTester 7 | 8 | 9 | class EnumTesterForm(ModelForm): 10 | no_coerce = EnumChoiceField( 11 | SmallPosIntEnum, 12 | initial=None, 13 | choices=BLANK_CHOICE_DASH + SmallPosIntEnum.choices, 14 | ) 15 | 16 | class Meta: 17 | model = EnumTester 18 | fields = "__all__" 19 | -------------------------------------------------------------------------------- /tests/enum_prop/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/enum_prop/migrations/__init__.py -------------------------------------------------------------------------------- /tests/enum_prop/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from tests.enum_prop.models import EnumTester 4 | from tests.enum_prop.views import ( 5 | EnumTesterCreateView, 6 | EnumTesterDeleteView, 7 | EnumTesterDetailView, 8 | EnumTesterListView, 9 | EnumTesterUpdateView, 10 | FlagTesterCreateView, 11 | FlagTesterDeleteView, 12 | FlagTesterDetailView, 13 | FlagTesterListView, 14 | FlagTesterUpdateView, 15 | ) 16 | 17 | app_name = "tests_enum_prop" 18 | 19 | urlpatterns = [ 20 | path("enum/", EnumTesterDetailView.as_view(), name="enum-detail"), 21 | path("enum/list/", EnumTesterListView.as_view(), name="enum-list"), 22 | path("enum/add/", EnumTesterCreateView.as_view(), name="enum-add"), 23 | path("enum//", EnumTesterUpdateView.as_view(), name="enum-update"), 24 | path("enum//delete/", EnumTesterDeleteView.as_view(), name="enum-delete"), 25 | path("flag/", FlagTesterDetailView.as_view(), name="flag-detail"), 26 | path("flag/list/", FlagTesterListView.as_view(), name="flag-list"), 27 | path("flag/add/", FlagTesterCreateView.as_view(), name="flag-add"), 28 | path("flag//", FlagTesterUpdateView.as_view(), name="flag-update"), 29 | path("flag//delete/", FlagTesterDeleteView.as_view(), name="flag-delete"), 30 | ] 31 | 32 | try: 33 | from rest_framework import routers 34 | 35 | from tests.enum_prop.views import DRFView, DRFFlagView 36 | 37 | router = routers.DefaultRouter() 38 | router.register(r"enumtesters", DRFView) 39 | router.register(r"flagtesters", DRFFlagView) 40 | urlpatterns.append(path("drf/", include(router.urls))) 41 | 42 | except ImportError: # pragma: no cover 43 | pass 44 | 45 | try: 46 | from django_filters.views import FilterView 47 | 48 | from tests.enum_prop.views import ( 49 | EnumTesterPropFilterViewSet, 50 | EnumTesterFilterExcludeViewSet, 51 | EnumTesterPropMultipleFilterViewSet, 52 | EnumTesterPropMultipleExcludeFilterViewSet, 53 | FlagTesterFilterViewSet, 54 | FlagTesterFilterExcludeViewSet, 55 | FlagTesterFilterConjoinedViewSet, 56 | FlagTesterFilterConjoinedExcludeViewSet, 57 | ) 58 | 59 | urlpatterns.extend( 60 | [ 61 | path( 62 | "enum/filter/symmetric/", 63 | EnumTesterPropFilterViewSet.as_view(), 64 | name="enum-filter-symmetric", 65 | ), 66 | path( 67 | "enum/filter/symmetric/exclude", 68 | EnumTesterFilterExcludeViewSet.as_view(), 69 | name="enum-filter-symmetric-exclude", 70 | ), 71 | path( 72 | "enum/filter/multiple/", 73 | EnumTesterPropMultipleFilterViewSet.as_view(), 74 | name="enum-filter-multiple", 75 | ), 76 | path( 77 | "enum/filter/multiple/exclude", 78 | EnumTesterPropMultipleExcludeFilterViewSet.as_view(), 79 | name="enum-filter-multiple-exclude", 80 | ), 81 | path( 82 | "flag/filter/", 83 | FlagTesterFilterViewSet.as_view(), 84 | name="flag-filter-symmetric", 85 | ), 86 | path( 87 | "flag/filter/exclude", 88 | FlagTesterFilterExcludeViewSet.as_view(), 89 | name="flag-filter-exclude-symmetric", 90 | ), 91 | path( 92 | "flag/filter/conjoined", 93 | FlagTesterFilterConjoinedViewSet.as_view(), 94 | name="flag-filter-conjoined-symmetric", 95 | ), 96 | path( 97 | "flag/filter/conjoined/exclude", 98 | FlagTesterFilterConjoinedExcludeViewSet.as_view(), 99 | name="flag-filter-conjoined-exclude-symmetric", 100 | ), 101 | ] 102 | ) 103 | except (ImportError, ModuleNotFoundError): # pragma: no cover 104 | pass 105 | -------------------------------------------------------------------------------- /tests/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/examples/__init__.py -------------------------------------------------------------------------------- /tests/examples/admin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.contrib import admin 3 | from django.forms import ModelForm, Select, RadioSelect 4 | from django_enum.forms import ( 5 | NonStrictRadioSelect, 6 | FlagSelectMultiple, 7 | FlagCheckbox, 8 | NonStrictFlagCheckbox, 9 | NonStrictFlagSelectMultiple, 10 | NonStrictSelect, 11 | NonStrictSelectMultiple 12 | ) 13 | from tests.examples.models import ( 14 | Map, 15 | BasicExample, 16 | FlagExample, 17 | NoCoerceExample, 18 | StrictExample, 19 | PropertyExample, 20 | ChoicesWithProperties, 21 | TextChoicesExample, 22 | WidgetDemoStrict, 23 | WidgetDemoNonStrict, 24 | WidgetDemoRadiosAndChecks, 25 | WidgetDemoRadiosAndChecksNonStrict, 26 | WidgetDemoRadiosAndChecksNulls 27 | ) 28 | 29 | admin.site.register(Map) 30 | admin.site.register(StrictExample) 31 | admin.site.register(NoCoerceExample) 32 | admin.site.register(PropertyExample) 33 | admin.site.register(BasicExample) 34 | admin.site.register(FlagExample) 35 | admin.site.register(ChoicesWithProperties) 36 | admin.site.register(TextChoicesExample) 37 | 38 | 39 | class WidgetDemoStrictAdminForm(ModelForm): 40 | class Meta: 41 | model = WidgetDemoStrict 42 | fields = "__all__" 43 | widgets = { 44 | "color": Select, 45 | "permissions": FlagSelectMultiple, 46 | } 47 | 48 | 49 | class WidgetDemoStrictAdmin(admin.ModelAdmin): 50 | form = WidgetDemoStrictAdminForm 51 | 52 | 53 | admin.site.register(WidgetDemoStrict, WidgetDemoStrictAdmin) 54 | 55 | 56 | class WidgetDemoNonStrictAdminForm(ModelForm): 57 | class Meta: 58 | model = WidgetDemoNonStrict 59 | fields = "__all__" 60 | widgets = { 61 | "color": NonStrictSelect, 62 | "permissions": NonStrictFlagSelectMultiple, 63 | } 64 | 65 | 66 | class WidgetDemoNonStrictAdmin(admin.ModelAdmin): 67 | form = WidgetDemoNonStrictAdminForm 68 | 69 | 70 | 71 | admin.site.register(WidgetDemoNonStrict, WidgetDemoNonStrictAdmin) 72 | 73 | 74 | class WidgetDemoRadiosAndChecksAdminForm(ModelForm): 75 | class Meta: 76 | model = WidgetDemoRadiosAndChecks 77 | fields = "__all__" 78 | widgets = { 79 | "color": RadioSelect, 80 | "permissions": FlagCheckbox, 81 | } 82 | 83 | 84 | class WidgetDemoRadiosAndChecksAdmin(admin.ModelAdmin): 85 | form = WidgetDemoRadiosAndChecksAdminForm 86 | 87 | 88 | class WidgetDemoRadiosAndChecksNullsAdmin(admin.ModelAdmin): 89 | form = WidgetDemoRadiosAndChecksAdminForm 90 | 91 | 92 | admin.site.register(WidgetDemoRadiosAndChecks, WidgetDemoRadiosAndChecksAdmin) 93 | admin.site.register(WidgetDemoRadiosAndChecksNulls, WidgetDemoRadiosAndChecksNullsAdmin) 94 | 95 | 96 | class WidgetDemoRadiosAndChecksNonStrictAdminForm(ModelForm): 97 | class Meta: 98 | model = WidgetDemoRadiosAndChecksNonStrict 99 | fields = "__all__" 100 | widgets = { 101 | "color": NonStrictRadioSelect, 102 | "permissions": NonStrictFlagCheckbox, 103 | } 104 | 105 | 106 | class WidgetDemoRadiosAndChecksNonStrictAdmin(admin.ModelAdmin): 107 | form = WidgetDemoRadiosAndChecksNonStrictAdminForm 108 | 109 | 110 | admin.site.register(WidgetDemoRadiosAndChecksNonStrict, WidgetDemoRadiosAndChecksNonStrictAdmin) 111 | -------------------------------------------------------------------------------- /tests/examples/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExamplesConfig(AppConfig): 5 | name = "tests.examples" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/examples/basic_example.py: -------------------------------------------------------------------------------- 1 | from .models.basic import BasicExample 2 | 3 | instance = BasicExample.objects.create( 4 | txt_enum=BasicExample.TextEnum.VALUE1, 5 | int_enum=3 # by-value assignment also works 6 | ) 7 | 8 | assert instance.txt_enum is BasicExample.TextEnum('V1') 9 | assert instance.txt_enum.label == 'Value 1' 10 | 11 | assert instance.int_enum is BasicExample.IntEnum.THREE 12 | assert instance.int_enum.value == 3 13 | -------------------------------------------------------------------------------- /tests/examples/checkboxes_form_howto.py: -------------------------------------------------------------------------------- 1 | from .models.flag_howto import Group 2 | from django.forms import Form 3 | from django_enum.forms import EnumFlagField, FlagCheckbox, NonStrictFlagCheckbox 4 | from django_enum import utils 5 | 6 | 7 | class PermissionsExampleForm(Form): 8 | 9 | permissions = EnumFlagField(Group.Permissions, widget=FlagCheckbox) 10 | 11 | # form fields can be non-strict just like model fields. 12 | # for this field we add an additional flag for delete and 13 | # set the field to be non-strict 14 | permissions_ext = EnumFlagField( 15 | Group.Permissions, 16 | strict=False, 17 | choices=[ 18 | *utils.choices(Group.Permissions), 19 | (1 << 3, "DELETE") 20 | ], 21 | widget=NonStrictFlagCheckbox 22 | ) 23 | 24 | 25 | form = PermissionsExampleForm( 26 | initial={ 27 | "permissions": Group.Permissions.READ | Group.Permissions.EXECUTE, 28 | "permissions_ext": Group.Permissions.READ | Group.Permissions.WRITE | (1 << 3) 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /tests/examples/choice_form_howto.py: -------------------------------------------------------------------------------- 1 | from .models.text_choices import TextChoicesExample 2 | from django.forms import Form 3 | from django_enum.forms import EnumChoiceField 4 | 5 | 6 | class TextChoicesExampleForm(Form): 7 | 8 | color = EnumChoiceField(TextChoicesExample.Color) 9 | 10 | # since this field is not strict, we can set it to a value not in our 11 | # enum or choice tuple. 12 | color_ext = EnumChoiceField( 13 | TextChoicesExample.Color, 14 | strict=False, 15 | choices=[ 16 | ('P', 'Purple'), 17 | ('O', 'Orange'), 18 | ] + TextChoicesExample.Color.choices 19 | ) 20 | 21 | # when this form is rendered in a template it will include a selected 22 | # option for the value 'Y' that is not part of our Color enumeration. 23 | form = TextChoicesExampleForm( 24 | initial={ 25 | "color": TextChoicesExample.Color.RED, 26 | "color_ext": "Y" 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /tests/examples/custom_value_example.py: -------------------------------------------------------------------------------- 1 | from .models.custom_value import CustomValueExample 2 | from django.db import models 3 | 4 | obj = CustomValueExample.objects.create( 5 | str_props=CustomValueExample.StrPropsEnum.STR2 6 | ) 7 | 8 | assert isinstance(obj._meta.get_field("str_props"), models.CharField) 9 | assert obj.str_props is CustomValueExample.StrPropsEnum.STR2 10 | assert obj.str_props.value.upper == "STR2" 11 | assert obj.str_props.value.lower == "str2" 12 | print(f"{obj.str_props=}") 13 | -------------------------------------------------------------------------------- /tests/examples/drf_modelserializer_howto.py: -------------------------------------------------------------------------------- 1 | from .models import TextChoicesExample 2 | 3 | from django_enum.drf import EnumFieldMixin 4 | from rest_framework import serializers 5 | 6 | 7 | class ExampleModelSerializer(EnumFieldMixin, serializers.Serializer): 8 | 9 | class Meta: 10 | model = TextChoicesExample 11 | 12 | 13 | ser = ExampleModelSerializer( 14 | data={ 15 | 'color': (1, 0, 0), 16 | } 17 | ) 18 | assert ser.is_valid() 19 | -------------------------------------------------------------------------------- /tests/examples/drf_serializer_howto.py: -------------------------------------------------------------------------------- 1 | from .models import TextChoicesExample, Constellation 2 | 3 | from django_enum.drf import EnumField, FlagField 4 | from rest_framework import serializers 5 | 6 | 7 | class ExampleSerializer(serializers.Serializer): 8 | 9 | color = EnumField(TextChoicesExample.Color) 10 | 11 | # from the flags tutorial 12 | constellation = FlagField(Constellation) 13 | 14 | 15 | ser = ExampleSerializer( 16 | data={ 17 | 'color': (1, 0, 0), 18 | 'constellation': [ 19 | Constellation.GALILEO.name, 20 | Constellation.GPS.name 21 | ] 22 | } 23 | ) 24 | assert ser.is_valid() 25 | -------------------------------------------------------------------------------- /tests/examples/enums/__init__.py: -------------------------------------------------------------------------------- 1 | from .color import Color 2 | from .permissions import Permissions 3 | -------------------------------------------------------------------------------- /tests/examples/enums/color.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from enum import Enum 3 | 4 | 5 | class Color(Enum): 6 | 7 | RED = 'R' 8 | GREEN = 'G' 9 | BLUE = 'B' 10 | -------------------------------------------------------------------------------- /tests/examples/enums/permissions.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from enum import IntFlag 3 | 4 | 5 | class Permissions(IntFlag): 6 | 7 | READ = 1 << 0 8 | WRITE = 1 << 1 9 | EXECUTE = 1 << 2 10 | -------------------------------------------------------------------------------- /tests/examples/equivalency_howto.py: -------------------------------------------------------------------------------- 1 | from .models.equivalency import EquivalencyExample 2 | from django.core.exceptions import ValidationError 3 | 4 | 5 | EquivalencyExample.objects.create(txt_enum='V0', txt_choices='V0') 6 | 7 | # txt_enum fields will always be an instance of the TextEnum type, unless 8 | # set to a value that is not part of the enumeration 9 | 10 | assert isinstance( 11 | EquivalencyExample.objects.first().txt_enum, 12 | EquivalencyExample.TextEnum 13 | ) 14 | assert isinstance( 15 | EquivalencyExample.objects.first().txt_choices, 16 | str 17 | ) 18 | 19 | # by default EnumFields are more strict, this is possible: 20 | EquivalencyExample.objects.create(txt_choices='AA') 21 | 22 | # but this will throw a ValueError (unless strict=False) 23 | try: 24 | EquivalencyExample.objects.create(txt_enum='AA') 25 | assert False 26 | except ValueError: 27 | assert True 28 | 29 | # and this will throw a ValidationError 30 | try: 31 | EquivalencyExample(txt_enum='AA').full_clean() 32 | assert False 33 | except ValidationError: 34 | assert True 35 | -------------------------------------------------------------------------------- /tests/examples/extern_howto.py: -------------------------------------------------------------------------------- 1 | from .models.extern import ExternalChoices 2 | 3 | assert ExternalChoices._meta.get_field('txt_enum1').choices == [ 4 | ('V0', 'VALUE0'), 5 | ('V1', 'VALUE1'), 6 | ('V2', 'VALUE2') 7 | ] 8 | 9 | assert ExternalChoices._meta.get_field('txt_enum2').choices == [ 10 | ('V0', 'Value0'), 11 | ('V1', 'Value1'), 12 | ('V2', 'Value2') 13 | ] 14 | -------------------------------------------------------------------------------- /tests/examples/filterfield_howto.py: -------------------------------------------------------------------------------- 1 | from .models import TextChoicesExample 2 | from django_enum.filters import EnumFilter 3 | from django_filters.views import FilterView 4 | from django_filters import FilterSet 5 | 6 | 7 | class TextChoicesExampleFilterViewSet(FilterView): 8 | 9 | class TextChoicesExampleFilter(FilterSet): 10 | 11 | color = EnumFilter(enum=TextChoicesExample.Color) 12 | 13 | class Meta: 14 | model = TextChoicesExample 15 | fields = '__all__' 16 | 17 | filterset_class = TextChoicesExampleFilter 18 | model = TextChoicesExample 19 | 20 | # now filtering by symmetric value in url parameters works: 21 | # e.g.: /?color=FF0000 22 | -------------------------------------------------------------------------------- /tests/examples/filterset_howto.py: -------------------------------------------------------------------------------- 1 | from .models import TextChoicesExample 2 | from django_enum.filters import FilterSet as EnumFilterSet 3 | from django_filters.views import FilterView 4 | 5 | 6 | class TextChoicesExampleFilterViewSet(FilterView): 7 | 8 | class TextChoicesExampleFilter(EnumFilterSet): 9 | class Meta: 10 | model = TextChoicesExample 11 | fields = '__all__' 12 | 13 | filterset_class = TextChoicesExampleFilter 14 | model = TextChoicesExample 15 | -------------------------------------------------------------------------------- /tests/examples/flag_example.py: -------------------------------------------------------------------------------- 1 | from .models import FlagExample 2 | from .models.flag import Permissions 3 | 4 | 5 | instance = FlagExample.objects.create( 6 | permissions=Permissions.READ | Permissions.WRITE | Permissions.EXECUTE 7 | ) 8 | 9 | # get all models with at least RW: 10 | assert instance in FlagExample.objects.filter( 11 | permissions__has_all=Permissions.READ | Permissions.WRITE 12 | ) 13 | -------------------------------------------------------------------------------- /tests/examples/flag_form_howto.py: -------------------------------------------------------------------------------- 1 | from .models.flag_howto import Group 2 | from django.forms import Form 3 | from django_enum.forms import EnumFlagField 4 | from django_enum import utils 5 | 6 | 7 | class PermissionsExampleForm(Form): 8 | 9 | permissions = EnumFlagField(Group.Permissions) 10 | 11 | # form fields can be non-strict just like model fields. 12 | # for this field we add an additional flag for delete and 13 | # set the field to be non-strict 14 | permissions_ext = EnumFlagField( 15 | Group.Permissions, 16 | strict=False, 17 | choices=[ 18 | *utils.choices(Group.Permissions), 19 | (1 << 3, "DELETE") 20 | ] 21 | ) 22 | 23 | 24 | form = PermissionsExampleForm( 25 | initial={ 26 | "permissions": Group.Permissions.READ | Group.Permissions.EXECUTE, 27 | "permissions_ext": Group.Permissions.READ | Group.Permissions.WRITE | (1 << 3) 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /tests/examples/flag_howto.py: -------------------------------------------------------------------------------- 1 | from .models.flag_howto import Group 2 | 3 | group = Group.objects.create( 4 | permissions=(Group.Permissions.READ | Group.Permissions.EXECUTE) 5 | ) 6 | 7 | group1 = Group.objects.create( 8 | permissions=(Group.Permissions.READ | Group.Permissions.EXECUTE) 9 | ) 10 | group2 = Group.objects.create( 11 | permissions=Group.Permissions.RWX 12 | ) 13 | 14 | assert Group.Permissions.READ in group1.permissions 15 | assert Group.Permissions.WRITE not in group1.permissions 16 | assert Group.Permissions.EXECUTE in group1.permissions 17 | 18 | assert Group.Permissions.READ in group2.permissions 19 | assert Group.Permissions.WRITE in group2.permissions 20 | assert Group.Permissions.EXECUTE in group2.permissions 21 | assert group2.permissions is Group.Permissions.RWX 22 | 23 | # this will return both group1 and group2 24 | read_or_write = Group.objects.filter( 25 | permissions__has_any=Group.Permissions.READ | Group.Permissions.WRITE 26 | ) 27 | assert ( 28 | group1 in read_or_write and 29 | group2 in read_or_write 30 | ) 31 | 32 | # this will return only group2 33 | read_and_write = Group.objects.filter( 34 | permissions__has_all=Group.Permissions.READ | Group.Permissions.WRITE 35 | ) 36 | assert ( 37 | group1 not in read_and_write and 38 | group2 in read_and_write 39 | ) 40 | -------------------------------------------------------------------------------- /tests/examples/flagfilterfield_howto.py: -------------------------------------------------------------------------------- 1 | from .models import Constellation, GNSSReceiver 2 | from django_enum.filters import EnumFlagFilter 3 | from django_filters.views import FilterView 4 | from django_filters import FilterSet 5 | 6 | 7 | class FlagExampleFilterViewSet(FilterView): 8 | 9 | class FlagExampleFilter(FilterSet): 10 | 11 | constellation = EnumFlagFilter(enum=Constellation) 12 | 13 | class Meta: 14 | model = GNSSReceiver 15 | fields = '__all__' 16 | 17 | filterset_class = FlagExampleFilter 18 | model = GNSSReceiver 19 | 20 | # now filtering by flags works: 21 | # e.g.: /?constellation=GPS&constellation=GLONASS 22 | -------------------------------------------------------------------------------- /tests/examples/gnss_tutorial.py: -------------------------------------------------------------------------------- 1 | from .models import GNSSReceiver, Constellation 2 | 3 | receiver1 = GNSSReceiver.objects.create( 4 | constellations=Constellation.GPS | Constellation.GLONASS 5 | ) 6 | 7 | receiver2 = GNSSReceiver.objects.create( 8 | constellations=( 9 | Constellation.GPS | 10 | Constellation.GLONASS | 11 | Constellation.GALILEO | 12 | Constellation.BEIDOU 13 | ) 14 | ) 15 | 16 | wanted = Constellation.GPS | Constellation.BEIDOU 17 | 18 | # check for GPS and BEIDOU 19 | assert not ( 20 | Constellation.GPS in receiver1.constellations and 21 | Constellation.BEIDOU in receiver1.constellations 22 | ) 23 | assert ( 24 | Constellation.GPS in receiver2.constellations and 25 | Constellation.BEIDOU in receiver2.constellations 26 | ) 27 | 28 | # we can treat IntFlags like bit masks so we can also check for having at 29 | # least GPS and BEIDOU like this: 30 | assert not wanted & receiver1.constellations == wanted 31 | assert wanted & receiver2.constellations == wanted 32 | 33 | # get all receives that have at least GPS and BEIDOU 34 | qry = GNSSReceiver.objects.filter(constellations__has_all=wanted) 35 | assert receiver1 not in qry 36 | assert receiver2 in qry 37 | 38 | # get all receivers that have either GPS or BEIDOU 39 | qry = GNSSReceiver.objects.filter(constellations__has_any=wanted) 40 | assert receiver1 in qry 41 | assert receiver2 in qry 42 | -------------------------------------------------------------------------------- /tests/examples/gnss_vanilla_tutorial.py: -------------------------------------------------------------------------------- 1 | from .models import GNSSReceiverBasic 2 | from django.db.models import Q 3 | 4 | receiver1 = GNSSReceiverBasic.objects.create( 5 | gps=True, 6 | glonass=True 7 | ) 8 | 9 | receiver2 = GNSSReceiverBasic.objects.create( 10 | gps=True, 11 | glonass=True, 12 | galileo=True, 13 | beidou=True 14 | ) 15 | 16 | # check for GPS and BEIDOU 17 | assert not (receiver1.gps and receiver1.beidou) 18 | assert receiver2.gps and receiver2.beidou 19 | 20 | # get all receives that have at least GPS and BEIDOU 21 | qry = GNSSReceiverBasic.objects.filter(Q(gps=True) & Q(beidou=True)) 22 | assert receiver1 not in qry 23 | assert receiver2 in qry 24 | 25 | # get all receivers that have either GPS or BEIDOU 26 | qry = GNSSReceiverBasic.objects.filter(Q(gps=True) | Q(beidou=True)) 27 | assert receiver1 in qry 28 | assert receiver2 in qry 29 | -------------------------------------------------------------------------------- /tests/examples/hash_equivalency_howto.py: -------------------------------------------------------------------------------- 1 | from .models.hash_equivalency import HashEquivalencyExample 2 | 3 | 4 | obj = HashEquivalencyExample.objects.create( 5 | not_hash_eq=HashEquivalencyExample.NotHashEq.VALUE1, 6 | hash_eq=HashEquivalencyExample.HashEq.VALUE1, 7 | hash_eq_str=HashEquivalencyExample.HashEqStr.VALUE1 8 | ) 9 | 10 | # direct comparisons to values do not work 11 | assert obj.not_hash_eq != "V1" 12 | 13 | # unless you have provided __eq__ or inherited from the primitive 14 | assert obj.hash_eq == obj.hash_eq_str == "V1" 15 | 16 | # here is the problem that can break some Django internals in rare instances: 17 | assert dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices) == { 18 | "V1": "VALUE1", 19 | "V2": "VALUE2", 20 | "V3": "VALUE3" 21 | } 22 | 23 | try: 24 | dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices)[ 25 | HashEquivalencyExample.NotHashEq.VALUE1 26 | ] 27 | assert False 28 | except KeyError: 29 | assert True 30 | 31 | # if we've made our enum hash equivalent though, this works: 32 | assert dict(HashEquivalencyExample._meta.get_field("hash_eq").flatchoices)[ 33 | HashEquivalencyExample.HashEq.VALUE1 34 | ] == "VALUE1" 35 | assert dict(HashEquivalencyExample._meta.get_field("hash_eq_str").flatchoices)[ 36 | HashEquivalencyExample.HashEqStr.VALUE1 37 | ] == "VALUE1" 38 | -------------------------------------------------------------------------------- /tests/examples/mapbox_tutorial.py: -------------------------------------------------------------------------------- 1 | from .models.mapbox import Map 2 | 3 | map = Map.objects.create() 4 | 5 | assert map.style.uri == 'mapbox://styles/mapbox/streets-v12' 6 | 7 | # uri's are symmetric 8 | map.style = 'mapbox://styles/mapbox/light-v11' 9 | map.full_clean() 10 | assert map.style is Map.MapBoxStyle.LIGHT 11 | 12 | # comparisons can be made directly to symmetric property values 13 | assert map.style == 3 14 | assert map.style == 'light' 15 | assert map.style == 'mapbox://styles/mapbox/light-v11' 16 | 17 | # so are labels (also case insensitive) 18 | map.style = 'satellite streets' 19 | map.full_clean() 20 | assert map.style is Map.MapBoxStyle.SATELLITE_STREETS 21 | 22 | # when used in API calls (coerced to strings) - they "do the right thing" 23 | assert str(map.style) == 'mapbox://styles/mapbox/satellite-streets-v12' 24 | -------------------------------------------------------------------------------- /tests/examples/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/examples/migrations/__init__.py -------------------------------------------------------------------------------- /tests/examples/mixed_value_example.py: -------------------------------------------------------------------------------- 1 | from .models.mixed_value import MixedValueExample, MixedValueEnum 2 | from django.db import models 3 | 4 | obj = MixedValueExample.objects.create( 5 | eccentric_str=MixedValueEnum.NONE, 6 | eccentric_float=MixedValueEnum.NONE 7 | ) 8 | 9 | assert isinstance(obj._meta.get_field("eccentric_str"), models.CharField) 10 | assert isinstance(obj._meta.get_field("eccentric_float"), models.FloatField) 11 | 12 | for en in list(MixedValueEnum): 13 | obj.eccentric_str = en 14 | obj.eccentric_float = en 15 | obj.save() 16 | obj.refresh_from_db() 17 | assert obj.eccentric_str is en 18 | assert obj.eccentric_float is en 19 | print(f"{obj.eccentric_str=}") 20 | print(f"{obj.eccentric_float=}") 21 | -------------------------------------------------------------------------------- /tests/examples/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .mapbox import Map 2 | from .strict import StrictExample 3 | from .no_coerce import NoCoerceExample 4 | from .properties import PropertyExample 5 | from .properties_choices import ChoicesWithProperties 6 | from .basic import BasicExample 7 | from .flag import FlagExample 8 | from .mixed_value import MixedValueExample 9 | from .path_value import PathValueExample 10 | from .custom_value import CustomValueExample 11 | from .gnss import GNSSReceiver, Constellation 12 | from .gnss_vanilla import GNSSReceiverBasic 13 | from .equivalency import EquivalencyExample 14 | from .hash_equivalency import HashEquivalencyExample 15 | from .extern import ExternalChoices 16 | from .flag_howto import Group 17 | from .text_choices import TextChoicesExample 18 | from .widgets import ( 19 | WidgetDemoStrict, 20 | WidgetDemoNonStrict, 21 | WidgetDemoRadiosAndChecks, 22 | WidgetDemoRadiosAndChecksNonStrict, 23 | WidgetDemoRadiosAndChecksNulls 24 | ) 25 | 26 | 27 | __all__ = [ 28 | "Map", 29 | "StrictExample", 30 | "NoCoerceExample", 31 | "PropertyExample", 32 | "BasicExample", 33 | "FlagExample", 34 | "MixedValueExample", 35 | "PathValueExample", 36 | "CustomValueExample", 37 | "GNSSReceiver", 38 | "Constellation", 39 | "GNSSReceiverBasic", 40 | "EquivalencyExample", 41 | "HashEquivalencyExample", 42 | "ExternalChoices", 43 | "Group", 44 | "TextChoicesExample", 45 | "ChoicesWithProperties", 46 | "WidgetDemoStrict", 47 | "WidgetDemoNonStrict", 48 | "WidgetDemoRadiosAndChecks", 49 | "WidgetDemoRadiosAndChecksNonStrict", 50 | "WidgetDemoRadiosAndChecksNulls" 51 | ] 52 | -------------------------------------------------------------------------------- /tests/examples/models/basic.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from django.db import models 3 | from django_enum import EnumField 4 | 5 | 6 | class BasicExample(models.Model): 7 | 8 | class TextEnum(models.TextChoices): 9 | 10 | VALUE0 = "V0", "Value 0" 11 | VALUE1 = "V1", "Value 1" 12 | VALUE2 = "V2", "Value 2" 13 | 14 | class IntEnum(models.IntegerChoices): 15 | 16 | # fmt: off 17 | ONE = 1, "One" 18 | TWO = 2, "Two" 19 | THREE = 3, "Three" 20 | # fmt: on 21 | 22 | # this is equivalent to: 23 | # CharField(max_length=2, choices=TextEnum.choices, null=True, blank=True) 24 | txt_enum = EnumField(TextEnum, null=True, blank=True, default=None) 25 | 26 | # this is equivalent to 27 | # PositiveSmallIntegerField(choices=IntEnum.choices, default=IntEnum.ONE.value) 28 | int_enum = EnumField(IntEnum, default=IntEnum.ONE) 29 | -------------------------------------------------------------------------------- /tests/examples/models/custom_value.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from django.db import models 3 | from django_enum import EnumField 4 | 5 | 6 | class StrProps: 7 | """ 8 | Wrap a string with some properties. 9 | """ 10 | 11 | _str = '' 12 | 13 | def __init__(self, string): 14 | self._str = string 15 | 16 | def __str__(self): 17 | """ 18 | coercion to str - str(StrProps('str1')) == 'str1' 19 | """ 20 | return self._str 21 | 22 | @property 23 | def upper(self): 24 | return self._str.upper() 25 | 26 | @property 27 | def lower(self): 28 | return self._str.lower() 29 | 30 | def __eq__(self, other): 31 | """ 32 | Make sure StrProps('str1') == 'str1' 33 | """ 34 | if isinstance(other, str): 35 | return self._str == other 36 | if other is not None: 37 | return self._str == other._str 38 | return False 39 | 40 | def deconstruct(self): 41 | """ 42 | Necessary to construct choices and default in migration files 43 | """ 44 | return ( 45 | f'{self.__class__.__module__}.{self.__class__.__qualname__}', 46 | (self._str,), 47 | {} 48 | ) 49 | 50 | 51 | class CustomValueExample(models.Model): 52 | 53 | class StrPropsEnum(Enum): 54 | 55 | STR1 = StrProps('str1') 56 | STR2 = StrProps('str2') 57 | STR3 = StrProps('str3') 58 | 59 | str_props = EnumField(StrPropsEnum, primitive=str) 60 | -------------------------------------------------------------------------------- /tests/examples/models/equivalency.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_enum import EnumField 3 | 4 | 5 | class EquivalencyExample(models.Model): 6 | 7 | class TextEnum(models.TextChoices): 8 | 9 | VALUE0 = 'V0', 'Value 0' 10 | VALUE1 = 'V1', 'Value 1' 11 | VALUE2 = 'V2', 'Value 2' 12 | 13 | txt_enum = EnumField(TextEnum, null=True, blank=True, default=None) 14 | 15 | txt_choices = models.CharField( 16 | max_length=2, 17 | choices=TextEnum.choices, 18 | null=True, 19 | blank=True, 20 | default=None 21 | ) 22 | -------------------------------------------------------------------------------- /tests/examples/models/extern.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from django.db import models 3 | from django_enum import EnumField 4 | 5 | 6 | class ExternalChoices(models.Model): 7 | 8 | class TextEnum(str, Enum): 9 | 10 | VALUE0 = 'V0' 11 | VALUE1 = 'V1' 12 | VALUE2 = 'V2' 13 | 14 | # choices will default to (value, name) pairs 15 | txt_enum1 = EnumField(TextEnum) 16 | 17 | # you can also override choices 18 | txt_enum2 = EnumField( 19 | TextEnum, 20 | choices=[(en.value, en.name.title()) for en in TextEnum] 21 | ) 22 | -------------------------------------------------------------------------------- /tests/examples/models/flag.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from django.db import models 3 | from enum import IntFlag 4 | from django_enum import EnumField 5 | 6 | 7 | class Permissions(IntFlag): 8 | 9 | # fmt: off 10 | READ = 1<<0 11 | WRITE = 1<<1 12 | EXECUTE = 1<<2 13 | # fmt: on 14 | 15 | 16 | class FlagExample(models.Model): 17 | 18 | permissions = EnumField(Permissions, null=True, blank=True) 19 | -------------------------------------------------------------------------------- /tests/examples/models/flag_howto.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from enum import IntFlag 3 | from django_enum import EnumField 4 | from django.db import models 5 | 6 | 7 | class Group(models.Model): 8 | 9 | class Permissions(IntFlag): 10 | 11 | # fmt: off 12 | READ = 1 << 0 13 | WRITE = 1 << 1 14 | EXECUTE = 1 << 2 15 | 16 | # IntFlags can have composite values! 17 | RWX = READ | WRITE | EXECUTE 18 | # fmt: on 19 | 20 | permissions = EnumField(Permissions) 21 | -------------------------------------------------------------------------------- /tests/examples/models/gnss.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from django.db import models 3 | from enum import IntFlag 4 | from django_enum import EnumField 5 | 6 | 7 | class Constellation(IntFlag): 8 | 9 | # fmt: off 10 | GPS = 1 << 0 # 1 11 | GLONASS = 1 << 1 # 2 12 | GALILEO = 1 << 2 # 4 13 | BEIDOU = 1 << 3 # 8 14 | QZSS = 1 << 4 # 16 15 | IRNSS = 1 << 5 # 32 16 | # fmt: on 17 | 18 | 19 | class GNSSReceiver(models.Model): 20 | 21 | constellations = EnumField(Constellation, db_index=True) 22 | -------------------------------------------------------------------------------- /tests/examples/models/gnss_vanilla.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class GNSSReceiverBasic(models.Model): 5 | 6 | gps = models.BooleanField(default=False) 7 | glonass = models.BooleanField(default=False) 8 | galileo = models.BooleanField(default=False) 9 | beidou = models.BooleanField(default=False) 10 | qzss = models.BooleanField(default=False) 11 | irnss = models.BooleanField(default=False) 12 | sbas = models.BooleanField(default=False) 13 | 14 | class Meta: 15 | 16 | # we can create an index for all fields, which will speed up queries for 17 | # exact matches on these fields 18 | indexes = [ 19 | models.Index(fields=[ 20 | 'gps', 21 | 'glonass', 22 | 'galileo', 23 | 'beidou', 24 | 'qzss', 25 | 'irnss', 26 | 'sbas' 27 | ]) 28 | ] 29 | -------------------------------------------------------------------------------- /tests/examples/models/hash_equivalency.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from django.db.models import Model 3 | from django_enum import EnumField 4 | 5 | 6 | class HashEquivalencyExample(Model): 7 | """ 8 | This example model defines three enum fields. The first uses an enum that 9 | is not hash equivalent to its values. The second two are. 10 | """ 11 | 12 | class NotHashEq(Enum): 13 | """ 14 | Enums that inherit only from :class:`~enum.Enum` are not hash equivalent 15 | to their values by default. 16 | """ 17 | 18 | VALUE1 = "V1" 19 | VALUE2 = "V2" 20 | VALUE3 = "V3" 21 | 22 | class HashEq(Enum): 23 | """ 24 | We can force our Enum to be hash equivalent by overriding the necessary 25 | dunder methods.. 26 | """ 27 | 28 | VALUE1 = "V1" 29 | VALUE2 = "V2" 30 | VALUE3 = "V3" 31 | 32 | def __hash__(self): 33 | return hash(self.value) 34 | 35 | def __eq__(self, value) -> bool: 36 | if isinstance(value, self.__class__): 37 | return self.value == value.value 38 | try: 39 | return self.value == self.__class__(value).value 40 | except (ValueError, TypeError): 41 | return False 42 | 43 | class HashEqStr(str, Enum): # or StrEnum on py 3.11+ 44 | """ 45 | Or we can inherit from the primitive value type. 46 | """ 47 | 48 | VALUE1 = "V1" 49 | VALUE2 = "V2" 50 | VALUE3 = "V3" 51 | 52 | 53 | not_hash_eq = EnumField(NotHashEq) 54 | hash_eq = EnumField(HashEq) 55 | hash_eq_str = EnumField(HashEqStr) 56 | -------------------------------------------------------------------------------- /tests/examples/models/mapbox.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import typing as t 3 | from django.db import models 4 | from enum_properties import symmetric, Symmetric, IntEnumProperties 5 | from django_enum import EnumField 6 | 7 | 8 | class Map(models.Model): 9 | 10 | class MapBoxStyle(IntEnumProperties): 11 | """ 12 | https://docs.mapbox.com/api/maps/styles/ 13 | """ 14 | 15 | label: t.Annotated[str, Symmetric(case_fold=True)] 16 | slug: t.Annotated[str, Symmetric(case_fold=True)] 17 | version: int 18 | 19 | # fmt: off 20 | # name value label slug version 21 | STREETS = 1, "Streets", "streets", 12 22 | OUTDOORS = 2, "Outdoors", "outdoors", 12 23 | LIGHT = 3, "Light", "light", 11 24 | DARK = 4, "Dark", "dark", 11 25 | SATELLITE = 5, "Satellite", "satellite", 9 26 | SATELLITE_STREETS = 6, "Satellite Streets", "satellite-streets", 12 27 | NAVIGATION_DAY = 7, "Navigation Day", "navigation-day", 1 28 | NAVIGATION_NIGHT = 8, "Navigation Night", "navigation-night", 1 29 | # fmt: on 30 | 31 | @symmetric() 32 | @property 33 | def uri(self): 34 | return f"mapbox://styles/mapbox/{self.slug}-v{self.version}" 35 | 36 | def __str__(self): 37 | return self.uri 38 | 39 | style = EnumField(MapBoxStyle, default=MapBoxStyle.STREETS) 40 | -------------------------------------------------------------------------------- /tests/examples/models/mixed_value.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from enum import Enum 3 | from django.db import models 4 | from django_enum import EnumField 5 | 6 | 7 | class MixedValueEnum(Enum): 8 | 9 | NONE = None 10 | VAL1 = 1 11 | VAL2 = '2.0' 12 | VAL3 = 3.0 13 | VAL4 = Decimal('4.5') 14 | 15 | 16 | class MixedValueExample(models.Model): 17 | 18 | # Since None is an enumeration value, EnumField will automatically set 19 | # null=True on these model fields. 20 | 21 | # column will be a CharField 22 | eccentric_str = EnumField(MixedValueEnum) 23 | 24 | # column will be a FloatField 25 | eccentric_float = EnumField(MixedValueEnum, primitive=float) 26 | -------------------------------------------------------------------------------- /tests/examples/models/no_coerce.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_enum import EnumField 3 | from django.db.models import TextChoices 4 | 5 | 6 | class NoCoerceExample(models.Model): 7 | 8 | class EnumType(TextChoices): 9 | 10 | ONE = "1", "One" 11 | TWO = "2", "Two" 12 | 13 | non_strict = EnumField( 14 | EnumType, 15 | strict=False, 16 | coerce=False, 17 | # it might be necessary to override max_length also, otherwise 18 | # max_length will be 1 19 | max_length=10, 20 | ) 21 | -------------------------------------------------------------------------------- /tests/examples/models/path_value.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from enum import Enum 3 | from django_enum import EnumField 4 | from pathlib import Path 5 | 6 | 7 | class PathValueExample(models.Model): 8 | 9 | class PathEnum(Enum): 10 | 11 | USR = Path('/usr') 12 | USR_LOCAL = Path('/usr/local') 13 | USR_LOCAL_BIN = Path('/usr/local/bin') 14 | 15 | path = EnumField(PathEnum, primitive=str) 16 | -------------------------------------------------------------------------------- /tests/examples/models/properties.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import typing as t 3 | from django.db import models 4 | from enum_properties import Symmetric, StrEnumProperties 5 | from typing_extensions import Annotated 6 | from django_enum import EnumField 7 | 8 | 9 | class PropertyExample(models.Model): 10 | 11 | class Color(StrEnumProperties): 12 | 13 | label: str 14 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 15 | hex: Annotated[str, Symmetric(case_fold=True)] 16 | 17 | # fmt: off 18 | # name value label rgb hex 19 | RED = "R", "Red", (1, 0, 0), "ff0000" 20 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 21 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 22 | # fmt: on 23 | 24 | # any type hints before the values in the Enum's definition become 25 | # properties on each value, and the enumeration value may be 26 | # instantiated from any symmetric property's value 27 | 28 | color = EnumField(Color) 29 | -------------------------------------------------------------------------------- /tests/examples/models/properties_choices.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import typing as t 3 | from django.db import models 4 | from enum_properties import Symmetric 5 | from typing_extensions import Annotated 6 | from django_enum import EnumField 7 | from django_enum.choices import TextChoices 8 | 9 | class ChoicesWithProperties(models.Model): 10 | 11 | class Color(TextChoices): 12 | 13 | # label is added as a symmetric property by the base class 14 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 15 | hex: Annotated[str, Symmetric(case_fold=True)] 16 | 17 | # fmt: off 18 | # name value label rgb hex 19 | RED = "R", "Red", (1, 0, 0), "ff0000" 20 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 21 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 22 | # fmt: on 23 | 24 | color = EnumField(Color) 25 | -------------------------------------------------------------------------------- /tests/examples/models/strict.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_enum import EnumField 3 | 4 | 5 | class StrictExample(models.Model): 6 | 7 | class EnumType(models.TextChoices): 8 | 9 | ONE = "1", "One" 10 | TWO = "2", "Two" 11 | 12 | non_strict = EnumField( 13 | EnumType, 14 | strict=False, 15 | # it might be necessary to override max_length also, 16 | # otherwise max_length will be 1 17 | max_length=10, 18 | ) 19 | -------------------------------------------------------------------------------- /tests/examples/models/text_choices.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import typing as t 3 | from django.db import models 4 | from typing_extensions import Annotated 5 | from django_enum import EnumField 6 | from django_enum.choices import TextChoices 7 | from enum_properties import Symmetric, symmetric 8 | 9 | 10 | class TextChoicesExample(models.Model): 11 | 12 | class Color(TextChoices): 13 | 14 | # no need to specify label because it is built in 15 | rgb: Annotated[t.Tuple[int, int, int], Symmetric()] 16 | hex: Annotated[str, Symmetric(case_fold=True)] 17 | 18 | # fmt: off 19 | # name value label rgb hex 20 | RED = "R", "Red", (1, 0, 0), "ff0000" 21 | GREEN = "G", "Green", (0, 1, 0), "00ff00" 22 | BLUE = "B", "Blue", (0, 0, 1), "0000ff" 23 | # fmt: on 24 | 25 | # by default label is symmetric, but case sensitive 26 | # to make it case-insensitive we can override the property 27 | # and mark it like this: 28 | @symmetric(case_fold=True) 29 | @property 30 | def label(self) -> str: 31 | return self._label_ 32 | 33 | color = EnumField(Color) 34 | -------------------------------------------------------------------------------- /tests/examples/models/widgets.py: -------------------------------------------------------------------------------- 1 | from tests.examples.enums import Color, Permissions 2 | from django.db import models 3 | from django_enum import EnumField 4 | 5 | 6 | class WidgetDemoStrict(models.Model): 7 | 8 | color = EnumField(Color, default=Color.RED) 9 | permissions = EnumField(Permissions) 10 | 11 | 12 | class WidgetDemoNonStrict(models.Model): 13 | 14 | color = EnumField(Color, strict=False, max_length=12) 15 | permissions = EnumField(Permissions, strict=False) 16 | 17 | 18 | class WidgetDemoRadiosAndChecks(WidgetDemoStrict): 19 | pass 20 | 21 | 22 | class WidgetDemoRadiosAndChecksNulls(models.Model): 23 | 24 | color = EnumField(Color, default=None, null=True, blank=True) 25 | permissions = EnumField(Permissions, default=None, null=True, blank=True) 26 | 27 | 28 | class WidgetDemoRadiosAndChecksNonStrict(WidgetDemoNonStrict): 29 | pass 30 | -------------------------------------------------------------------------------- /tests/examples/no_coerce_howto.py: -------------------------------------------------------------------------------- 1 | from .models import NoCoerceExample 2 | 3 | 4 | obj = NoCoerceExample() 5 | 6 | # set to a valid EnumType value 7 | obj.non_strict = '1' 8 | 9 | # when accessed will be the primitive value 10 | assert obj.non_strict == '1' 11 | assert isinstance(obj.non_strict, str) 12 | assert not isinstance(obj.non_strict, NoCoerceExample.EnumType) 13 | -------------------------------------------------------------------------------- /tests/examples/path_value_example.py: -------------------------------------------------------------------------------- 1 | from .models.path_value import PathValueExample 2 | from django.db import models 3 | 4 | obj = PathValueExample.objects.create( 5 | path=PathValueExample.PathEnum.USR_LOCAL_BIN, 6 | ) 7 | 8 | assert isinstance(obj._meta.get_field("path"), models.CharField) 9 | assert obj.path is PathValueExample.PathEnum.USR_LOCAL_BIN 10 | -------------------------------------------------------------------------------- /tests/examples/properties_example.py: -------------------------------------------------------------------------------- 1 | from .models import PropertyExample 2 | 3 | 4 | instance = PropertyExample.objects.create( 5 | color=PropertyExample.Color('FF0000') 6 | ) 7 | assert instance.color is PropertyExample.Color['RED'] 8 | assert instance.color is PropertyExample.Color('R') 9 | assert instance.color is PropertyExample.Color((1, 0, 0)) 10 | # note that we did not make label symmetric, so this does not work: 11 | # PropertyExample.Color('Red') 12 | 13 | # direct comparison to any symmetric value also works 14 | assert instance.color == 'FF0000' 15 | assert instance.color == 'R' 16 | assert instance.color == (1, 0, 0) 17 | assert instance.color != 'Red' # because label is not symmetric 18 | 19 | # save by any symmetric value 20 | instance.color = 'FF0000' 21 | 22 | # access any enum property right from the model field 23 | assert instance.color.hex == 'ff0000' 24 | 25 | # this also works! 26 | assert instance.color == 'ff0000' 27 | 28 | # and so does this! 29 | assert instance.color == 'FF0000' 30 | 31 | instance.save() 32 | 33 | # filtering works by any symmetric value or enum type instance 34 | assert PropertyExample.objects.filter( 35 | color=PropertyExample.Color.RED 36 | ).first() == instance 37 | 38 | assert PropertyExample.objects.filter(color=(1, 0, 0)).first() == instance 39 | 40 | assert PropertyExample.objects.filter(color='FF0000').first() == instance 41 | -------------------------------------------------------------------------------- /tests/examples/radio_form_howto.py: -------------------------------------------------------------------------------- 1 | from .models.text_choices import TextChoicesExample 2 | from django.forms import Form, RadioSelect 3 | from django_enum.forms import EnumChoiceField, NonStrictRadioSelect 4 | 5 | 6 | class TextChoicesExampleForm(Form): 7 | 8 | color = EnumChoiceField(TextChoicesExample.Color, widget=RadioSelect) 9 | 10 | # since this field is not strict, we can set it to a value not in our 11 | # enum or choice tuple. 12 | color_ext = EnumChoiceField( 13 | TextChoicesExample.Color, 14 | strict=False, 15 | choices=[ 16 | ('P', 'Purple'), 17 | ('O', 'Orange'), 18 | ] + TextChoicesExample.Color.choices, 19 | widget=NonStrictRadioSelect 20 | ) 21 | 22 | 23 | # when this form is rendered in a template it will include a selected 24 | # option for the value 'Y' that is not part of our Color enumeration. 25 | form = TextChoicesExampleForm( 26 | initial={ 27 | "color": TextChoicesExample.Color.RED, 28 | "color_ext": "Y" 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /tests/examples/strict_howto.py: -------------------------------------------------------------------------------- 1 | from .models import StrictExample 2 | 3 | 4 | obj = StrictExample() 5 | 6 | # set to a valid EnumType value 7 | obj.non_strict = '1' 8 | # when accessed will be an EnumType instance 9 | assert obj.non_strict is StrictExample.EnumType.ONE 10 | 11 | # we can also store any string less than or equal to length 10 12 | obj.non_strict = 'arbitrary' 13 | obj.full_clean() # no errors 14 | # when accessed will be a str instance 15 | assert obj.non_strict == 'arbitrary' 16 | -------------------------------------------------------------------------------- /tests/examples/templates/tests_examples/choice_form_howto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EnumChoiceField Form 7 | 8 | 9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/examples/templates/tests_examples/flag_form_howto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EnumFlagField Form 7 | 8 | 9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/examples/templates/tests_examples/textchoicesexample_filter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Filtered List 7 | 8 | 9 |

Filtered Objects

10 | 11 |
12 | {{ filter.form.as_p }} 13 | 14 |
15 | 16 |
    17 | {% for obj in filter.qs %} 18 |
  • {{ obj.color.label }}
  • 19 | {% empty %} 20 |
  • No results found.
  • 21 | {% endfor %} 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/examples/text_choices_howto.py: -------------------------------------------------------------------------------- 1 | from .models import TextChoicesExample 2 | 3 | 4 | obj = TextChoicesExample.objects.create(color=TextChoicesExample.Color.RED) 5 | 6 | assert obj.color is TextChoicesExample.Color.RED 7 | assert obj.color.label == 'Red' 8 | assert obj.color.rgb == (1, 0, 0) 9 | assert obj.color.hex == 'ff0000' 10 | 11 | # enum-properties symmetric properties work as expected 12 | assert obj.color == 'Red' 13 | assert obj.color == (1, 0, 0) 14 | assert obj.color == 'ff0000' 15 | -------------------------------------------------------------------------------- /tests/examples/url_converter_howto.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from tests.examples.urls import Enum1 3 | 4 | 5 | assert reverse("examples:enum_default", kwargs={"enum": Enum1.A}) == "/1" 6 | assert reverse("examples:enum_default", kwargs={"enum": Enum1.B}) == "/2" 7 | 8 | assert reverse("examples:enum_by_name", kwargs={"enum": Enum1.A}) == "/A" 9 | assert reverse("examples:enum_by_name", kwargs={"enum": Enum1.B}) == "/B" 10 | -------------------------------------------------------------------------------- /tests/examples/urls.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from django.http import HttpResponse 4 | from django.urls import path 5 | 6 | from django_enum.urls import register_enum_converter 7 | 8 | 9 | class Enum1(IntEnum): 10 | A = 1 11 | B = 2 12 | 13 | 14 | register_enum_converter(Enum1) 15 | register_enum_converter(Enum1, type_name="Enum1ByName", prop="name") 16 | 17 | 18 | def enum_converter_view(request, enum): 19 | assert isinstance(enum, Enum1) 20 | return HttpResponse(status=200, content=f"{enum=}") 21 | 22 | 23 | app_name = "examples" 24 | 25 | urlpatterns = [ 26 | # this will match paths /1 and /2 27 | path("", enum_converter_view, name="enum_default"), 28 | 29 | # this will match paths /A and /B 30 | path("", enum_converter_view, name="enum_by_name"), 31 | ] 32 | -------------------------------------------------------------------------------- /tests/examples/urls_forms.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | try: 3 | from django.shortcuts import render 4 | from .choice_form_howto import TextChoicesExampleForm, TextChoicesExample 5 | from .flag_form_howto import PermissionsExampleForm, Group 6 | 7 | def choice_form_view(request): 8 | from .choice_form_howto import TextChoicesExampleForm, form 9 | if request.method == "POST": 10 | form = TextChoicesExampleForm(request.POST) 11 | if form.is_valid(): 12 | # Process the form data (for now, just print it) 13 | print("Valid form data:", form.cleaned_data) 14 | return render(request, "tests_examples/choice_form_howto.html", {"form": form}) 15 | 16 | def radio_form_view(request): 17 | from .radio_form_howto import TextChoicesExampleForm, form 18 | if request.method == "POST": 19 | form = TextChoicesExampleForm(request.POST) 20 | if form.is_valid(): 21 | # Process the form data (for now, just print it) 22 | print("Valid form data:", form.cleaned_data) 23 | return render(request, "tests_examples/choice_form_howto.html", {"form": form}) 24 | 25 | def flag_form_view(request): 26 | from .flag_form_howto import PermissionsExampleForm, form 27 | if request.method == "POST": 28 | form = PermissionsExampleForm(request.POST) 29 | if form.is_valid(): 30 | # Process the form data (for now, just print it) 31 | print("Valid form data:", form.cleaned_data) 32 | return render(request, "tests_examples/flag_form_howto.html", {"form": form}) 33 | 34 | def checkboxes_form_view(request): 35 | from .checkboxes_form_howto import PermissionsExampleForm, form 36 | if request.method == "POST": 37 | form = PermissionsExampleForm(request.POST) 38 | if form.is_valid(): 39 | # Process the form data (for now, just print it) 40 | print("Valid form data:", form.cleaned_data) 41 | return render(request, "tests_examples/flag_form_howto.html", {"form": form}) 42 | 43 | app_name = "howto_forms" 44 | 45 | urlpatterns = [ 46 | path("choice/", choice_form_view, name="choice"), 47 | path("radio/", radio_form_view, name="radio"), 48 | path("flag/", flag_form_view, name="flag"), 49 | path("checkboxes/", checkboxes_form_view, name="checkboxes"), 50 | ] 51 | except ImportError: 52 | urlpatterns = [] 53 | -------------------------------------------------------------------------------- /tests/examples/urls_howto.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | try: 3 | from .filterfield_howto import TextChoicesExampleFilterViewSet 4 | from .filterset_howto import ( 5 | TextChoicesExampleFilterViewSet as TextChoicesExampleFilterSetViewSet 6 | ) 7 | from .flagfilterfield_howto import FlagExampleFilterViewSet 8 | 9 | app_name = "howto" 10 | 11 | urlpatterns = [ 12 | path( 13 | 'filterfield/', 14 | TextChoicesExampleFilterViewSet.as_view(), 15 | name='filterfield' 16 | ), 17 | path( 18 | 'filterset/', 19 | TextChoicesExampleFilterSetViewSet.as_view(), 20 | name='filterset' 21 | ), 22 | path('flagfilterfield/', FlagExampleFilterViewSet.as_view(), name="flagfilterfield") 23 | ] 24 | except ImportError: 25 | urlpatterns = [] 26 | -------------------------------------------------------------------------------- /tests/flag_constraints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/flag_constraints/__init__.py -------------------------------------------------------------------------------- /tests/flag_constraints/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FlagConstraintsConfig(AppConfig): 5 | name = "tests.flag_constraints" 6 | label = name.replace(".", "_") 7 | -------------------------------------------------------------------------------- /tests/flag_constraints/enums.py: -------------------------------------------------------------------------------- 1 | """ 2 | "strict" -> error is raised [default for Flag] 3 | "conform" -> extra bits are discarded 4 | "eject" -> lose flag status [default for IntFlag] 5 | "keep" -> keep flag status and all bits 6 | """ 7 | 8 | import sys 9 | 10 | if sys.version_info >= (3, 11): 11 | from enum import CONFORM, EJECT, KEEP, STRICT, Flag, IntFlag 12 | 13 | class KeepFlagEnum(IntFlag, boundary=KEEP): 14 | VAL1 = 2**12 # 4096 15 | VAL2 = 2**13 # 8192 16 | VAL3 = 2**14 # 16384 17 | 18 | class EjectFlagEnum(IntFlag, boundary=EJECT): 19 | VAL1 = 2**12 20 | VAL2 = 2**13 21 | VAL3 = 2**14 22 | 23 | class StrictFlagEnum(Flag, boundary=STRICT): 24 | VAL1 = 2**12 25 | VAL2 = 2**13 26 | VAL3 = 2**14 27 | 28 | class ConformFlagEnum(IntFlag, boundary=CONFORM): 29 | VAL1 = 2**12 30 | VAL2 = 2**13 31 | VAL3 = 2**14 32 | -------------------------------------------------------------------------------- /tests/flag_constraints/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/flag_constraints/migrations/__init__.py -------------------------------------------------------------------------------- /tests/flag_constraints/models.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.db import models 4 | 5 | from django_enum import EnumField 6 | 7 | if sys.version_info >= (3, 11): 8 | from tests.flag_constraints.enums import ( 9 | ConformFlagEnum, 10 | EjectFlagEnum, 11 | KeepFlagEnum, 12 | StrictFlagEnum, 13 | ) 14 | 15 | class FlagConstraintTestModel(models.Model): 16 | keep = EnumField(KeepFlagEnum, null=True, default=None, blank=True) 17 | eject = EnumField( 18 | EjectFlagEnum, null=False, default=EjectFlagEnum(0), blank=True 19 | ) 20 | eject_non_strict = EnumField( 21 | EjectFlagEnum, 22 | null=False, 23 | default=EjectFlagEnum(0), 24 | blank=True, 25 | strict=False, 26 | ) 27 | conform = EnumField(ConformFlagEnum, null=True, default=None, blank=True) 28 | strict = EnumField(StrictFlagEnum, null=True, default=None, blank=True) 29 | -------------------------------------------------------------------------------- /tests/oracle_patch.py: -------------------------------------------------------------------------------- 1 | """ 2 | monkey patch a fix to django oracle backend bug where DATE literals in 3 | CheckConstraints are not recognized by Oracle without explicitly casting to 4 | DATE. This patch allows eccentric tests to pass on Oracle - and is necessary 5 | because those tests block the normal tests just based on how the test suite is 6 | put together. So to do any significant testing on Oracle, this monkey patch is 7 | necessary - remove when there is an upstream fix. 8 | """ 9 | 10 | from datetime import date, datetime, timedelta 11 | 12 | from django.db.backends.oracle.schema import DatabaseSchemaEditor 13 | from django.utils.duration import duration_iso_string 14 | 15 | 16 | def patch_oracle(): 17 | quote_value = DatabaseSchemaEditor.quote_value 18 | 19 | def quote_value_patched(self, value): 20 | if isinstance(value, date) and not isinstance(value, datetime): 21 | return "DATE '%s'" % value.isoformat() 22 | elif isinstance(value, timedelta): 23 | return "'%s'" % duration_iso_string(value) 24 | return quote_value(self, value) 25 | 26 | DatabaseSchemaEditor.quote_value = quote_value_patched 27 | -------------------------------------------------------------------------------- /tests/test_admin_ep.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from enum import Enum 3 | import pytest 4 | 5 | pytest.importorskip("enum_properties") 6 | 7 | from tests.test_admin import TestAdmin, TestEnumTesterAdminForm, _GenericAdminFormTest 8 | from tests.enum_prop.models import AdminDisplayBug35, EnumTester, BitFieldModel 9 | from tests.enum_prop.enums import ( 10 | GNSSConstellation, 11 | LargeBitField, 12 | LargeNegativeField, 13 | LargeBitField, 14 | ) 15 | 16 | 17 | class TestEnumPropAdmin(TestAdmin): 18 | BUG35_CLASS = AdminDisplayBug35 19 | 20 | 21 | class TestEnumTesterPropsAdminForm(TestEnumTesterAdminForm): 22 | MODEL_CLASS = EnumTester 23 | __test__ = True 24 | 25 | 26 | class TestBitFieldAdminForm(_GenericAdminFormTest): 27 | MODEL_CLASS = BitFieldModel 28 | HEADLESS = True 29 | __test__ = True 30 | 31 | @property 32 | def changes(self) -> t.List[t.Dict[str, t.Any]]: 33 | return [ 34 | { 35 | "bit_field_small": GNSSConstellation.GLONASS | GNSSConstellation.GPS, 36 | "no_default": LargeBitField.ONE | LargeBitField.TWO, 37 | }, 38 | { 39 | "bit_field_small": GNSSConstellation.GLONASS, 40 | "bit_field_large": LargeBitField.TWO | LargeBitField.ONE, 41 | "bit_field_large_empty_default": LargeBitField.TWO, 42 | "no_default": LargeBitField.TWO, 43 | }, 44 | { 45 | "bit_field_small": GNSSConstellation(0), 46 | "bit_field_large": None, 47 | "bit_field_large_empty_default": LargeBitField(0), 48 | "no_default": LargeBitField(0), 49 | }, 50 | ] 51 | 52 | 53 | TestAdmin = None 54 | TestEnumTesterAdminForm = None 55 | -------------------------------------------------------------------------------- /tests/test_bulk.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from tests.utils import EnumTypeMixin, IGNORE_ORA_01843 3 | from tests.djenum.models import EnumTester 4 | from django.db import connection 5 | from django.db.utils import DatabaseError 6 | import pytest 7 | 8 | 9 | class TestBulkOperations(EnumTypeMixin, TestCase): 10 | """Tests bulk insertions and updates""" 11 | 12 | MODEL_CLASS = EnumTester 13 | NUMBER = 250 14 | 15 | def setUp(self): 16 | self.MODEL_CLASS.objects.all().delete() 17 | 18 | @property 19 | def create_params(self): 20 | return { 21 | "small_pos_int": self.SmallPosIntEnum.VAL2, 22 | "small_int": self.SmallIntEnum.VALn1, 23 | "pos_int": 2147483647, 24 | "int": -2147483648, 25 | "big_pos_int": self.BigPosIntEnum.VAL3, 26 | "big_int": self.BigIntEnum.VAL2, 27 | "constant": self.Constants.GOLDEN_RATIO, 28 | "text": self.TextEnum.VALUE2, 29 | "extern": self.ExternEnum.TWO, 30 | "date_enum": self.DateEnum.HUGO, 31 | "datetime_enum": self.DateTimeEnum.KATRINA, 32 | "time_enum": self.TimeEnum.COB, 33 | "duration_enum": self.DurationEnum.FORTNIGHT, 34 | "decimal_enum": self.DecimalEnum.FIVE, 35 | "dj_int_enum": 3, 36 | "dj_text_enum": self.DJTextEnum.A, 37 | "non_strict_int": 15, 38 | "non_strict_text": "arbitrary", 39 | "no_coerce": "0", 40 | } 41 | 42 | @property 43 | def update_params(self): 44 | return { 45 | "non_strict_int": 100, 46 | "constant": self.Constants.PI, 47 | "big_int": -2147483649, 48 | "date_enum": self.DateEnum.BRIAN, 49 | "datetime_enum": self.DateTimeEnum.ST_HELENS, 50 | "time_enum": self.TimeEnum.LUNCH, 51 | "duration_enum": self.DurationEnum.WEEK, 52 | "decimal_enum": self.DecimalEnum.TWO, 53 | } 54 | 55 | def test_bulk_create(self): 56 | try: 57 | objects = [] 58 | for _ in range(0, self.NUMBER): 59 | objects.append(self.MODEL_CLASS(**self.create_params)) 60 | 61 | self.MODEL_CLASS.objects.bulk_create(objects) 62 | 63 | self.assertEqual( 64 | self.MODEL_CLASS.objects.filter(**self.create_params).count(), 65 | self.NUMBER, 66 | ) 67 | except DatabaseError as err: 68 | print(str(err)) 69 | if ( 70 | IGNORE_ORA_01843 71 | and connection.vendor == "oracle" 72 | and "ORA-01843" in str(err) 73 | ): 74 | # this is an oracle bug - intermittent failure on 75 | # perfectly fine date format in SQL 76 | # continue 77 | pytest.skip("Oracle bug ORA-01843 encountered - skipping") 78 | raise 79 | 80 | def test_bulk_update(self): 81 | try: 82 | objects = [] 83 | for obj in range(0, self.NUMBER): 84 | obj = self.MODEL_CLASS.objects.create(**self.create_params) 85 | for param, value in self.update_params.items(): 86 | setattr(obj, param, value) 87 | objects.append(obj) 88 | 89 | self.assertEqual(len(objects), self.NUMBER) 90 | to_update = ["constant", "non_strict_int"] 91 | self.MODEL_CLASS.objects.bulk_update(objects, to_update) 92 | 93 | self.assertEqual( 94 | self.MODEL_CLASS.objects.filter( 95 | **{ 96 | **self.create_params, 97 | **{ 98 | param: val 99 | for param, val in self.update_params.items() 100 | if param in to_update 101 | }, 102 | } 103 | ).count(), 104 | self.NUMBER, 105 | ) 106 | except DatabaseError as err: 107 | print(str(err)) 108 | if ( 109 | IGNORE_ORA_01843 110 | and connection.vendor == "oracle" 111 | and "ORA-01843" in str(err) 112 | ): 113 | # this is an oracle bug - intermittent failure on 114 | # perfectly fine date format in SQL 115 | # continue 116 | pytest.skip("Oracle bug ORA-01843 encountered - skipping") 117 | raise 118 | -------------------------------------------------------------------------------- /tests/test_bulk_ep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("enum_properties") 4 | 5 | from tests.test_bulk import TestBulkOperations 6 | from tests.enum_prop.models import EnumTester 7 | 8 | 9 | class TestBulkOperationsProps(TestBulkOperations): 10 | MODEL_CLASS = EnumTester 11 | 12 | @property 13 | def create_params(self): 14 | return { 15 | "small_pos_int": self.SmallPosIntEnum.VAL2, 16 | "small_int": "Value -32768", 17 | "pos_int": 2147483647, 18 | "int": -2147483648, 19 | "big_pos_int": "Value 2147483648", 20 | "big_int": "VAL2", 21 | "constant": "φ", 22 | "text": "V TWo", 23 | "extern": "One", 24 | "dj_int_enum": 3, 25 | "dj_text_enum": self.DJTextEnum.A, 26 | "non_strict_int": 15, 27 | "non_strict_text": "arbitrary", 28 | "no_coerce": "Value 2", 29 | } 30 | 31 | @property 32 | def update_params(self): 33 | return { 34 | "non_strict_int": 100, 35 | "non_strict_text": self.TextEnum.VALUE3, 36 | "constant": "π", 37 | "big_int": -2147483649, 38 | "coerce": 2, 39 | } 40 | 41 | 42 | TestBulkOperations = None 43 | -------------------------------------------------------------------------------- /tests/test_converter.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from decimal import Decimal 3 | 4 | 5 | class TestEnumConverter(TestCase): 6 | def test_enum_converter(self): 7 | from django.urls import reverse 8 | from django.urls.converters import get_converters 9 | 10 | from tests.converters.urls import TestEnum, record 11 | from tests.djenum.enums import Constants, DecimalEnum 12 | 13 | converter = get_converters()["TestEnum"] 14 | self.assertEqual(converter.regex, "1|2") 15 | self.assertEqual(converter.to_python("1"), TestEnum.A) 16 | self.assertEqual(converter.to_python("2"), TestEnum.B) 17 | self.assertEqual(converter.primitive, int) 18 | self.assertEqual(converter.enum, TestEnum) 19 | self.assertEqual(converter.prop, "value") 20 | 21 | self.assertEqual( 22 | reverse("enum1_view", kwargs={"enum": TestEnum.A}), "/converters/1" 23 | ) 24 | 25 | response = self.client.get("/converters/1") 26 | self.assertEqual(response.status_code, 200) 27 | self.assertEqual(record[0], TestEnum.A) 28 | 29 | converter = get_converters()["decimal_enum"] 30 | self.assertEqual(converter.regex, "0.99|0.999|0.9999|99.9999|999") 31 | self.assertEqual(converter.to_python("0.999"), DecimalEnum.TWO) 32 | self.assertEqual(converter.to_python("99.9999"), DecimalEnum.FOUR) 33 | self.assertEqual(converter.primitive, Decimal) 34 | self.assertEqual(converter.enum, DecimalEnum) 35 | self.assertEqual(converter.prop, "value") 36 | 37 | self.assertEqual( 38 | reverse("decimal_enum_view", kwargs={"enum": DecimalEnum.ONE}), 39 | "/converters/0.99", 40 | ) 41 | 42 | response = self.client.get("/converters/0.99") 43 | self.assertEqual(response.status_code, 200) 44 | self.assertEqual(record[1], DecimalEnum.ONE) 45 | 46 | converter = get_converters()["Constants"] 47 | self.assertEqual(converter.regex, "Pi|Euler's Number|Golden Ratio") 48 | self.assertEqual(converter.to_python("Golden Ratio"), Constants.GOLDEN_RATIO) 49 | self.assertEqual(converter.to_python("Euler's Number"), Constants.e) 50 | self.assertEqual(converter.to_python("Pi"), Constants.PI) 51 | self.assertEqual(converter.primitive, float) 52 | self.assertEqual(converter.enum, Constants) 53 | self.assertEqual(converter.prop, "label") 54 | 55 | self.assertEqual( 56 | reverse("constants_view", kwargs={"enum": Constants.GOLDEN_RATIO}), 57 | "/converters/Golden%20Ratio", 58 | ) 59 | 60 | response = self.client.get("/converters/Euler's Number") 61 | self.assertEqual(response.status_code, 200) 62 | self.assertEqual(record[2], Constants.e) 63 | -------------------------------------------------------------------------------- /tests/test_db_defaults.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as django_version 2 | import pytest 3 | 4 | if django_version[0:2] < (5, 0): 5 | pytest.skip(reason="Requires Django >= 5.0", allow_module_level=True) 6 | 7 | from tests.utils import EnumTypeMixin 8 | from django.test import TestCase 9 | from tests.db_default.models import DBDefaultTester 10 | from django.db import connection 11 | 12 | 13 | class DBDefaultTests(EnumTypeMixin, TestCase): 14 | MODEL_CLASS = DBDefaultTester 15 | 16 | @property 17 | def defaults(self): 18 | return { 19 | "small_pos_int": None, 20 | "small_int": self.SmallIntEnum.VAL3, 21 | "small_int_shadow": self.SmallIntEnum.VAL3, 22 | "pos_int": self.PosIntEnum.VAL3, 23 | "int": self.IntEnum.VALn1, 24 | "big_pos_int": None, 25 | "big_int": self.BigIntEnum.VAL0, 26 | "constant": self.Constants.GOLDEN_RATIO, 27 | "char_field": "db_default", 28 | "doubled_char_field": "default", 29 | "text": "", 30 | "doubled_text": "", 31 | "doubled_text_strict": self.TextEnum.DEFAULT, 32 | "extern": self.ExternEnum.THREE, 33 | "dj_int_enum": self.DJIntEnum.ONE, 34 | "dj_text_enum": self.DJTextEnum.A, 35 | "non_strict_int": 5, 36 | "non_strict_text": "arbitrary", 37 | "no_coerce": 2, 38 | "no_coerce_value": 32767, 39 | "no_coerce_none": None, 40 | } 41 | 42 | def test_db_defaults(self): 43 | obj = DBDefaultTester.objects.create() 44 | # TODO - there seems to be a mysql bug here where DatabaseDefaults 45 | # are not refreshed from the db after creation - works on all other platforms 46 | if connection.vendor == "mysql": 47 | obj.refresh_from_db() 48 | 49 | for field, value in self.defaults.items(): 50 | obj_field = DBDefaultTester._meta.get_field(field) 51 | obj_value = getattr(obj, field) 52 | self.assertEqual(obj_value, value) 53 | from django_enum.fields import EnumField 54 | 55 | if ( 56 | isinstance(obj_field, EnumField) 57 | and obj_field.strict 58 | and obj_field.coerce 59 | and obj_value is not None 60 | ): 61 | self.assertIsInstance(obj_value, obj_field.enum) 62 | 63 | def test_db_defaults_not_coerced(self): 64 | from django.db.models.expressions import DatabaseDefault 65 | 66 | empty_inst = DBDefaultTester() 67 | 68 | # check that the database default value fields are not coerced 69 | for field in [ 70 | field for field in self.defaults.keys() if not field.startswith("doubled") 71 | ]: 72 | self.assertIsInstance(getattr(empty_inst, field), DatabaseDefault) 73 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.core.exceptions import ValidationError 3 | from django_enum import EnumField 4 | from tests.djenum.models import EnumTester 5 | 6 | 7 | class MiscOffNominalTests(TestCase): 8 | def test_field_def_errors(self): 9 | from django.db.models import Model 10 | 11 | with self.assertRaises(ValueError): 12 | 13 | class TestModel(Model): 14 | enum = EnumField() 15 | 16 | def test_full_clean_raises_validation_error(self): 17 | with self.assertRaises(ValidationError): 18 | en = EnumTester(text="wrong") 19 | en.full_clean() 20 | 21 | with self.assertRaises(ValidationError): 22 | EnumTester(text="").full_clean() 23 | 24 | def test_variable_primitive_type(self): 25 | from enum import Enum 26 | 27 | from django.db.models import Model 28 | 29 | from django_enum.utils import determine_primitive 30 | 31 | class MultiPrimitive(Enum): 32 | VAL1 = 1 33 | VAL2 = "2" 34 | VAL3 = 3.0 35 | VAL4 = b"4" 36 | 37 | self.assertIsNone(determine_primitive(MultiPrimitive)) 38 | 39 | with self.assertRaises(ValueError): 40 | 41 | class TestModel(Model): 42 | enum = EnumField(MultiPrimitive) 43 | 44 | with self.assertRaises(ValueError): 45 | """ 46 | 2 is not symmetrically convertable float<->str 47 | """ 48 | 49 | class TestModel(Model): 50 | enum = EnumField(MultiPrimitive, primitive=float) 51 | 52 | def test_unsupported_primitive(self): 53 | from enum import Enum 54 | 55 | from django_enum.utils import determine_primitive 56 | 57 | class MyPrimitive: 58 | pass 59 | 60 | class WeirdPrimitive(Enum): 61 | VAL1 = MyPrimitive() 62 | VAL2 = MyPrimitive() 63 | VAL3 = MyPrimitive() 64 | 65 | self.assertEqual(determine_primitive(WeirdPrimitive), MyPrimitive) 66 | 67 | with self.assertRaises(NotImplementedError): 68 | EnumField(WeirdPrimitive) 69 | 70 | def test_bit_length_override(self): 71 | from enum import IntFlag 72 | 73 | class IntEnum(IntFlag): 74 | VAL1 = 2**0 75 | VAL2 = 2**2 76 | VAL3 = 2**3 77 | VAL8 = 2**8 78 | 79 | with self.assertRaises(AssertionError): 80 | EnumField(IntEnum, bit_length=7) 81 | 82 | field = EnumField(IntEnum, bit_length=12) 83 | self.assertEqual(field.bit_length, 12) 84 | 85 | def test_no_value_enum(self): 86 | from enum import Enum 87 | 88 | from django_enum.utils import determine_primitive 89 | 90 | class EmptyEnum(Enum): 91 | pass 92 | 93 | self.assertIsNone(determine_primitive(EmptyEnum)) 94 | 95 | with self.assertRaises(ValueError): 96 | EnumField(EmptyEnum) 97 | 98 | def test_copy_field(self): 99 | from copy import copy, deepcopy 100 | from enum import Enum 101 | 102 | class BasicEnum(Enum): 103 | VAL1 = "1" 104 | VAL2 = "2" 105 | VAL3 = "3" 106 | 107 | field = EnumField(BasicEnum) 108 | field2 = deepcopy(field) 109 | field3 = copy(field) 110 | 111 | self.assertEqual(field.enum, field2.enum, field3.enum) 112 | 113 | 114 | class TestEmptyEnumValues(TestCase): 115 | def test_none_enum_values(self): 116 | # TODO?? 117 | pass 118 | -------------------------------------------------------------------------------- /tests/test_external.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from django.test import TestCase 3 | from django_enum.utils import choices, names, values, labels 4 | from django.utils.functional import classproperty 5 | 6 | 7 | class TestEnumCompat(TestCase): 8 | """Test that django_enum allows non-choice derived enums to be used""" 9 | 10 | from django.db.models import IntegerChoices as DJIntegerChoices 11 | 12 | class NormalIntEnum(enum.IntEnum): 13 | VAL1 = 1 14 | VAL2 = 2 15 | 16 | class IntEnumWithLabels(enum.IntEnum): 17 | __empty__ = 0 18 | 19 | VAL1 = 1 20 | VAL2 = 2 21 | 22 | @property 23 | def label(self): 24 | return { 25 | self.VAL1: "Label 1", 26 | self.VAL2: "Label 2", 27 | }.get(self) 28 | 29 | class ChoicesIntEnum(DJIntegerChoices): 30 | __empty__ = 0 31 | 32 | VAL1 = 1, "Label 1" 33 | VAL2 = 2, "Label 2" 34 | 35 | class EnumWithChoicesProperty(enum.Enum): 36 | VAL1 = 1 37 | VAL2 = 2 38 | 39 | @classproperty 40 | def choices(self): 41 | return [(self.VAL1.value, "Label 1"), (self.VAL2.value, "Label 2")] 42 | 43 | def test_choices(self): 44 | self.assertEqual( 45 | choices(TestEnumCompat.NormalIntEnum), [(1, "VAL1"), (2, "VAL2")] 46 | ) 47 | self.assertEqual( 48 | choices(TestEnumCompat.IntEnumWithLabels), 49 | TestEnumCompat.ChoicesIntEnum.choices, 50 | ) 51 | self.assertEqual( 52 | choices(TestEnumCompat.EnumWithChoicesProperty), 53 | [(1, "Label 1"), (2, "Label 2")], 54 | ) 55 | self.assertEqual(choices(None), []) 56 | 57 | def test_labels(self): 58 | self.assertEqual(labels(TestEnumCompat.NormalIntEnum), ["VAL1", "VAL2"]) 59 | self.assertEqual( 60 | labels(TestEnumCompat.IntEnumWithLabels), 61 | TestEnumCompat.ChoicesIntEnum.labels, 62 | ) 63 | self.assertEqual( 64 | labels(TestEnumCompat.EnumWithChoicesProperty), ["Label 1", "Label 2"] 65 | ) 66 | self.assertEqual(labels(None), []) 67 | 68 | def test_values(self): 69 | self.assertEqual(values(TestEnumCompat.NormalIntEnum), [1, 2]) 70 | self.assertEqual( 71 | values(TestEnumCompat.IntEnumWithLabels), 72 | TestEnumCompat.ChoicesIntEnum.values, 73 | ) 74 | self.assertEqual(values(TestEnumCompat.EnumWithChoicesProperty), [1, 2]) 75 | self.assertEqual(values(None), []) 76 | 77 | def test_names(self): 78 | self.assertEqual(names(TestEnumCompat.NormalIntEnum), ["VAL1", "VAL2"]) 79 | self.assertEqual( 80 | names(TestEnumCompat.IntEnumWithLabels), TestEnumCompat.ChoicesIntEnum.names 81 | ) 82 | self.assertEqual( 83 | names(TestEnumCompat.EnumWithChoicesProperty), ["VAL1", "VAL2"] 84 | ) 85 | self.assertEqual(names(None), []) 86 | -------------------------------------------------------------------------------- /tests/test_field_types_ep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("enum_properties") 4 | from tests.djenum.models import EnumTester 5 | from django.core.exceptions import FieldError 6 | from django.db.models import F 7 | from tests.test_field_types import TestFieldTypeResolution 8 | from tests.enum_prop.models import EnumTester 9 | from tests.enum_prop.enums import GNSSConstellation, LargeBitField, LargeNegativeField 10 | from tests.enum_prop.models import BitFieldModel, EnumTester 11 | 12 | 13 | class TestFieldTypeResolutionProps(TestFieldTypeResolution): 14 | MODEL_CLASS = EnumTester 15 | 16 | def test_large_bitfields(self): 17 | tester = BitFieldModel.objects.create( 18 | bit_field_small=GNSSConstellation.GPS | GNSSConstellation.GLONASS 19 | ) 20 | from django.db.models import BinaryField, PositiveSmallIntegerField 21 | 22 | self.assertIsInstance( 23 | tester._meta.get_field("bit_field_small"), PositiveSmallIntegerField 24 | ) 25 | self.assertIsInstance(tester._meta.get_field("bit_field_large"), BinaryField) 26 | self.assertIsInstance(tester._meta.get_field("large_neg"), BinaryField) 27 | 28 | self.assertEqual( 29 | tester.bit_field_small, 30 | GNSSConstellation.GPS | GNSSConstellation.GLONASS, 31 | ) 32 | self.assertEqual(tester.bit_field_large, None) 33 | self.assertEqual(tester.large_neg, LargeNegativeField.NEG_ONE) 34 | self.assertEqual(tester.no_default, LargeBitField(0)) 35 | 36 | self.assertEqual( 37 | BitFieldModel.objects.filter(bit_field_large__isnull=True).count(), 1 38 | ) 39 | tester.bit_field_large = LargeBitField.ONE | LargeBitField.TWO 40 | tester.save() 41 | self.assertEqual( 42 | BitFieldModel.objects.filter(bit_field_large__isnull=True).count(), 0 43 | ) 44 | self.assertEqual( 45 | BitFieldModel.objects.filter( 46 | bit_field_large=LargeBitField.ONE | LargeBitField.TWO 47 | ).count(), 48 | 1, 49 | ) 50 | self.assertEqual( 51 | BitFieldModel.objects.filter(bit_field_large=LargeBitField.ONE).count(), 52 | 0, 53 | ) 54 | 55 | # todo this breaks on sqlite, integer overflow - what about other backends? 56 | # BitFieldModel.objects.filter(bit_field_large=LargeBitField.ONE | LargeBitField.TWO).update(bit_field_large=F('bit_field_large').bitand(~LargeBitField.TWO)) 57 | 58 | BitFieldModel.objects.filter( 59 | bit_field_large=LargeBitField.ONE | LargeBitField.TWO 60 | ).update(bit_field_large=LargeBitField.ONE & ~LargeBitField.TWO) 61 | 62 | self.assertEqual( 63 | BitFieldModel.objects.filter(bit_field_large=LargeBitField.ONE).count(), 64 | 1, 65 | ) 66 | 67 | self.assertEqual( 68 | BitFieldModel.objects.filter( 69 | bit_field_small=GNSSConstellation.GPS | GNSSConstellation.GLONASS 70 | ).count(), 71 | 1, 72 | ) 73 | 74 | BitFieldModel.objects.filter( 75 | bit_field_small=GNSSConstellation.GPS | GNSSConstellation.GLONASS 76 | ).update( 77 | bit_field_small=F("bit_field_small").bitand(~GNSSConstellation.GLONASS) 78 | ) 79 | 80 | self.assertEqual( 81 | BitFieldModel.objects.filter( 82 | bit_field_small=GNSSConstellation.GPS | GNSSConstellation.GLONASS 83 | ).count(), 84 | 0, 85 | ) 86 | 87 | self.assertEqual( 88 | BitFieldModel.objects.filter(bit_field_small=GNSSConstellation.GPS).count(), 89 | 1, 90 | ) 91 | 92 | tester2 = BitFieldModel.objects.create( 93 | bit_field_small=GNSSConstellation.GPS | GNSSConstellation.GLONASS, 94 | bit_field_large=LargeBitField.ONE | LargeBitField.TWO, 95 | large_neg=None, 96 | ) 97 | 98 | # has_any and has_all are not supported on ExtraLarge bit fields 99 | with self.assertRaises(FieldError): 100 | BitFieldModel.objects.filter(bit_field_large__has_any=LargeBitField.ONE) 101 | 102 | with self.assertRaises(FieldError): 103 | BitFieldModel.objects.filter( 104 | bit_field_large__has_all=LargeBitField.ONE | LargeBitField.TWO 105 | ) 106 | 107 | with self.assertRaises(FieldError): 108 | BitFieldModel.objects.filter(large_neg__has_any=LargeNegativeField.NEG_ONE) 109 | 110 | with self.assertRaises(FieldError): 111 | BitFieldModel.objects.filter( 112 | large_neg__has_all=LargeNegativeField.NEG_ONE | LargeNegativeField.ZERO 113 | ) 114 | 115 | 116 | TestFieldTypeResolution = None 117 | -------------------------------------------------------------------------------- /tests/test_flags_ep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("enum_properties") 4 | from tests.test_flags import FlagTests 5 | from tests.enum_prop.models import EnumFlagPropTester, EnumFlagPropTesterRelated 6 | from django_enum.utils import choices, names 7 | 8 | 9 | class FlagTestsProp(FlagTests): 10 | MODEL_CLASS = EnumFlagPropTester 11 | RELATED_CLASS = EnumFlagPropTesterRelated 12 | 13 | def test_prop_enum(self): 14 | from tests.enum_prop.enums import ( 15 | GNSSConstellation, 16 | SmallNegativeFlagEnum, 17 | SmallPositiveFlagEnum, 18 | ) 19 | 20 | self.assertEqual(GNSSConstellation.GPS, GNSSConstellation("gps")) 21 | self.assertEqual(GNSSConstellation.GLONASS, GNSSConstellation("GLONASS")) 22 | self.assertEqual(GNSSConstellation.GALILEO, GNSSConstellation("galileo")) 23 | self.assertEqual(GNSSConstellation.BEIDOU, GNSSConstellation("BeiDou")) 24 | self.assertEqual(GNSSConstellation.QZSS, GNSSConstellation("qzss")) 25 | 26 | self.assertEqual(choices(SmallNegativeFlagEnum), SmallNegativeFlagEnum.choices) 27 | self.assertEqual(names(SmallNegativeFlagEnum), SmallNegativeFlagEnum.names) 28 | 29 | self.assertEqual(choices(SmallPositiveFlagEnum), SmallPositiveFlagEnum.choices) 30 | self.assertEqual(names(SmallPositiveFlagEnum), SmallPositiveFlagEnum.names) 31 | 32 | 33 | FlagTests = None 34 | -------------------------------------------------------------------------------- /tests/test_name_override.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from pathlib import Path 3 | from decimal import Decimal 4 | from django_enum.forms import EnumChoiceField 5 | from django_enum.utils import choices 6 | 7 | 8 | class TestNameOverride(TestCase): 9 | """ 10 | https://github.com/django-commons/django-enum/issues/77 11 | """ 12 | 13 | def test_name_override(self): 14 | from tests.djenum.models import NameOverrideTest 15 | 16 | self.assertEqual(NameOverrideTest._meta.get_field("enum_field").primitive, str) 17 | 18 | NameOverrideTest.objects.create(enum_field="V1") 19 | obj = NameOverrideTest.objects.first() 20 | self.assertEqual(obj.enum_field, "V1") 21 | self.assertEqual(obj.get_enum_field_display(), "Value 1") 22 | -------------------------------------------------------------------------------- /tests/test_queries.py: -------------------------------------------------------------------------------- 1 | from tests.utils import EnumTypeMixin 2 | from django.test import TestCase 3 | from tests.djenum.models import EnumTester 4 | 5 | 6 | class TestEnumQueries(EnumTypeMixin, TestCase): 7 | MODEL_CLASS = EnumTester 8 | 9 | def setUp(self): 10 | self.MODEL_CLASS.objects.all().delete() 11 | 12 | self.MODEL_CLASS.objects.create( 13 | small_pos_int=self.SmallPosIntEnum.VAL2, 14 | small_int=self.SmallIntEnum.VAL0, 15 | pos_int=self.PosIntEnum.VAL1, 16 | int=self.IntEnum.VALn1, 17 | big_pos_int=self.BigPosIntEnum.VAL3, 18 | big_int=self.BigIntEnum.VAL2, 19 | constant=self.Constants.GOLDEN_RATIO, 20 | text=self.TextEnum.VALUE2, 21 | extern=self.ExternEnum.ONE, 22 | ) 23 | self.MODEL_CLASS.objects.create( 24 | small_pos_int=self.SmallPosIntEnum.VAL2, 25 | small_int=self.SmallIntEnum.VAL0, 26 | pos_int=self.PosIntEnum.VAL1, 27 | int=self.IntEnum.VALn1, 28 | big_pos_int=self.BigPosIntEnum.VAL3, 29 | big_int=self.BigIntEnum.VAL2, 30 | constant=self.Constants.GOLDEN_RATIO, 31 | text=self.TextEnum.VALUE2, 32 | extern=self.ExternEnum.ONE, 33 | ) 34 | 35 | self.MODEL_CLASS.objects.create() 36 | 37 | def test_query(self): 38 | self.assertEqual( 39 | self.MODEL_CLASS.objects.filter( 40 | small_pos_int=self.SmallPosIntEnum.VAL2 41 | ).count(), 42 | 2, 43 | ) 44 | self.assertEqual( 45 | self.MODEL_CLASS.objects.filter( 46 | small_pos_int=self.SmallPosIntEnum.VAL2.value 47 | ).count(), 48 | 2, 49 | ) 50 | 51 | self.assertEqual( 52 | self.MODEL_CLASS.objects.filter( 53 | big_pos_int=self.BigPosIntEnum.VAL3 54 | ).count(), 55 | 2, 56 | ) 57 | self.assertEqual(self.MODEL_CLASS.objects.filter(big_pos_int=None).count(), 1) 58 | 59 | self.assertEqual( 60 | self.MODEL_CLASS.objects.filter( 61 | constant=self.Constants.GOLDEN_RATIO 62 | ).count(), 63 | 2, 64 | ) 65 | self.assertEqual( 66 | self.MODEL_CLASS.objects.filter( 67 | constant=self.Constants.GOLDEN_RATIO.value 68 | ).count(), 69 | 2, 70 | ) 71 | self.assertEqual( 72 | self.MODEL_CLASS.objects.filter(constant__isnull=True).count(), 1 73 | ) 74 | 75 | self.assertEqual( 76 | self.MODEL_CLASS.objects.filter(text=self.TextEnum.VALUE2).count(), 2 77 | ) 78 | 79 | self.assertEqual( 80 | self.MODEL_CLASS.objects.filter(extern=self.ExternEnum.ONE).count(), 2 81 | ) 82 | self.assertEqual( 83 | self.MODEL_CLASS.objects.filter(extern=self.ExternEnum.TWO).count(), 0 84 | ) 85 | 86 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, int_field="a") 87 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, float_field="a") 88 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, constant="Pi") 89 | self.assertRaises( 90 | ValueError, self.MODEL_CLASS.objects.filter, big_pos_int="Val3" 91 | ) 92 | self.assertRaises( 93 | ValueError, self.MODEL_CLASS.objects.filter, big_pos_int=type("WrongType")() 94 | ) 95 | -------------------------------------------------------------------------------- /tests/test_queries_ep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("enum_properties") 4 | 5 | from tests.test_queries import TestEnumQueries 6 | from tests.enum_prop.models import EnumTester 7 | 8 | 9 | class TestEnumQueriesProps(TestEnumQueries): 10 | MODEL_CLASS = EnumTester 11 | 12 | def test_query(self): 13 | # don't call super b/c referenced types are different 14 | 15 | self.assertEqual( 16 | self.MODEL_CLASS.objects.filter( 17 | small_pos_int=self.SmallPosIntEnum.VAL2 18 | ).count(), 19 | 2, 20 | ) 21 | self.assertEqual( 22 | self.MODEL_CLASS.objects.filter( 23 | small_pos_int=self.SmallPosIntEnum.VAL2.value 24 | ).count(), 25 | 2, 26 | ) 27 | self.assertEqual( 28 | self.MODEL_CLASS.objects.filter(small_pos_int="Value 2").count(), 2 29 | ) 30 | self.assertEqual( 31 | self.MODEL_CLASS.objects.filter( 32 | small_pos_int=self.SmallPosIntEnum.VAL2.name 33 | ).count(), 34 | 2, 35 | ) 36 | 37 | self.assertEqual( 38 | self.MODEL_CLASS.objects.filter( 39 | big_pos_int=self.BigPosIntEnum.VAL3 40 | ).count(), 41 | 2, 42 | ) 43 | self.assertEqual( 44 | self.MODEL_CLASS.objects.filter( 45 | big_pos_int=self.BigPosIntEnum.VAL3.label 46 | ).count(), 47 | 2, 48 | ) 49 | self.assertEqual(self.MODEL_CLASS.objects.filter(big_pos_int=None).count(), 1) 50 | 51 | self.assertEqual( 52 | self.MODEL_CLASS.objects.filter( 53 | constant=self.Constants.GOLDEN_RATIO 54 | ).count(), 55 | 2, 56 | ) 57 | self.assertEqual( 58 | self.MODEL_CLASS.objects.filter( 59 | constant=self.Constants.GOLDEN_RATIO.name 60 | ).count(), 61 | 2, 62 | ) 63 | self.assertEqual( 64 | self.MODEL_CLASS.objects.filter( 65 | constant=self.Constants.GOLDEN_RATIO.value 66 | ).count(), 67 | 2, 68 | ) 69 | self.assertEqual( 70 | self.MODEL_CLASS.objects.filter(constant__isnull=True).count(), 1 71 | ) 72 | 73 | # test symmetry 74 | self.assertEqual( 75 | self.MODEL_CLASS.objects.filter( 76 | constant=self.Constants.GOLDEN_RATIO.symbol 77 | ).count(), 78 | 2, 79 | ) 80 | self.assertEqual(self.MODEL_CLASS.objects.filter(constant="φ").count(), 2) 81 | 82 | self.assertEqual( 83 | self.MODEL_CLASS.objects.filter(text=self.TextEnum.VALUE2).count(), 2 84 | ) 85 | self.assertEqual(len(self.TextEnum.VALUE2.aliases), 3) 86 | for alias in self.TextEnum.VALUE2.aliases: 87 | self.assertEqual(self.MODEL_CLASS.objects.filter(text=alias).count(), 2) 88 | 89 | self.assertEqual(self.MODEL_CLASS.objects.filter(extern="One").count(), 2) 90 | self.assertEqual(self.MODEL_CLASS.objects.filter(extern="Two").count(), 0) 91 | 92 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, int_field="a") 93 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, float_field="a") 94 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, constant="p") 95 | self.assertRaises(ValueError, self.MODEL_CLASS.objects.filter, big_pos_int="p") 96 | self.assertRaises( 97 | ValueError, 98 | self.MODEL_CLASS.objects.filter, 99 | big_pos_int=type("WrongType")(), 100 | ) 101 | 102 | 103 | TestEnumQueries = None 104 | -------------------------------------------------------------------------------- /tests/test_requests_flags_ep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("enum_properties") 4 | from importlib.util import find_spec 5 | from tests.test_requests_flags import TestFlagRequests 6 | from tests.enum_prop.models import FlagFilterTester 7 | from django.urls import reverse 8 | 9 | 10 | class TestFlagRequestsProps(TestFlagRequests): 11 | MODEL_CLASS = FlagFilterTester 12 | NAMESPACE = "tests_enum_prop" 13 | 14 | @property 15 | def post_params(self): 16 | return { 17 | "small_flag": self.SmallPositiveFlagEnum.TWO 18 | | self.SmallPositiveFlagEnum.THREE, 19 | "flag": [self.PositiveFlagEnum.ONE.value | self.PositiveFlagEnum.TWO.value], 20 | "flag_no_coerce": 0, 21 | "big_flag": self.BigPositiveFlagEnum.ONE | (1 << 12) | (1 << 4), 22 | } 23 | 24 | @property 25 | def post_params_symmetric(self): 26 | return { 27 | "small_flag": self.SmallPositiveFlagEnum.TWO.number, 28 | "flag": [self.PositiveFlagEnum.ONE.name, self.PositiveFlagEnum.TWO.label], 29 | "flag_no_coerce": self.PositiveFlagEnum(0), 30 | "big_flag": [self.BigPositiveFlagEnum.ONE.version, (1 << 12), (1 << 4)], 31 | } 32 | 33 | @property 34 | def field_filter_properties(self): 35 | return { 36 | "small_flag": ["value", "name", "label", "number"], 37 | "flag": ["value", "name", "label", "number"], 38 | "flag_no_coerce": ["value", "name", "label", "number"], 39 | "big_flag": ["value", "name", "label", "version"], 40 | } 41 | 42 | def get_enum_val(self, enum, value): 43 | if value == "": 44 | return None 45 | return enum(value) 46 | 47 | if find_spec("rest_framework"): # pragma: no cover 48 | 49 | def test_drf_flag_field(self): 50 | from django_enum.drf import FlagField 51 | 52 | super().test_drf_flag_field() 53 | 54 | field = FlagField(self.SmallPositiveFlagEnum) 55 | self.assertEqual( 56 | field.to_internal_value( 57 | [ 58 | self.SmallPositiveFlagEnum.ONE.label, 59 | self.SmallPositiveFlagEnum.TWO.number, 60 | ] 61 | ), 62 | (self.SmallPositiveFlagEnum.ONE | self.SmallPositiveFlagEnum.TWO), 63 | ) 64 | 65 | if find_spec("django_filters"): 66 | 67 | def test_django_filter_flags(self): 68 | self.do_test_django_filter( 69 | reverse(f"{self.NAMESPACE}:flag-filter-symmetric") 70 | ) 71 | 72 | def test_django_filter_flags_exclude(self): 73 | self.do_test_django_filter( 74 | reverse(f"{self.NAMESPACE}:flag-filter-exclude-symmetric"), exclude=True 75 | ) 76 | 77 | def test_django_filter_flags_conjoined(self): 78 | self.do_test_django_filter( 79 | reverse(f"{self.NAMESPACE}:flag-filter-conjoined-symmetric"), 80 | conjoined=True, 81 | ) 82 | 83 | def test_django_filter_flags_conjoined_exclude(self): 84 | self.do_test_django_filter( 85 | reverse(f"{self.NAMESPACE}:flag-filter-conjoined-exclude-symmetric"), 86 | exclude=True, 87 | conjoined=True, 88 | ) 89 | 90 | 91 | # don't run these tests again! 92 | TestFlagRequests = None 93 | -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from decimal import Decimal 3 | from django.core.exceptions import ValidationError 4 | 5 | 6 | class TestValidatorAdapter(TestCase): 7 | def test(self): 8 | from django.core.validators import DecimalValidator 9 | 10 | from django_enum.fields import EnumValidatorAdapter 11 | 12 | validator = DecimalValidator(max_digits=5, decimal_places=2) 13 | adapted = EnumValidatorAdapter(validator, allow_null=False) 14 | self.assertEqual(adapted.max_digits, validator.max_digits) 15 | self.assertEqual(adapted.decimal_places, validator.decimal_places) 16 | self.assertEqual(adapted, validator) 17 | self.assertEqual(repr(adapted), f"EnumValidatorAdapter({repr(validator)})") 18 | ok = Decimal("123.45") 19 | bad = Decimal("123.456") 20 | self.assertIsNone(validator(ok)) 21 | self.assertIsNone(adapted(ok)) 22 | self.assertRaises(ValidationError, validator, bad) 23 | self.assertRaises(ValidationError, adapted, bad) 24 | -------------------------------------------------------------------------------- /tests/test_verify_environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import django 4 | from django.db import connection 5 | import typing as t 6 | from django.test import TestCase 7 | import pytest 8 | 9 | 10 | def get_postgresql_version() -> t.Tuple[int, ...]: 11 | with connection.cursor() as cursor: 12 | cursor.execute("SELECT version();") 13 | version = cursor.fetchone() 14 | db, ver = version[0].split(" ")[:2] 15 | assert db == "PostgreSQL" 16 | return tuple(int(v) for v in ver.split(".")[:2] if v) 17 | 18 | 19 | def get_mysql_version(): 20 | with connection.cursor() as cursor: 21 | cursor.execute("SELECT VERSION();") 22 | version = cursor.fetchone() 23 | print("MySQL version:", version[0]) 24 | 25 | 26 | @pytest.mark.skipif( 27 | os.environ.get("GITHUB_ACTIONS", None) != "true", 28 | reason="This test is only for the CI environment.", 29 | ) 30 | class TestEnvironment(TestCase): 31 | def test(self): 32 | # verify that the environment is set up correctly - this is used in CI to make 33 | # sure we're testing against the dependencies we think we are 34 | 35 | rdbms = os.environ["RDBMS"] 36 | expected_python = os.environ["TEST_PYTHON_VERSION"] 37 | expected_django = os.environ["TEST_DJANGO_VERSION"] 38 | expected_db_ver = os.environ.get("TEST_DATABASE_VERSION", None) 39 | expected_client = os.environ.get("TEST_DATABASE_CLIENT_VERSION", None) 40 | 41 | expected_python = tuple(int(v) for v in expected_python.split(".") if v) 42 | assert sys.version_info[: len(expected_python)] == expected_python, ( 43 | f"Python Version Mismatch: {sys.version_info[: len(expected_python)]} != " 44 | f"{expected_python}" 45 | ) 46 | 47 | try: 48 | expected_django = tuple(int(v) for v in expected_django.split(".") if v) 49 | assert django.VERSION[: len(expected_django)] == expected_django, ( 50 | f"Django Version Mismatch: {django.VERSION[: len(expected_django)]} != " 51 | f"{expected_django}" 52 | ) 53 | except ValueError: 54 | assert expected_django == django.__version__ 55 | 56 | if expected_db_ver: 57 | if rdbms == "postgres": 58 | if expected_db_ver == "latest": 59 | # todo 60 | pass 61 | else: 62 | expected_version = tuple( 63 | int(v) for v in expected_db_ver.split(".") if v 64 | ) 65 | assert ( 66 | expected_version 67 | == get_postgresql_version()[: len(expected_version)] 68 | ) 69 | if expected_client == "psycopg3": 70 | import psycopg 71 | 72 | assert psycopg.__version__[0] == "3" 73 | else: 74 | import psycopg2 75 | 76 | assert psycopg2.__version__[0] == "2" 77 | elif rdbms == "mysql": 78 | pass 79 | elif rdbms == "mariadb": 80 | pass 81 | elif rdbms == "sqlite": 82 | pass 83 | elif rdbms == "oracle": 84 | pass 85 | -------------------------------------------------------------------------------- /tests/tmpls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/tmpls/__init__.py -------------------------------------------------------------------------------- /tests/tmpls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TmplsConfig(AppConfig): 5 | name = "tests.tmpls" 6 | label = name.replace(".", "_") 7 | 8 | 9 | 7 10 | -------------------------------------------------------------------------------- /tests/tmpls/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enum Testing 5 | 6 | 7 | 8 | {% block content %} 9 | {% endblock content %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/tmpls/templates/enumtester_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
{% csrf_token %} 5 |

Are you sure you want to delete "{{ object }}"?

6 | {{ form }} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /tests/tmpls/templates/enumtester_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load test_tags %} 3 | 4 | {% block content %} 5 |
6 |

{{ object.pk }}

7 |

small_pos_int: {{ object.small_pos_int.value|to_str }} {{ object.small_pos_int.label|to_str }}

8 |

small_int: {{ object.small_int.value|to_str }} {{ object.small_int.label|to_str }}

9 |

pos_int: {{ object.pos_int.value|to_str }} {{ object.pos_int.label|to_str }}

10 |

int: {{ object.int.value|to_str }} {{ object.int.label|to_str }}

11 |

big_pos_int: {{ object.big_pos_int.value|to_str }} {{ object.big_pos_int.label|to_str }}

12 |

big_int: {{ object.big_int.value|to_str }} {{ object.big_int.label|to_str }}

13 | 14 |

date_enum: {{ object.date_enum.value|to_str }} {{ object.date_enum.label|to_str }}

15 | 16 | {% if object.datetime_enum|is_enum %} 17 |

datetime_enum: {{ object.datetime_enum.value|to_str }} {{ object.datetime_enum.label|to_str }}

18 | {% else %} 19 |

datetime_enum: {{ object.datetime_enum|to_str }} {{ object.datetime_enum|to_str }}

20 | {% endif %} 21 | 22 |

time_enum: {{ object.time_enum.value|to_str }} {{ object.time_enum.label|to_str }}

23 |

duration_enum: {{ object.duration_enum.value|to_str }} {{ object.duration_enum.label|to_str }}

24 |

decimal_enum: {{ object.decimal_enum.value|to_str }} {{ object.decimal_enum.label|to_str }}

25 | 26 |

constant: {{ object.constant.value|to_str }} {{ object.constant.label|to_str }}

27 |

text: {{ object.text.value|to_str }} {{ object.text.label|to_str }}

28 |

extern: {{ object.extern.value|to_str }} {{ object.extern.name|to_str }}

29 |

dj_int_enum: {{ object.dj_int_enum.value|to_str }} {{ object.dj_int_enum.label|to_str }}

30 |

dj_text_enum: {{ object.dj_text_enum.value|to_str }} {{ object.dj_text_enum.label|to_str }}

31 | 32 | {% if object.non_strict_int|is_enum %} 33 |

non_strict_int: {{ object.non_strict_int.value|to_str }} {{ object.non_strict_int.label|to_str }}

34 | {% else %} 35 |

non_strict_int: {{ object.non_strict_int|to_str }} {{ object.non_strict_int|to_str }}

36 | {% endif %} 37 | 38 | {% if object.non_strict_text|is_enum %} 39 |

non_strict_text: {{ object.non_strict_text.value|to_str }} {{ object.non_strict_text.label|to_str }}

40 | {% else %} 41 |

non_strict_text: {{ object.non_strict_text|to_str }} {{ object.non_strict_text|to_str }}

42 | {% endif %} 43 | 44 |

no_coerce: {{ object.no_coerce|to_str }} {{ object.no_coerce|to_str }}

45 | 46 |

47 | {% if update_path %} 48 | Edit 49 | {% endif %} 50 | {% if delete_path %} 51 | Delete 52 | {% endif %} 53 |

54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /tests/tmpls/templates/enumtester_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | 7 | {{ form.as_table }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /tests/tmpls/templates/enumtester_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load test_tags %} 3 | 4 | {% block content %} 5 |

EnumTester Objects

6 | {% for object in object_list %} 7 |
8 |

{{ object.pk }}

9 |

small_pos_int: {{ object.small_pos_int.value|to_str }} {{ object.small_pos_int.label|to_str }}

10 |

small_int: {{ object.small_int.value|to_str }} {{ object.small_int.label|to_str }}

11 |

pos_int: {{ object.pos_int.value|to_str }} {{ object.pos_int.label|to_str }}

12 |

int: {{ object.int.value|to_str }} {{ object.int.label|to_str }}

13 |

big_pos_int: {{ object.big_pos_int.value|to_str }} {{ object.big_pos_int.label|to_str }}

14 |

big_int: {{ object.big_int.value|to_str }} {{ object.big_int.label|to_str }}

15 | 16 |

date_enum: {{ object.date_enum.value|to_str }} {{ object.date_enum.label|to_str }}

17 | 18 | {% if object.datetime_enum|is_enum %} 19 |

datetime_enum: {{ object.datetime_enum.value|to_str }} {{ object.datetime_enum.label|to_str }}

20 | {% else %} 21 |

datetime_enum: {{ object.datetime_enum|to_str }} {{ object.datetime_enum|to_str }}

22 | {% endif %} 23 | 24 |

time_enum: {{ object.time_enum.value|to_str }} {{ object.time_enum.label|to_str }}

25 |

duration_enum: {{ object.duration_enum.value|to_str }} {{ object.duration_enum.label|to_str }}

26 |

decimal_enum: {{ object.decimal_enum.value|to_str }} {{ object.decimal_enum.label|to_str }}

27 | 28 |

constant: {{ object.constant.value|to_str }} {{ object.constant.label|to_str }}

29 |

text: {{ object.text.value|to_str }} {{ object.text.label|to_str }}

30 |

extern: {{ object.extern.value|to_str }} {{ object.extern.name|to_str }}

31 |

dj_int_enum: {{ object.dj_int_enum.value|to_str }} {{ object.dj_int_enum.label|to_str }}

32 |

dj_text_enum: {{ object.dj_text_enum.value|to_str }} {{ object.dj_text_enum.label|to_str }}

33 | 34 | {% if object.non_strict_int|is_enum %} 35 |

non_strict_int: {{ object.non_strict_int.value|to_str}} {{ object.non_strict_int.label|to_str }}

36 | {% else %} 37 |

non_strict_int: {{ object.non_strict_int|to_str}} {{ object.non_strict_int|to_str }}

38 | {% endif %} 39 | 40 | {% if object.non_strict_text|is_enum %} 41 |

non_strict_text: {{ object.non_strict_text.value|to_str}} {{ object.non_strict_text.label|to_str }}

42 | {% else %} 43 |

non_strict_text: {{ object.non_strict_text|to_str}} {{ object.non_strict_text|to_str }}

44 | {% endif %} 45 | 46 |

no_coerce: {{ object.no_coerce|to_str}} {{ object.no_coerce|to_str }}

47 | 48 |

49 | {% if update_path %} 50 | Edit 51 | {% endif %} 52 | {% if delete_path %} 53 | Delete 54 | {% endif %} 55 |

56 |
57 | {% endfor %} 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /tests/tmpls/templates/flagtester_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
{% csrf_token %} 5 |

Are you sure you want to delete "{{ object }}"?

6 | {{ form }} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /tests/tmpls/templates/flagtester_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load test_tags %} 3 | 4 | {% block content %} 5 |
6 |

{{ object.pk }}

7 | 8 |

small_flag: {{ object.small_flag.value|to_str }} {{ object.small_flag|flags_str }}

9 |

flag: {{ object.flag.value|to_str }} {{ object.flag|flags_str }}

10 |

flag: {{ object.flag_no_coerce|to_str }} {{ object.flag_no_coerce|flags_str }}

11 |

big_flag: {{ object.big_flag.value|to_str }} {{ object.big_flag|flags_str }}

12 | 13 |

14 | {% if update_path %} 15 | Edit 16 | {% endif %} 17 | {% if delete_path %} 18 | Delete 19 | {% endif %} 20 |

21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /tests/tmpls/templates/flagtester_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | 7 | {{ form.as_table }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /tests/tmpls/templates/flagtester_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load test_tags %} 3 | 4 | {% block content %} 5 |

FlagTester Objects

6 | {% for object in object_list %} 7 |
8 |

{{ object.pk }}

9 |

small_flag: {{ object.small_flag.value|to_str }} {{ object.small_flag|flags_str }}

10 |

flag: {{ object.flag.value|to_str }} {{ object.flag|flags_str }}

11 |

flag: {{ object.flag_no_coerce|to_str }} {{ object.flag_no_coerce|flags_str }}

12 |

big_flag: {{ object.big_flag.value|to_str }} {{ object.big_flag|flags_str }}

13 | 14 |

15 | {% if update_path %} 16 | Edit 17 | {% endif %} 18 | {% if delete_path %} 19 | Delete 20 | {% endif %} 21 |

22 |
23 | {% endfor %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /tests/tmpls/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-enum/d82eb840da01957ffad84ccf12fb8ffe4e2fae74/tests/tmpls/templatetags/__init__.py -------------------------------------------------------------------------------- /tests/tmpls/templatetags/test_tags.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, Flag 2 | from django_enum.utils import decompose, get_set_values, get_set_bits 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter(name="is_enum") 9 | def is_enum(instance): 10 | return isinstance(instance, Enum) 11 | 12 | 13 | @register.filter(name="to_str") 14 | def to_str(value): 15 | if value is None: 16 | return "" 17 | return str(value) 18 | 19 | 20 | @register.filter(name="flags_str") 21 | def flags_str(value): 22 | if not value: 23 | return "" 24 | if isinstance(value, Flag): 25 | labeled = decompose(value) 26 | values = {en.value for en in labeled} 27 | extra_values = [val for val in get_set_values(value) if val not in values] 28 | return "|".join( 29 | [en.name for en in labeled] + [str(val) for val in extra_values] 30 | ) 31 | return "|".join([str(bit) for bit in get_set_bits(value)]) 32 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.urls import include, path 4 | 5 | 6 | urlpatterns = [ 7 | path("admin/", admin.site.urls), 8 | path("djenum/", include("tests.djenum.urls")), 9 | path("converters/", include("tests.converters.urls")), 10 | path("", include("tests.examples.urls")), 11 | path("howto/", include("tests.examples.urls_howto")), 12 | path("howto/forms/", include("tests.examples.urls_forms")), 13 | ] 14 | 15 | if "tests.enum_prop" in settings.INSTALLED_APPS: # pragma: no cover 16 | urlpatterns.append(path("enum_prop/", include("tests.enum_prop.urls"))) 17 | --------------------------------------------------------------------------------