├── .gitignore ├── CONTRIBUTORS.txt ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── _static │ └── .gitignore ├── admin.rst ├── conf.py ├── enum.rst ├── field.rst ├── index.rst ├── queries.rst ├── requirements.txt ├── setup.rst └── usage.rst ├── enumchoicefield ├── __init__.py ├── admin.py ├── enum.py ├── fields.py ├── forms.py ├── locale │ └── de │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── tests │ ├── __init__.py │ ├── locale │ │ ├── de │ │ │ └── LC_MESSAGES │ │ │ │ ├── django.mo │ │ │ │ └── django.po │ │ └── en │ │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_make_nullable.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── test_enum.py │ ├── test_field.py │ ├── test_forms.py │ └── test_utils.py ├── utils.py └── version.py ├── requirements-dev.txt ├── requirements-test.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | dist/ 4 | .python-version 5 | .tox 6 | venv/ 7 | __pycache__ 8 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Tim Heap 2 | Zach Rose 3 | Danielle Madeley 4 | Andreas Donig 5 | mrodmat (on Github) 6 | Kevin Gutiérrez @jkevingutierrez 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, The Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include enumchoicefield/ *.po *.mo 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Django EnumChoiceField 3 | ====================== 4 | 5 | .. image:: https://travis-ci.org/timheap/django-enumchoicefield.svg?branch=master 6 | :target: https://travis-ci.org/timheap/django-enumchoicefield 7 | .. image:: https://readthedocs.org/projects/django-enumchoicefield/badge/?version=latest 8 | :target: https://django-enumchoicefield.readthedocs.io/en/latest/ 9 | .. image:: https://badge.fury.io/py/django-enumchoicefield.svg 10 | :target: https://pypi.org/project/django-enumchoicefield/ 11 | 12 | A Django model field for native Python Enums. 13 | 14 | .. code:: python 15 | 16 | from enumchoicefield import ChoiceEnum, EnumChoiceField 17 | 18 | class Fruit(ChoiceEnum): 19 | apple = "Apple" 20 | banana = "Banana" 21 | orange = "Orange" 22 | 23 | class Profile(models.Model): 24 | name = models.CharField(max_length=100) 25 | favourite_fruit = EnumChoiceField(Fruit, default=Fruit.banana) 26 | 27 | Documentation 28 | ============= 29 | 30 | See `Django EnumChoiceField on ReadTheDocs `_. 31 | 32 | Testing 33 | ======= 34 | 35 | To run the tests: 36 | 37 | .. code:: sh 38 | 39 | $ virtualenv venv 40 | $ source venv/bin/activate 41 | $ pip install -r requirements-dev.txt 42 | $ tox 43 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoEnumChoiceField.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoEnumChoiceField.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoEnumChoiceField" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoEnumChoiceField" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-moth/django-enumchoicefield/2d30334b27ed76eae6adbe3eb746c2b37b28666d/docs/_static/.gitignore -------------------------------------------------------------------------------- /docs/admin.rst: -------------------------------------------------------------------------------- 1 | .. _admin: 2 | 3 | =========================== 4 | Using with the Django admin 5 | =========================== 6 | 7 | :class:`~enumchoicefield.fields.EnumChoiceField`\s 8 | are compatible with the Django admin out of the box, 9 | with one exception. If you want to use a 10 | :class:`~enumchoicefield.fields.EnumChoiceField` 11 | in a :attr:`~django.contrib.admin.ModelAdmin.list_filter`, you need to use the 12 | :class:`~enumchoicefield.admin.EnumListFilter`. 13 | 14 | .. autoclass:: enumchoicefield.admin.EnumListFilter 15 | 16 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django EnumChoiceField documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 3 16:17:00 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | 18 | import sphinx_rtd_theme 19 | from enumchoicefield.version import version as module_version 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.intersphinx', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'Django EnumChoiceField' 55 | copyright = '2016, Tim Heap' 56 | author = 'Tim Heap' 57 | 58 | version = '.'.join(module_version.split('.')[:2]) 59 | # The full version, including alpha/beta/rc tags. 60 | release = module_version 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | #today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | #today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ['_build'] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | #default_role = None 82 | 83 | # If true, '()' will be appended to :func: etc. cross-reference text. 84 | #add_function_parentheses = True 85 | 86 | # If true, the current module name will be prepended to all description 87 | # unit titles (such as .. function::). 88 | #add_module_names = True 89 | 90 | # If true, sectionauthor and moduleauthor directives will be shown in the 91 | # output. They are ignored by default. 92 | #show_authors = False 93 | 94 | # The name of the Pygments (syntax highlighting) style to use. 95 | pygments_style = 'sphinx' 96 | 97 | # A list of ignored prefixes for module index sorting. 98 | #modindex_common_prefix = [] 99 | 100 | # If true, keep warnings as "system message" paragraphs in the built documents. 101 | #keep_warnings = False 102 | 103 | # If true, `todo` and `todoList` produce output, else they produce nothing. 104 | todo_include_todos = False 105 | 106 | # -- Interspinx options --------------------------------------------------- 107 | 108 | intersphinx_mapping = { 109 | 'django': ('https://docs.djangoproject.com/en/dev/', 110 | 'https://docs.djangoproject.com/en/dev/_objects/'), 111 | 'python': ('https://docs.python.org/3.5', None), 112 | } 113 | 114 | 115 | # -- Options for HTML output ---------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. See the documentation for 118 | # a list of builtin themes. 119 | html_theme = "sphinx_rtd_theme" 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | #html_theme_options = {} 128 | 129 | # The name for this set of Sphinx documents. If None, it defaults to 130 | # " v documentation". 131 | #html_title = None 132 | 133 | # A shorter title for the navigation bar. Default is the same as html_title. 134 | #html_short_title = None 135 | 136 | # The name of an image file (relative to this directory) to place at the top 137 | # of the sidebar. 138 | #html_logo = None 139 | 140 | # The name of an image file (within the static path) to use as favicon of the 141 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 142 | # pixels large. 143 | #html_favicon = None 144 | 145 | # Add any paths that contain custom static files (such as style sheets) here, 146 | # relative to this directory. They are copied after the builtin static files, 147 | # so a file named "default.css" will overwrite the builtin "default.css". 148 | html_static_path = ['_static'] 149 | 150 | # Add any extra paths that contain custom files (such as robots.txt or 151 | # .htaccess) here, relative to this directory. These files are copied 152 | # directly to the root of the documentation. 153 | #html_extra_path = [] 154 | 155 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 156 | # using the given strftime format. 157 | #html_last_updated_fmt = '%b %d, %Y' 158 | 159 | # If true, SmartyPants will be used to convert quotes and dashes to 160 | # typographically correct entities. 161 | #html_use_smartypants = True 162 | 163 | # Custom sidebar templates, maps document names to template names. 164 | #html_sidebars = {} 165 | 166 | # Additional templates that should be rendered to pages, maps page names to 167 | # template names. 168 | #html_additional_pages = {} 169 | 170 | # If false, no module index is generated. 171 | #html_domain_indices = True 172 | 173 | # If false, no index is generated. 174 | #html_use_index = True 175 | 176 | # If true, the index is split into individual pages for each letter. 177 | #html_split_index = False 178 | 179 | # If true, links to the reST sources are added to the pages. 180 | #html_show_sourcelink = True 181 | 182 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 183 | #html_show_sphinx = True 184 | 185 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 186 | #html_show_copyright = True 187 | 188 | # If true, an OpenSearch description file will be output, and all pages will 189 | # contain a tag referring to it. The value of this option must be the 190 | # base URL from which the finished HTML is served. 191 | #html_use_opensearch = '' 192 | 193 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 194 | #html_file_suffix = None 195 | 196 | # Language to be used for generating the HTML full-text search index. 197 | # Sphinx supports the following languages: 198 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 199 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 200 | #html_search_language = 'en' 201 | 202 | # A dictionary with options for the search language support, empty by default. 203 | # Now only 'ja' uses this config value 204 | #html_search_options = {'type': 'default'} 205 | 206 | # The name of a javascript file (relative to the configuration directory) that 207 | # implements a search results scorer. If empty, the default will be used. 208 | #html_search_scorer = 'scorer.js' 209 | 210 | # Output file base name for HTML help builder. 211 | htmlhelp_basename = 'DjangoEnumChoiceFielddoc' 212 | 213 | # -- Options for LaTeX output --------------------------------------------- 214 | 215 | latex_elements = { 216 | # The paper size ('letterpaper' or 'a4paper'). 217 | #'papersize': 'letterpaper', 218 | 219 | # The font size ('10pt', '11pt' or '12pt'). 220 | #'pointsize': '10pt', 221 | 222 | # Additional stuff for the LaTeX preamble. 223 | #'preamble': '', 224 | 225 | # Latex figure (float) alignment 226 | #'figure_align': 'htbp', 227 | } 228 | 229 | # Grouping the document tree into LaTeX files. List of tuples 230 | # (source start file, target name, title, 231 | # author, documentclass [howto, manual, or own class]). 232 | latex_documents = [ 233 | (master_doc, 'DjangoEnumChoiceField.tex', 'Django EnumChoiceField Documentation', 234 | 'Tim Heap', 'manual'), 235 | ] 236 | 237 | # The name of an image file (relative to this directory) to place at the top of 238 | # the title page. 239 | #latex_logo = None 240 | 241 | # For "manual" documents, if this is true, then toplevel headings are parts, 242 | # not chapters. 243 | #latex_use_parts = False 244 | 245 | # If true, show page references after internal links. 246 | #latex_show_pagerefs = False 247 | 248 | # If true, show URL addresses after external links. 249 | #latex_show_urls = False 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #latex_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #latex_domain_indices = True 256 | 257 | 258 | # -- Options for manual page output --------------------------------------- 259 | 260 | # One entry per manual page. List of tuples 261 | # (source start file, name, description, authors, manual section). 262 | man_pages = [ 263 | (master_doc, 'djangoenumchoicefield', 'Django EnumChoiceField Documentation', 264 | [author], 1) 265 | ] 266 | 267 | # If true, show URL addresses after external links. 268 | #man_show_urls = False 269 | 270 | 271 | # -- Options for Texinfo output ------------------------------------------- 272 | 273 | # Grouping the document tree into Texinfo files. List of tuples 274 | # (source start file, target name, title, author, 275 | # dir menu entry, description, category) 276 | texinfo_documents = [ 277 | (master_doc, 'DjangoEnumChoiceField', 'Django EnumChoiceField Documentation', 278 | author, 'DjangoEnumChoiceField', 'One line description of project.', 279 | 'Miscellaneous'), 280 | ] 281 | 282 | # Documents to append as an appendix to all manuals. 283 | #texinfo_appendices = [] 284 | 285 | # If false, no module index is generated. 286 | #texinfo_domain_indices = True 287 | 288 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 289 | #texinfo_show_urls = 'footnote' 290 | 291 | # If true, do not generate a @detailmenu in the "Top" node's menu. 292 | #texinfo_no_detailmenu = False 293 | -------------------------------------------------------------------------------- /docs/enum.rst: -------------------------------------------------------------------------------- 1 | .. _enum: 2 | 3 | ============ 4 | Enum classes 5 | ============ 6 | 7 | .. module:: enumchoicefield.enum 8 | 9 | .. class:: PrettyEnum 10 | 11 | A :class:`PrettyEnum` makes defining nice, human-readable names 12 | for enum members easy. 13 | To use it, subclass :class:`PrettyEnum` and 14 | declare the enum members with their human-readable name as their value: 15 | 16 | .. code-block:: python 17 | 18 | class Fruit(PrettyEnum): 19 | apple = "Apple" 20 | banana = "Banana" 21 | orange = "Orange" 22 | 23 | The members' values will be automatically set to ascending integers, 24 | starting at one. 25 | In the example above, 26 | ``Fruit.apple.value`` is ``1``, and 27 | ``Fruit.orange.value`` is ``3``. 28 | 29 | 30 | .. class:: DeconstructableEnum 31 | 32 | .. py:method:: deconstruct() 33 | 34 | a :class:`DeconstructableEnum` defines :meth:`deconstruct`, 35 | compatible with Django migrations. 36 | If you want to set a default for an 37 | :class:`~enumchoicefield.fields.EnumChoiceField`, 38 | the enum must be deconstructable. 39 | 40 | 41 | .. class:: ChoiceEnum 42 | 43 | a :class:`ChoiceEnum` extends both 44 | :class:`PrettyEnum` and :class:`DeconstructableEnum`. 45 | It is recommended to use a :class:`ChoiceEnum` subclass with 46 | :class:`~enumchoicefield.fields.EnumChoiceField`, 47 | but this is not required. 48 | -------------------------------------------------------------------------------- /docs/field.rst: -------------------------------------------------------------------------------- 1 | .. _field: 2 | 3 | =============== 4 | EnumChoiceField 5 | =============== 6 | 7 | .. module:: enumchoicefield.fields 8 | 9 | .. class:: EnumChoiceField(enum_class, ...) 10 | 11 | Create an EnumChoiceField. This field generates choices from an :class:`enum.Enum`. 12 | 13 | The :class:`EnumChoiceField` extends :class:`django.db.models.Field`. 14 | It accepts one additional argument: 15 | ``enum_class``, which should be a subclass of :class:`enum.Enum`. 16 | It is recommended that this enum subclasses 17 | :class:`~enumchoicefield.enum.ChoiceEnum`, 18 | but this is not required. 19 | 20 | When saving enum members to the database, The chosen member is stored 21 | in the database using its ``name`` attribute. This keeps the database 22 | representation stable when adding and removing enum members. 23 | 24 | A ``max_length`` is automatically generated from the longest ``name``. 25 | If you add a new enum member with a longer name, or remove the longest member, 26 | the generated ``max_length`` will change. 27 | To prevent this, you can manually set a ``max_length`` argument, 28 | and this will be used instead. 29 | 30 | If a default choice is supplied, 31 | the enum class must have a ``deconstruct`` method. 32 | If the enum inherits from :class:`~enumchoicefield.enum.DeconstructableEnum`, 33 | this will be handled for you. 34 | 35 | The display value for the Enums is taken from 36 | the ``str`` representation of each value. 37 | By default this is something like ``MyEnum.foo``, 38 | which is not very user friendly. 39 | :class:`~enumchoicefield.enum.PrettyEnum` makes defining 40 | a human-readable ``str`` representation easy. 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Django EnumChoiceField's documentation! 2 | ================================================== 3 | 4 | For a quick example, check out the code below: 5 | 6 | .. code-block:: python 7 | 8 | from enumchoicefield import ChoiceEnum, EnumChoiceField 9 | 10 | class Fruit(ChoiceEnum): 11 | apple = "Apple" 12 | banana = "Banana" 13 | orange = "Orange" 14 | 15 | class Profile(models.Model): 16 | name = models.CharField(max_length=100) 17 | favourite_fruit = EnumChoiceField(Fruit, default=Fruit.banana) 18 | 19 | 20 | citrus_lovers = Profile.objects.filter(favourite_fruit=Fruit.orange) 21 | 22 | 23 | Contents: 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | 28 | setup 29 | usage 30 | field 31 | enum 32 | queries 33 | admin 34 | -------------------------------------------------------------------------------- /docs/queries.rst: -------------------------------------------------------------------------------- 1 | .. _queries: 2 | 3 | =========== 4 | ORM Queries 5 | =========== 6 | 7 | You can filter and search for enum members using standard Django ORM queries. 8 | The following queries demonstrate some of what is possible: 9 | 10 | .. code-block:: python 11 | 12 | from enumchoicefield import ChoiceEnum, EnumChoiceField 13 | 14 | class Fruit(ChoiceEnum): 15 | apple = "Apple" 16 | banana = "Banana" 17 | lemon = "Lemon" 18 | lime = "Lime" 19 | orange = "Orange" 20 | 21 | class Profile(models.Model): 22 | name = models.CharField(max_length=100) 23 | favourite_fruit = EnumChoiceField(Fruit, default=Fruit.banana) 24 | 25 | 26 | apple_lovers = Profile.objects.filter(favourite_fruit=Fruit.apple) 27 | banana_haters = Profile.objects.exclude(favourite_fruit=Fruit.banana) 28 | 29 | citrus_fans = Profile.objects.filter( 30 | favourite_fruit__in=[Fruit.orange, Fruit.lemon, Fruit.lime]) 31 | 32 | Ordering 33 | ======== 34 | 35 | Ordering on a :class:`~enumchoicefield.fields.EnumChoiceField` field 36 | will order results alphabetically by the ``name``\s of the enum members, 37 | which is probably not useful. 38 | To order results by an enum value, 39 | :func:`enumchoicefield.utils.order_enum` can be used. 40 | 41 | .. module:: enumchoicefield.utils 42 | 43 | .. autofunction:: order_enum 44 | 45 | Undefined behaviour 46 | =================== 47 | 48 | Internally, the enum member is stored as a CharField 49 | using the ``name`` attribute. 50 | Any operation that CharFields support are also supported by an 51 | :class:`~enumchoicefield.fields.EnumChoiceField`. 52 | Not all of these operations make sense, 53 | such as ``contains``, ``gt``, and ``startswith``, 54 | and may not behave in a sensible manner. 55 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==2.3.1 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | .. _installing: 2 | 3 | ===== 4 | Setup 5 | ===== 6 | 7 | ``django-enumchoicefield`` is compatible with Django 2.0 and higher, 8 | and Python 3.4 and higher. 9 | 10 | You can install ``django-enumchoicefield`` using pip: 11 | 12 | .. code-block:: console 13 | 14 | $ pip install django-enumchoicefield 15 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | ===== 4 | Usage 5 | ===== 6 | 7 | The following code outlines the most simple usecase of ``EnumChoiceField``: 8 | 9 | .. code-block:: python 10 | 11 | from enumchoicefield import ChoiceEnum, EnumChoiceField 12 | 13 | class Fruit(ChoiceEnum): 14 | apple = "Apple" 15 | banana = "Banana" 16 | orange = "Orange" 17 | 18 | class Profile(models.Model): 19 | name = models.CharField(max_length=100) 20 | favourite_fruit = EnumChoiceField(Fruit, default=Fruit.banana) 21 | 22 | 23 | citrus_lovers = Profile.objects.filter(favourite_fruit=Fruit.orange) 24 | 25 | 26 | The enumerations should extend the :class:`~enumchoicefield.enum.ChoiceEnum` class. 27 | For each member in the enumeration, their human-readable name should be their value. 28 | This human-readable name will be used when presenting forms to the user. 29 | 30 | For more advanced usage, refer to the documentation on 31 | :doc:`/field`, :doc:`/enum`, or :doc:`/queries`. 32 | -------------------------------------------------------------------------------- /enumchoicefield/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from .enum import ChoiceEnum 4 | from .fields import EnumChoiceField 5 | from .version import version 6 | 7 | __all__ = ['ChoiceEnum', 'EnumChoiceField', 'version'] 8 | -------------------------------------------------------------------------------- /enumchoicefield/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.filters import ChoicesFieldListFilter 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class EnumListFilter(ChoicesFieldListFilter): 6 | """ 7 | A FieldListFilter for use in Django admin in combination with an 8 | :class:`~enumchoicefield.fields.EnumChoiceField`. Use like: 9 | 10 | .. code-block:: python 11 | 12 | class FooModelAdmin(ModelAdmin): 13 | list_filter = [ 14 | ('enum_field', EnumListFilter), 15 | ] 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | 20 | self.enum = self.field.enum 21 | self.used_parameters = { 22 | k: self.enum[v] for k, v in self.used_parameters.items()} 23 | 24 | def choices(self, cl): 25 | yield { 26 | 'selected': self.lookup_val is None, 27 | 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 28 | 'display': _('All'), 29 | } 30 | for member in self.enum: 31 | yield { 32 | 'selected': (member.name) == self.lookup_val, 33 | 'query_string': cl.get_query_string({ 34 | self.lookup_kwarg: member.name}), 35 | 'display': str(member), 36 | } 37 | -------------------------------------------------------------------------------- /enumchoicefield/enum.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import enum 4 | 5 | 6 | class DeconstructableEnum(enum.Enum): 7 | """ 8 | Enums that have a Django migration compatible ``deconstruct()`` method. 9 | """ 10 | def deconstruct(self): 11 | return ('.'.join([type(self).__module__, type(self).__name__]), 12 | (self.value,), {}) 13 | 14 | 15 | class PrettyEnum(enum.Enum): 16 | """ 17 | Enums with a nice str representation:: 18 | 19 | class MyEnum(PrettyEnum): 20 | foo = "Foo" 21 | bar = "Bar" 22 | baz = "Baz" 23 | 24 | The Enum value is automatically generated, numbering from 1 upwards, using 25 | the AutoNumber receipe from https://docs.python.org/3.4/library/enum.html 26 | """ 27 | 28 | def __new__(cls, name, *args): 29 | # Still go for auto-numbered things 30 | value = len(cls.__members__) + 1 31 | obj = object.__new__(cls) 32 | obj._value_ = value 33 | return obj 34 | 35 | def __init__(self, name): 36 | self.verbose_name = name 37 | 38 | def __str__(self): 39 | return str(self.verbose_name) 40 | 41 | 42 | class ChoiceEnum(PrettyEnum, DeconstructableEnum): 43 | """ 44 | Enums that work nicely with an EnumChoiceField 45 | """ 46 | pass 47 | -------------------------------------------------------------------------------- /enumchoicefield/fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.db.models.fields import Field 4 | 5 | from .forms import EnumField 6 | 7 | 8 | class EnumChoiceField(Field): 9 | """ 10 | A field that generates choices from an ``enum.Enum``. 11 | 12 | The ``EnumChoiceField`` extends ``Field``, and the chosen enum value is 13 | stored in the database using the Enums ``name`` attributes. This keeps the 14 | database representation stable when adding and removing enum values. 15 | 16 | A ``max_length`` is automatically generated from the Enum ``name`` 17 | attributes. If you add a new, longer Enum value, or remove the longest Enum 18 | value, the generated ``max_length`` will change. To prevent this, you can 19 | supply a ``max_length`` as normal, and this will be used instead. 20 | 21 | If a default choice is supplied, the Enum must have a ``deconstruct`` 22 | method. If your Enum inherits from ``DeconstructableEnum``, this will be 23 | handled for you. 24 | 25 | The display value for the Enums is taken from the ``str`` representation 26 | of each value. By default this is something like ``MyEnum.foo``, which is 27 | not very user friendly. ``PrettyEnum`` makes defining a better ``str`` 28 | representation easy. 29 | 30 | ``enumchoicefield.enum.ChoiceEnum`` combines both ``DeconstructableEnum`` 31 | and ``PrettyEnum`` into a class that works very nicely with an 32 | ``EnumChoiceField``. 33 | """ 34 | 35 | empty_strings_allowed = False 36 | 37 | def __init__(self, enum_class, *args, **kwargs): 38 | self.enum = enum_class 39 | kwargs.setdefault('max_length', max( 40 | len(item.name) for item in enum_class)) 41 | super(EnumChoiceField, self).__init__(*args, **kwargs) 42 | 43 | def from_db_value(self, value, expression, connection): 44 | """ 45 | Convert a string from the database into an Enum value 46 | """ 47 | if value is None: 48 | return value 49 | try: 50 | return self.enum[value] 51 | except KeyError: 52 | raise ValueError("Unknown value {value!r} of type {cls}".format( 53 | value=value, cls=type(value))) 54 | 55 | def to_python(self, value): 56 | """ 57 | Convert a string from a form into an Enum value. 58 | """ 59 | if value is None: 60 | return value 61 | if isinstance(value, self.enum): 62 | return value 63 | return self.enum[value] 64 | 65 | def get_prep_value(self, value): 66 | """ 67 | Convert an Enum value into a string for the database 68 | """ 69 | if value is None: 70 | return None 71 | if isinstance(value, self.enum): 72 | return value.name 73 | raise ValueError("Unknown value {value!r} of type {cls}".format( 74 | value=value, cls=type(value))) 75 | 76 | def deconstruct(self): 77 | name, path, args, kwargs = super(EnumChoiceField, self).deconstruct() 78 | kwargs['enum_class'] = self.enum 79 | return name, path, args, kwargs 80 | 81 | def formfield(self, **kwargs): 82 | defaults = { 83 | 'form_class': EnumField, 84 | 'enum': self.enum, 85 | } 86 | defaults.update(kwargs) 87 | out = super(EnumChoiceField, self).formfield(**defaults) 88 | return out 89 | 90 | def get_internal_type(self): 91 | return "CharField" 92 | 93 | def value_to_string(self, obj): 94 | value = self.value_from_object(obj) 95 | return '' if value is None else value.name 96 | -------------------------------------------------------------------------------- /enumchoicefield/forms.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.core.exceptions import ValidationError 3 | from django.forms.fields import Field 4 | from django.forms.utils import flatatt 5 | from django.forms.widgets import Widget 6 | from django.utils.encoding import force_str 7 | from django.utils.html import format_html, mark_safe 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | 11 | class EnumSelect(Widget): 12 | allow_multiple_selected = False 13 | 14 | def __init__(self, members=None, attrs=None): 15 | super(EnumSelect, self).__init__(attrs) 16 | self.members = members 17 | 18 | def render(self, name, value, attrs=None, renderer=None): 19 | if attrs is None: 20 | attrs = {} 21 | else: 22 | attrs = attrs.copy() 23 | 24 | attrs['name'] = name 25 | if django.VERSION >= (1, 11): 26 | final_attrs = self.build_attrs(self.attrs, attrs) 27 | else: 28 | final_attrs = self.build_attrs(attrs) 29 | output = [format_html('', flatatt(final_attrs))] 30 | options = self.render_options([value]) 31 | if options: 32 | output.append(options) 33 | output.append('') 34 | return mark_safe('\n'.join(output)) 35 | 36 | def render_options(self, selected_choices): 37 | selected_choices = set(map(force_str, selected_choices)) 38 | options = [] 39 | if not self.is_required: 40 | options.append(self.render_option(selected_choices, None)) 41 | options.extend(self.render_option(selected_choices, value) 42 | for value in self.members) 43 | return '\n'.join(options) 44 | 45 | def render_option(self, selected_choices, option): 46 | if option is None: 47 | option_value = '' 48 | option_label = '---------' 49 | else: 50 | option_value = option.name 51 | option_label = force_str(option) 52 | 53 | attrs = {'value': option_value} 54 | if option_value in selected_choices: 55 | attrs['selected'] = True 56 | if not self.allow_multiple_selected: 57 | # Only allow for a single selection. 58 | selected_choices.remove(option_value) 59 | return format_html('{}', 60 | flatatt(attrs), 61 | option_label) 62 | 63 | 64 | class EnumField(Field): 65 | widget = EnumSelect 66 | members = [] 67 | empty_value = None 68 | 69 | default_error_messages = { 70 | 'invalid_choice': _('Select a valid choice. %(value)s is not one of ' 71 | 'the available choices.'), 72 | } 73 | 74 | def __init__(self, enum, members=None, widget=None, **kwargs): 75 | self.enum = enum 76 | 77 | if members is None: 78 | members = list(enum) 79 | else: 80 | members = list(members) 81 | 82 | if widget is None: 83 | widget = self.widget 84 | if isinstance(widget, type): 85 | widget = widget(members) 86 | 87 | self.members = members 88 | 89 | super(EnumField, self).__init__(widget=widget, **kwargs) 90 | 91 | def prepare_value(self, value): 92 | if value in self.empty_values: 93 | return self.empty_value 94 | if isinstance(value, str): 95 | return value 96 | return value.name 97 | 98 | def to_python(self, value): 99 | if value == self.empty_value or value in self.empty_values: 100 | return self.empty_value 101 | 102 | try: 103 | member = self.enum[value] 104 | except KeyError: 105 | raise ValidationError( 106 | self.error_messages['invalid_choice'], 107 | code='invalid_choice', 108 | params={'value': value}, 109 | ) 110 | if member not in self.members: 111 | raise ValidationError( 112 | self.error_messages['invalid_choice'], 113 | code='invalid_choice', 114 | params={'value': value}, 115 | ) 116 | return member 117 | 118 | def _get_members(self): 119 | return list(self._members) 120 | 121 | def _set_members(self, members): 122 | self._members = list(members) 123 | self.widget.members = self._members 124 | 125 | members = property(_get_members, _set_members) 126 | -------------------------------------------------------------------------------- /enumchoicefield/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-moth/django-enumchoicefield/2d30334b27ed76eae6adbe3eb746c2b37b28666d/enumchoicefield/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /enumchoicefield/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-07-18 13:15+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | #: enumchoicefield/admin.py:28 21 | msgid "All" 22 | msgstr "Alle" 23 | 24 | #: enumchoicefield/forms.py:71 25 | #, python-format 26 | msgid "Select a valid choice. %(value)s is not one of the available choices." 27 | msgstr "Bitte treffen Sie eine valide Wahl. %(value)s ist keine der auswählbaren Möglichkeiten." 28 | -------------------------------------------------------------------------------- /enumchoicefield/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-moth/django-enumchoicefield/2d30334b27ed76eae6adbe3eb746c2b37b28666d/enumchoicefield/tests/__init__.py -------------------------------------------------------------------------------- /enumchoicefield/tests/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-moth/django-enumchoicefield/2d30334b27ed76eae6adbe3eb746c2b37b28666d/enumchoicefield/tests/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /enumchoicefield/tests/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-07-20 04:00-0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: test_field.py:125 22 | msgid "One" 23 | msgstr "Eins" 24 | 25 | #: test_field.py:126 26 | msgid "Two" 27 | msgstr "Zwei" 28 | 29 | #: test_field.py:127 30 | msgid "Three" 31 | msgstr "Drei" 32 | -------------------------------------------------------------------------------- /enumchoicefield/tests/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-moth/django-enumchoicefield/2d30334b27ed76eae6adbe3eb746c2b37b28666d/enumchoicefield/tests/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /enumchoicefield/tests/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-07-20 04:00-0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: test_field.py:125 22 | msgid "One" 23 | msgstr "" 24 | 25 | #: test_field.py:126 26 | msgid "Two" 27 | msgstr "" 28 | 29 | #: test_field.py:127 30 | msgid "Three" 31 | msgstr "" 32 | -------------------------------------------------------------------------------- /enumchoicefield/tests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-30 18:33 2 | 3 | from django.db import migrations, models 4 | 5 | import enumchoicefield.fields 6 | import enumchoicefield.tests.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ChoiceModel', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('choice', enumchoicefield.fields.EnumChoiceField(enum_class=enumchoicefield.tests.models.MyEnum, max_length=3)), 22 | ], 23 | options={ 24 | 'ordering': ('id',), 25 | 'abstract': False, 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='DefaultChoiceModel', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('choice', enumchoicefield.fields.EnumChoiceField(default=enumchoicefield.tests.models.MyEnum(3), enum_class=enumchoicefield.tests.models.MyEnum, max_length=3)), 33 | ], 34 | options={ 35 | 'ordering': ('id',), 36 | 'abstract': False, 37 | }, 38 | ), 39 | migrations.CreateModel( 40 | name='NullableChoiceModel', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('choice', enumchoicefield.fields.EnumChoiceField(blank=True, enum_class=enumchoicefield.tests.models.MyEnum, max_length=3)), 44 | ], 45 | options={ 46 | 'ordering': ('id',), 47 | 'abstract': False, 48 | }, 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /enumchoicefield/tests/migrations/0002_make_nullable.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-30 18:35 2 | 3 | from django.db import migrations 4 | import enumchoicefield.fields 5 | import enumchoicefield.tests.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('tests', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='nullablechoicemodel', 17 | name='choice', 18 | field=enumchoicefield.fields.EnumChoiceField(blank=True, enum_class=enumchoicefield.tests.models.MyEnum, max_length=3, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /enumchoicefield/tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-moth/django-enumchoicefield/2d30334b27ed76eae6adbe3eb746c2b37b28666d/enumchoicefield/tests/migrations/__init__.py -------------------------------------------------------------------------------- /enumchoicefield/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from enumchoicefield.enum import ChoiceEnum 4 | from enumchoicefield.fields import EnumChoiceField 5 | 6 | 7 | class MyEnum(ChoiceEnum): 8 | __order__ = 'foo bar baz' 9 | foo = "Foo" 10 | bar = "Bar" 11 | baz = "Baz Quux" 12 | 13 | 14 | class BaseModel(models.Model): 15 | class Meta: 16 | abstract = True 17 | ordering = ('id',) 18 | 19 | 20 | class ChoiceModel(BaseModel): 21 | choice = EnumChoiceField(MyEnum) 22 | 23 | def __str__(self): 24 | return '{} chosen'.format(self.choice) 25 | 26 | 27 | class NullableChoiceModel(BaseModel): 28 | choice = EnumChoiceField(MyEnum, null=True, blank=True) 29 | 30 | 31 | class DefaultChoiceModel(BaseModel): 32 | choice = EnumChoiceField(MyEnum, default=MyEnum.baz) 33 | -------------------------------------------------------------------------------- /enumchoicefield/tests/settings.py: -------------------------------------------------------------------------------- 1 | import dj_database_url 2 | 3 | DATABASES = { 4 | 'default': dj_database_url.config(default='sqlite://:memory:'), 5 | } 6 | 7 | INSTALLED_APPS = ['enumchoicefield', 'enumchoicefield.tests'] 8 | 9 | SECRET_KEY = 'not a secret' 10 | 11 | DEBUG = True 12 | 13 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 14 | -------------------------------------------------------------------------------- /enumchoicefield/tests/test_enum.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | 3 | from enumchoicefield.enum import DeconstructableEnum, PrettyEnum 4 | 5 | 6 | class EnumTests(SimpleTestCase): 7 | 8 | def test_pretty_enum(self): 9 | class MyEnum(PrettyEnum): 10 | foo = "Foo" 11 | bar = "Bar" 12 | baz = "Baz Quux" 13 | 14 | # Ensure names are set 15 | self.assertEqual(str(MyEnum.foo), "Foo") 16 | self.assertEqual(str(MyEnum.bar), "Bar") 17 | self.assertEqual(str(MyEnum.baz), "Baz Quux") 18 | 19 | # Ensure values are automatically generated 20 | self.assertEqual(MyEnum.foo.value, 1) 21 | self.assertEqual(MyEnum.bar.value, 2) 22 | self.assertEqual(MyEnum.baz.value, 3) 23 | 24 | self.assertEqual(MyEnum(2), MyEnum.bar) 25 | 26 | def test_extended_pretty_enum(self): 27 | class MyEnum(PrettyEnum): 28 | foo = ("Foo", 10) 29 | bar = ("Bar", 20) 30 | baz = ("Baz Quux", 40) 31 | 32 | def __init__(self, name, number): 33 | # TODO Work out if super() can be used through mad haxs 34 | PrettyEnum.__init__(self, name) 35 | self.number = number 36 | 37 | # Make sure names still work 38 | self.assertEqual(str(MyEnum.foo), "Foo") 39 | self.assertEqual(str(MyEnum.bar), "Bar") 40 | self.assertEqual(str(MyEnum.baz), "Baz Quux") 41 | 42 | # Ensure the extra data is included 43 | self.assertEqual(MyEnum.foo.number, 10) 44 | self.assertEqual(MyEnum.bar.number, 20) 45 | self.assertEqual(MyEnum.baz.number, 40) 46 | 47 | def test_deconstrubable_enum(self): 48 | class MyEnum(DeconstructableEnum): 49 | foo = 1 50 | bar = 2 51 | baz = 3 52 | 53 | # This should be a 3-tuple of: python path to module, constructor args, 54 | # and constructor kwargs. Enum(1) will get the Enum value with a value 55 | # of 1, which is close enough to a constructor with arguments... 56 | self.assertEqual(MyEnum.foo.deconstruct(), ( 57 | "enumchoicefield.tests.test_enum.MyEnum", 58 | (MyEnum.foo.value,), 59 | {})) 60 | -------------------------------------------------------------------------------- /enumchoicefield/tests/test_field.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.db import connections 4 | from django.core import serializers 5 | from django.test import TestCase 6 | 7 | from enumchoicefield.forms import EnumField 8 | 9 | from .models import ( 10 | ChoiceModel, DefaultChoiceModel, MyEnum, NullableChoiceModel) 11 | 12 | 13 | class EnumTestCase(TestCase): 14 | def test_saving(self): 15 | instance = ChoiceModel(choice=MyEnum.foo) 16 | instance.save() 17 | self.assertEqual(instance.choice, MyEnum.foo) 18 | 19 | def test_loading(self): 20 | ChoiceModel.objects.create( 21 | choice=MyEnum.bar) 22 | 23 | instance = ChoiceModel.objects.get() 24 | self.assertEqual(instance.choice, MyEnum.bar) 25 | 26 | def test_modifying(self): 27 | ChoiceModel.objects.create( 28 | choice=MyEnum.bar) 29 | instance = ChoiceModel.objects.get() 30 | self.assertEqual(instance.choice, MyEnum.bar) 31 | instance.choice = MyEnum.foo 32 | instance.save() 33 | 34 | instance = ChoiceModel.objects.get() 35 | self.assertEqual(instance.choice, MyEnum.foo) 36 | 37 | def test_null_saving(self): 38 | instance = NullableChoiceModel() 39 | instance.save() 40 | self.assertIsNone(instance.choice) 41 | 42 | def test_null_loading(self): 43 | NullableChoiceModel.objects.create() 44 | instance = NullableChoiceModel.objects.get() 45 | self.assertIsNone(instance.choice) 46 | 47 | def test_null_clearing(self): 48 | NullableChoiceModel.objects.create(choice=MyEnum.foo) 49 | instance = NullableChoiceModel.objects.get() 50 | self.assertEqual(instance.choice, MyEnum.foo) 51 | 52 | instance.choice = None 53 | instance.save() 54 | 55 | instance = NullableChoiceModel.objects.get() 56 | self.assertIsNone(instance.choice) 57 | 58 | def test_invalid_saving(self): 59 | msg = "Unknown value {value!r} of type {cls}".format(value='invalid', cls=str) 60 | with self.assertRaises(ValueError, msg=msg): 61 | ChoiceModel.objects.create(choice='invalid') 62 | 63 | def test_invalid_loading(self): 64 | # Bypass the model to save some invalid data to the database 65 | cursor = connections['default'].cursor() 66 | cursor.execute( 67 | f"INSERT INTO {ChoiceModel._meta.db_table} (choice) VALUES (%s)", 68 | ['invalid'], 69 | ) 70 | 71 | msg = "Unknown value {value!r} of type {cls}".format(value='invalid', cls=str) 72 | with self.assertRaises(ValueError, msg=msg): 73 | ChoiceModel.objects.get() 74 | 75 | def test_default(self): 76 | instance = DefaultChoiceModel() 77 | self.assertEqual(instance.choice, MyEnum.baz) 78 | 79 | def test_default_create(self): 80 | DefaultChoiceModel.objects.create() 81 | instance = DefaultChoiceModel.objects.get() 82 | self.assertEqual(instance.choice, MyEnum.baz) 83 | 84 | def test_deconstruct(self): 85 | self.assertEqual( 86 | ChoiceModel._meta.get_field('choice').deconstruct(), 87 | ('choice', 'enumchoicefield.fields.EnumChoiceField', [], { 88 | 'enum_class': MyEnum, 89 | 'max_length': 3})) 90 | 91 | def test_value_to_string(self): 92 | model_field = ChoiceModel._meta.get_field('choice') 93 | 94 | self.assertEqual( 95 | model_field.value_to_string(ChoiceModel(choice=MyEnum.bar)), 96 | 'bar') 97 | self.assertEqual( 98 | model_field.value_to_string(ChoiceModel(choice=None)), 99 | '') 100 | 101 | def test_seralize(self): 102 | pk_1 = NullableChoiceModel.objects.create(choice=MyEnum.baz).pk 103 | pk_2 = NullableChoiceModel.objects.create(choice=None).pk 104 | 105 | serialized = serializers.serialize( 106 | 'json', NullableChoiceModel.objects.all()) 107 | self.assertEqual( 108 | json.loads(serialized), 109 | [ 110 | {"model": "tests.nullablechoicemodel", "pk": pk_1, "fields": { 111 | "choice": "baz"}}, 112 | {"model": "tests.nullablechoicemodel", "pk": pk_2, "fields": { 113 | "choice": None}}, 114 | ]) 115 | 116 | def test_formfield(self): 117 | model_field = ChoiceModel._meta.get_field('choice') 118 | form_field = model_field.formfield() 119 | self.assertIsInstance(form_field, EnumField) 120 | self.assertIs(form_field.enum, model_field.enum) 121 | 122 | 123 | class TestQuery(TestCase): 124 | def setUp(self): 125 | self.foo = ChoiceModel.objects.create(choice=MyEnum.foo) 126 | self.bar = ChoiceModel.objects.create(choice=MyEnum.bar) 127 | self.baz = ChoiceModel.objects.create(choice=MyEnum.baz) 128 | 129 | def test_exact(self): 130 | self.assertEqual( 131 | self.foo, 132 | ChoiceModel.objects.get(choice=MyEnum.foo)) 133 | 134 | def test_in(self): 135 | self.assertQuerysetEqual( 136 | ChoiceModel.objects.filter(choice__in=[MyEnum.bar, MyEnum.baz]), 137 | [self.bar, self.baz], transform=lambda x: x) 138 | -------------------------------------------------------------------------------- /enumchoicefield/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import forms 3 | from django.core.exceptions import ValidationError 4 | from django.forms.utils import flatatt 5 | from django.test import SimpleTestCase 6 | from django.utils import translation 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from enumchoicefield.enum import ChoiceEnum, PrettyEnum 10 | from enumchoicefield.forms import EnumField, EnumSelect 11 | 12 | 13 | class MyEnum(PrettyEnum): 14 | foo = "Foo" 15 | bar = "Bar" 16 | baz = "Baz Quux" 17 | 18 | 19 | class TranslatedEnum(ChoiceEnum): 20 | one = _('One') 21 | two = _('Two') 22 | three = _('Three') 23 | 24 | 25 | class SelectTestCase(SimpleTestCase): 26 | def assertSelectOptions(self, html, options, required=True, name='choice'): 27 | attrs = {'id': 'id_' + name, 'name': name} 28 | if django.VERSION >= (1, 10) and required: 29 | attrs['required'] = True 30 | select = '{options}'.format( 31 | attrs=flatatt(attrs), options=''.join(options)) 32 | self.assertHTMLEqual(select, html) 33 | 34 | 35 | class TestEnumForms(SelectTestCase): 36 | 37 | class EnumForm(forms.Form): 38 | choice = EnumField(MyEnum) 39 | 40 | def test_enum_field(self): 41 | form = self.EnumForm() 42 | self.assertIsInstance(form.fields['choice'].widget, EnumSelect) 43 | 44 | def test_rendering(self): 45 | form = self.EnumForm() 46 | html = str(form['choice']) 47 | self.assertSelectOptions(html, [ 48 | '', 49 | '', 50 | '', 51 | ]) 52 | 53 | def test_initial(self): 54 | form = self.EnumForm(initial={'choice': MyEnum.bar}) 55 | html = str(form['choice']) 56 | self.assertInHTML('', html) 57 | 58 | def test_submission(self): 59 | form = self.EnumForm(data={'choice': 'baz'}) 60 | self.assertTrue(form.is_valid()) 61 | self.assertEqual(form.cleaned_data['choice'], MyEnum.baz) 62 | 63 | def test_empty_submission(self): 64 | form = self.EnumForm(data={'choice': ''}) 65 | self.assertFalse(form.is_valid()) 66 | 67 | def test_missing_submission(self): 68 | form = self.EnumForm(data={}) 69 | self.assertFalse(form.is_valid()) 70 | 71 | def test_invalid_submission(self): 72 | form = self.EnumForm(data={'choice': 'nope'}) 73 | self.assertFalse(form.is_valid()) 74 | 75 | 76 | class TestOptionalEnumForms(SelectTestCase): 77 | 78 | class EnumForm(forms.Form): 79 | choice = EnumField(MyEnum, required=False) 80 | 81 | def test_enum_field(self): 82 | form = self.EnumForm() 83 | self.assertIsInstance(form.fields['choice'].widget, EnumSelect) 84 | 85 | def test_rendering(self): 86 | form = self.EnumForm() 87 | html = str(form['choice']) 88 | self.assertSelectOptions(html, [ 89 | '', 90 | '', 91 | '', 92 | '', 93 | ], required=False) 94 | 95 | def test_initial(self): 96 | form = self.EnumForm(initial={'choice': MyEnum.bar}) 97 | html = str(form['choice']) 98 | self.assertInHTML('', html) 99 | 100 | def test_submission(self): 101 | form = self.EnumForm(data={'choice': 'baz'}) 102 | self.assertTrue(form.is_valid()) 103 | self.assertEqual(form.cleaned_data['choice'], MyEnum.baz) 104 | 105 | def test_empty_submission(self): 106 | form = self.EnumForm(data={'choice': ''}) 107 | self.assertTrue(form.is_valid()) 108 | self.assertEqual(form.cleaned_data['choice'], None) 109 | 110 | def test_missing_submission(self): 111 | form = self.EnumForm(data={}) 112 | self.assertTrue(form.is_valid()) 113 | self.assertEqual(form.cleaned_data['choice'], None) 114 | 115 | 116 | class TestComplicatedForm(SelectTestCase): 117 | 118 | class EnumForm(forms.Form): 119 | choice = EnumField(MyEnum) 120 | number = forms.IntegerField() 121 | 122 | def test_valid_form(self): 123 | form = self.EnumForm(data={'choice': 'foo', 'number': '10'}) 124 | self.assertTrue(form.is_valid()) 125 | self.assertEqual(form.cleaned_data, { 126 | 'choice': MyEnum.foo, 'number': 10}) 127 | 128 | def test_invalid_number(self): 129 | form = self.EnumForm(data={'choice': 'bar', 'number': 'abc'}) 130 | self.assertFalse(form.is_valid()) 131 | html = str(form['choice']) 132 | self.assertSelectOptions(html, [ 133 | '', 134 | '', 135 | '', 136 | ]) 137 | 138 | def test_invalid_choice(self): 139 | form = self.EnumForm(data={'choice': 'nope', 'number': '10'}) 140 | self.assertFalse(form.is_valid()) 141 | html = str(form['choice']) 142 | self.assertSelectOptions(html, [ 143 | '', 144 | '', 145 | '', 146 | ]) 147 | 148 | 149 | class TestLimitedMembers(SelectTestCase): 150 | members = [MyEnum.baz, MyEnum.foo] 151 | 152 | def setUp(self): 153 | 154 | class EnumForm(forms.Form): 155 | choice = EnumField(MyEnum, members=self.members) 156 | 157 | self.EnumForm = EnumForm 158 | 159 | def test_field(self): 160 | field = EnumField(MyEnum) 161 | self.assertEqual(field.members, list(MyEnum)) 162 | 163 | def test_limited_members(self): 164 | form = self.EnumForm() 165 | self.assertEqual(form['choice'].field.members, self.members) 166 | html = str(form['choice']) 167 | self.assertSelectOptions(html, [ 168 | '', 169 | '', 170 | ]) 171 | 172 | def test_invalid_choice(self): 173 | form = self.EnumForm({'choice': 'bar'}) 174 | self.assertFalse(form.is_valid()) 175 | 176 | def test_valid_choice(self): 177 | form = self.EnumForm({'choice': 'baz'}) 178 | self.assertTrue(form.is_valid()) 179 | html = str(form['choice']) 180 | self.assertSelectOptions(html, [ 181 | '', 182 | '', 183 | ]) 184 | 185 | 186 | class TestEnumField(SelectTestCase): 187 | # The EnumField instance to test with. It is missing MyEnum.bar 188 | field = EnumField(MyEnum, members=[MyEnum.baz, MyEnum.foo]) 189 | 190 | def test_prepare_value(self): 191 | self.assertEqual(self.field.prepare_value(None), None) 192 | self.assertEqual(self.field.prepare_value(''), None) 193 | self.assertEqual(self.field.prepare_value(MyEnum.baz), 'baz') 194 | 195 | def test_to_python(self): 196 | self.assertEqual(self.field.to_python(None), None) 197 | self.assertEqual(self.field.to_python(''), None) 198 | self.assertEqual(self.field.to_python('baz'), MyEnum.baz) 199 | 200 | def test_to_python_invalid(self): 201 | with self.assertRaises(ValidationError): 202 | self.field.to_python('nope') 203 | 204 | def test_to_python_non_member(self): 205 | with self.assertRaises(ValidationError): 206 | self.field.to_python('bar') 207 | 208 | 209 | class TestTranslatedChoiceEnum(SelectTestCase): 210 | 211 | class EnumForm(forms.Form): 212 | choice = EnumField(TranslatedEnum) 213 | 214 | def test_english(self): 215 | with translation.override('en'): 216 | form = self.EnumForm() 217 | html = str(form['choice']) 218 | self.assertSelectOptions(html, [ 219 | '', 220 | '', 221 | '', 222 | ]) 223 | 224 | def test_german(self): 225 | with translation.override('de'): 226 | form = self.EnumForm() 227 | html = str(form['choice']) 228 | self.assertSelectOptions(html, [ 229 | '', 230 | '', 231 | '', 232 | ]) 233 | -------------------------------------------------------------------------------- /enumchoicefield/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Case, IntegerField, When 2 | from django.test import TestCase 3 | 4 | from enumchoicefield.utils import order_enum 5 | 6 | from .models import ChoiceModel, MyEnum 7 | 8 | 9 | class OrderEnumTestCase(TestCase): 10 | def test_case_when_enum(self): 11 | """Test sending an Enum class to order_enum""" 12 | order = order_enum('choice', MyEnum) 13 | expected = Case( 14 | When(choice=MyEnum.foo, then=0), 15 | When(choice=MyEnum.bar, then=1), 16 | When(choice=MyEnum.baz, then=2), 17 | default=3, 18 | output_field=IntegerField()) 19 | self.assertEqual(str(order), str(expected)) 20 | 21 | def test_case_when_list(self): 22 | """Test sending an incomplete list to order_enum""" 23 | order = order_enum('choice', [MyEnum.baz, MyEnum.bar]) 24 | expected = Case( 25 | When(choice=MyEnum.baz, then=0), 26 | When(choice=MyEnum.bar, then=1), 27 | default=2, 28 | output_field=IntegerField()) 29 | self.assertEqual(str(order), str(expected)) 30 | 31 | def test_ordering_enum(self): 32 | c_bar = ChoiceModel(pk=1, choice=MyEnum.bar) 33 | c_baz = ChoiceModel(pk=2, choice=MyEnum.baz) 34 | c_foo = ChoiceModel(pk=3, choice=MyEnum.foo) 35 | ChoiceModel.objects.bulk_create([c_bar, c_baz, c_foo]) 36 | 37 | qs = ChoiceModel.objects\ 38 | .annotate(choice_order=order_enum('choice', MyEnum))\ 39 | .order_by('choice_order') 40 | self.assertEqual(list(qs), [c_foo, c_bar, c_baz]) 41 | 42 | def test_ordering_list(self): 43 | """Test ordering by an incomplete list of members.""" 44 | c_bar = ChoiceModel(pk=1, choice=MyEnum.bar) 45 | c_baz = ChoiceModel(pk=2, choice=MyEnum.baz) 46 | c_foo = ChoiceModel(pk=3, choice=MyEnum.foo) 47 | ChoiceModel.objects.bulk_create([c_bar, c_baz, c_foo]) 48 | 49 | # MyEnum.baz is not in the list, so should be sorted to the end 50 | desired_order = [MyEnum.bar, MyEnum.foo] 51 | qs = ChoiceModel.objects\ 52 | .annotate(choice_order=order_enum('choice', desired_order))\ 53 | .order_by('choice_order') 54 | self.assertEqual(list(qs), [c_bar, c_foo, c_baz]) 55 | -------------------------------------------------------------------------------- /enumchoicefield/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.db.models import Case, IntegerField, When 4 | 5 | 6 | def order_enum(field, members): 7 | """ 8 | Make an annotation value that can be used to sort by an enum field. 9 | 10 | ``field`` 11 | The name of an EnumChoiceField. 12 | 13 | ``members`` 14 | An iterable of Enum members in the order to sort by. 15 | 16 | Use like: 17 | 18 | .. code-block:: python 19 | 20 | desired_order = [MyEnum.bar, MyEnum.baz, MyEnum.foo] 21 | ChoiceModel.objects\\ 22 | .annotate(my_order=order_enum('choice', desired_order))\\ 23 | .order_by('my_order') 24 | 25 | As Enums are iterable, ``members`` can be the Enum itself 26 | if the default ordering is desired: 27 | 28 | .. code-block:: python 29 | 30 | ChoiceModel.objects\\ 31 | .annotate(my_order=order_enum('choice', MyEnum))\\ 32 | .order_by('my_order') 33 | 34 | Any enum members not present in the list of members 35 | will be sorted to the end of the results. 36 | 37 | """ 38 | members = list(members) 39 | 40 | return Case( 41 | *(When(**{field: member, 'then': i}) 42 | for i, member in enumerate(members)), 43 | default=len(members), 44 | output_field=IntegerField()) 45 | -------------------------------------------------------------------------------- /enumchoicefield/version.py: -------------------------------------------------------------------------------- 1 | version = '3.0.0' 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-test.txt 2 | tox==2.0.1 3 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | dj-database-url~=0.4.1 2 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | 7 | def run(): 8 | import django 9 | from django.conf import settings 10 | from django.core.management import execute_from_command_line 11 | 12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'enumchoicefield.tests.settings') 13 | execute_from_command_line([sys.argv[0], 'test'] + sys.argv[1:]) 14 | 15 | 16 | if __name__ == '__main__': 17 | run() 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.rst 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | 7 | [flake8] 8 | exclude = migrations 9 | 10 | [isort] 11 | line_length = 79 12 | multi_line_output = 4 13 | skip = migrations 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Install django-enumchoicefield using setuptools 4 | """ 5 | from setuptools import find_packages, setup 6 | 7 | with open('README.rst', 'r') as f: 8 | readme = f.read() 9 | 10 | with open('enumchoicefield/version.py') as v: 11 | version = None 12 | exec(v.read()) # Get version 13 | 14 | 15 | setup( 16 | name='django-enumchoicefield', 17 | version=version, 18 | description='A choice field for Django using native Python Enums', 19 | long_description=readme, 20 | author='Tim Heap', 21 | author_email='tim@timheap.me', 22 | url='https://github.com/timheap/django-enumchoicefield', 23 | 24 | install_requires=['Django>=2.0'], 25 | zip_safe=False, 26 | license='BSD License', 27 | 28 | packages=find_packages(), 29 | 30 | include_package_data=True, 31 | package_data={}, 32 | 33 | classifiers=[ 34 | 'Environment :: Web Environment', 35 | 'Intended Audience :: Developers', 36 | 'Operating System :: OS Independent', 37 | 'Programming Language :: Python', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Programming Language :: Python :: 3.7', 42 | 'Programming Language :: Python :: 3.8', 43 | 'Framework :: Django', 44 | 'License :: OSI Approved :: BSD License', 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skip_missing_interpreters = True 3 | 4 | envlist = 5 | py37-dj{2x,3x} 6 | py38-dj{2x,3x,4x} 7 | py39-dj{2x,3x,4x} 8 | py310-dj{3x,4x} 9 | flake8,isort,docs 10 | 11 | [testenv] 12 | commands = python runtests.py {posargs} 13 | usedevelop = True 14 | pip_pre = True 15 | setenv = PYTHONDONTWRITEBYTECODE=1 16 | passenv = DJANGO_SETTINGS_MODULE DATABASE_URL 17 | 18 | deps = 19 | -rrequirements-test.txt 20 | dj2x: Django~=2.2.17 21 | dj3x: Django~=3.2.9 22 | dj4x: Django~=4.0.0 23 | postgres: psycopg2 24 | mysql: mysqlclient 25 | 26 | [testenv:flake8] 27 | basepython = python3 28 | deps = flake8 29 | commands = flake8 enumchoicefield/ 30 | 31 | [testenv:isort] 32 | basepython = python3 33 | deps = isort 34 | commands = isort --check-only --diff enumchoicefield/ 35 | 36 | [testenv:docs] 37 | basepython = python3 38 | deps = -r{toxinidir}/docs/requirements.txt 39 | changedir = docs 40 | whitelist_externals = make 41 | commands = make SPHINXOPTS=-nW clean html 42 | --------------------------------------------------------------------------------