Skip to main content

dj.choicee

https://badgen.net/pypi/v/dj.choicee https://travis-ci.org/AgriConnect/dj.choicee.png

This is a much clearer way to specify choices for fields in models and forms. A basic example:

>>> from dj.choicee import Choices
>>> class Gender(Choices):
...   _ = Choices.Choice
...
...   male = _("Male")
...   female = _("Female")
...
>>> Gender()
[(1, 'Male'), (2, 'Female')]
>>> Gender.male
<Choice: male (id: 1)>
>>> Gender.female
<Choice: female (id: 2)>
>>> Gender.male.id
1
>>> Gender.male.desc
'Male'
>>> Gender.male.raw
'Male'
>>> Gender.male.name
'male'
>>> Gender.from_name("male")
<Choice: male (id: 1)>
>>> Gender.id_from_name("male")
1
>>> Gender.raw_from_name("male")
'Male'
>>> Gender.desc_from_name("male")
'Male'
>>> Gender.name_from_id(2)
'female'
>>> Gender.name_from_id(3)
Traceback (most recent call last):
...
ValueError: Nothing found for '3'.
>>> Gender.from_name("perez")
Traceback (most recent call last):
...
ValueError: Nothing found for 'perez'.

You define a class of choices, specifying each choice as a class attribute. Those attributes are int subclasses, numbered automatically starting with 1. The class provides several features which support the DRY principle:

  • An object instantiated from the choices class is basically a list of (id, localized_description) pairs straight for consumption by Django.

  • Each attribute defined can be retrieved directly from the class.

  • Metadata (e.g. attribute name, raw and localized description, numeric ID) of each attribute is accessible.

  • Choices which are suffixed by _ to avoid clashing with Python keywords have this suffix automatically removed in their .name attributes

  • Lookup functions are available to help getting attributes or their metadata.

Grouping choices

One of the worst problems with choices is their weak extensibility. For instance, an application defines a group of possible choices like this:

>>> class License(Choices):
...   _ = Choices.Choice
...
...   gpl = _("GPL")
...   bsd = _("BSD")
...   proprietary = _("Proprietary")
...
>>> License()
[(1, 'GPL'), (2, 'BSD'), (3, 'Proprietary')]

All is well until the application goes live and after a while the developer wants to include LGPL. The natural choice would be to add it after gpl but when we do that, the indexing would break. On the other hand, adding the new entry at the end of the definition looks ugly and makes the resulting combo boxes in the UI sorted in a counter-intuitive way. Grouping lets us solve this problem by explicitly defining the structure within a class of choices:

>>> class License(Choices):
...   _ = Choices.Choice
...
...   COPYLEFT = Choices.Group(0)
...   gpl = _("GPL")
...
...   PUBLIC_DOMAIN = Choices.Group(100)
...   bsd = _("BSD")
...
...   OSS = Choices.Group(200)
...   apache2 = _("Apache 2")
...
...   COMMERCIAL = Choices.Group(300)
...   proprietary = _("Proprietary")
...
>>> License()
[(1, 'GPL'), (101, 'BSD'), (201, 'Apache 2'), (301, 'Proprietary')]

This enables the developer to include more licenses of each group later on:

>>> class License(Choices):
...   _ = Choices.Choice
...
...   COPYLEFT = Choices.Group(0)
...   gpl_any = _("GPL, any")
...   gpl2 = _("GPL 2")
...   gpl3 = _("GPL 3")
...   lgpl = _("LGPL")
...   agpl = _("Affero GPL")
...
...   PUBLIC_DOMAIN = Choices.Group(100)
...   bsd = _("BSD")
...   public_domain = _("Public domain")
...
...   OSS = Choices.Group(200)
...   apache2 = _("Apache 2")
...   mozilla = _("MPL")
...
...   COMMERCIAL = Choices.Group(300)
...   proprietary = _("Proprietary")
...
>>> License()
[(1, 'GPL, any'), (2, 'GPL 2'), (3, 'GPL 3'), (4, 'LGPL'),
 (5, 'Affero GPL'), (101, 'BSD'), (102, 'Public domain'),
 (201, 'Apache 2'), (202, 'MPL'), (301, 'Proprietary')]

Note the behaviour:

  • the developer renamed the GPL choice but its meaning and ID remained stable

  • BSD, Apache and proprietary choices have their IDs unchanged

  • the resulting class is self-descriptive, readable and extensible

As a bonus, the explicitly specified groups can be used when needed:

>>> License.COPYLEFT
<ChoiceGroup: COPYLEFT (id: 0)>
>>> License.gpl2 in License.COPYLEFT.choices
True
>>> [(c.id, c.desc) for c in License.COPYLEFT.choices]
[(1, 'GPL, any'), (2, 'GPL 2'), (3, 'GPL 3'), (4, 'LGPL'),
 (5, 'Affero GPL')]

ChoiceField

Choices can be used with generic IntegerField and CharField instances. When you do that though, some minor API deficiencies show up fairly quickly. First, when you define the field, you have to instanciate the choices class and the default value has to be converted to the proper type explicitly:

color = models.IntegerField(choices=Color(), default=Color.green.id)

Second, when getting the attribute back from a model, it has to be converted to a Choice instance to do anything interesting with it:

>>> obj = Model.objects.get(pk=3)
>>> obj.color
3
>>> Color.from_id(obj.color)
<Choice: Blue (id: 3, name: blue)>

To overcome those problems a ChoiceField is available in the dj.choicee.fields package. It is based on integers on the database level but the API exposes Choice instances. This helps both on the definition side:

color = ChoiceField(choices=Color, default=Color.green)

and on the access side:

>>> obj = Model.objects.get(pk=3)
>>> obj.color
<Choice: Blue (id: 3, name: blue)>
>>> obj.color = Color.green
>>> obj.save()
>>> Model.objects.get(pk=3).color
<Choice: Green (id: 2, name: green)>

For rendering forms, the field coerces to integer values. That also means that wherever Choice instances are accepted, integers are also fine.

Advanced functionality

Filtering

The developer can specify all possible choices for future use and then filter out only the currently applicable values on choices creation:

>>> class Language(Choices):
...   _ = Choices.Choice
...
...   de = _("German")
...   en = _("English")
...   fr = _("French")
...   pl = _("Polish")
...
>>> Language()
[(1, 'German'), (2, 'English'), (3, 'French'), (4, 'Polish')]
>>> Language(filter=("en", "pl"))
[(2, 'English'), (4, 'Polish')]

This has the great advantage of keeping the IDs and sorting intact.

Custom item format

One can also change how the pairs are constructed by providing a factory function. For instance, to use the class of choices defined above for the LANGUAGES setting in settings.py, one could specify:

>>> Language(item=lambda choice: (choice.name, choice.raw))
[('de', 'German'), ('en', 'English'), ('fr', 'French'),
 ('pl', 'Polish')]

Extra attributes on choices

Each choice can receive extra arguments using the extra() method:

>>> class Python(Choices):
...   _ = Choices.Choice
...
...   cpython = _("CPython").extra(language='C')
...   pypy = _("PyPy").extra(language='Python')
...   jython = _("Jython").extra(language='Java')
...   iron_python = _("IronPython").extra(language='C#')

This adds a language attribute to each choice so you can get it back like this:

>>> Python.jython.language
'Java'

This enables polymorphic attribute access later on when using models or forms. For instance, suppose you have a simple model like:

>>> class Library(models.Model):
...   name = models.CharField(max_length=100)
...   python_kind = models.IntegerField(choices=Python(), default=Python.cpython.id)

In that case to get the implementation language back yo’d do:

>>> library = Library.objects.get(name='dj.choicee')
>>> Python.from_id(library.python_kind).language
'C'

That frees your user code of any conditionals or dictionaries that depend on the state of the choices class. If you would add another choice to it, no user code needs to be changed to support it. This also supports the DRY principle because the choices class becomes the single place where configuration of that kind is held.

Extra attributes on choice groups

Unsurprisingly, choice groups can have extra attributes as well. They are then inherited by choices in such a group and can be overriden if necessary. For instance:

>>> class ProfileChange(Choices):
...   _ = Choices.Choice
...
...   USER = Choices.Group(0).extra(icon='bookkeeping.png', is_public=True)
...   email = _("e-mail").extra(is_public=False)
...   first_name = _("first name")
...   last_name = _("last name")
...
...   BASIC_INFO = Choices.Group(10).extra(icon='bookkeeping.png', is_public=True)
...   birth_date = _("birth date").extra(icon='calendar.png')
...   gender = _("gender").extra(icon='male_female.png')
...   country = _("country")
...   city = _("city")
...
...   CONTACT_INFO = Choices.Group(20).extra(icon='contactbook.png', is_public=False)
...   skype = _("Skype ID")
...   icq = _("ICQ number")
...   msn = _("MSN login")
...   xfire = _("X-Fire login")
...   irc = _("IRC info").extra(is_public=True)

In that case proper inheritance takes place:

>>> ProfileChange.first_name.is_public
True
>>> ProfileChange.email.is_public
False
>>> ProfileChange.country.icon
'bookkeeping.png'
>>> ProfileChange.birth_date.icon
'calendar.png'

Predefined choices

There are several classes of choices which are very common in web applications so they are provided already: Country, Gender and Language.

How do I run the tests?

Install tox then run:

$ tox -e py

Change Log

1.0.0

  • Pure Python 3 (drop Python 2 compatibility).

  • Support only Python3.5+ and Django 2.0+

  • Fork from dj.choices to new project.

Authors

Metadata

Release files for dj.choicee 0.9.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dj.choicee 0.9.2
File Size Uploaded
dj.choicee-0.9.2.tar.gz 19.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dj.choicee 0.9.2
File Interpreter ABI Platform
dj.choicee-0.9.2-py3-none-any.whl Python 3 none any Details

Total release size: 36.8 kB

Release files / dj.choicee-0.9.2.tar.gz

Download URL dj.choicee-0.9.2.tar.gz
Size 19.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4af6572c6607c26501259fb98ec76e1ac2aa39ed1ad87359b076f1d62bd36a71
BLAKE2b-256 checksum
How to use checksums
1e5b7e45674eb38ad21b4f6d38e7650b38af878bbc6c62b339cb94bbd973a7d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.0.10 CPython/3.8.2 Linux/5.4.0-42-generic

Release files / dj.choicee-0.9.2-py3-none-any.whl

Download URL dj.choicee-0.9.2-py3-none-any.whl
Size 17.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2f1f5cb23e5839ea359e2814bb3c3d97d8476a28ff81c24a79390e4be4b348c3
BLAKE2b-256 checksum
How to use checksums
c8e716d329b017d693cf2e89022702c1f827c1c9aa2b757c66581d131fd56eb1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.0.10 CPython/3.8.2 Linux/5.4.0-42-generic

Release history Release notifications | RSS feed

This release

0.9.2 This release

2 release files

0.9.1

2 release files

0.9.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page