django-extended-choices 1.3.3
Little helper application to improve django choices (for fields)
License: BSD
Requires Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Keywords: redis,orm,jobs,queue

|PyPI Version| |Build Status| |Doc Status|

django-extended-choices
=======================

A little application to improve Django choices
----------------------------------------------

``django-extended-choices`` aims to provide a better and more readable
way of using choices_ in Django_.

Installation
------------

You can install directly via pip (since version ``0.3``)::

    $ pip install django-extended-choices

Or from the Github_ repository (``master`` branch by default)::

    $ git clone git://github.com/twidi/django-extended-choices.git
    $ cd django-extended-choices
    $ sudo python setup.py install

Usage
-----

The aim is to replace this:

.. code-block:: python

    STATE_ONLINE  = 1
    STATE_DRAFT   = 2
    STATE_OFFLINE = 3

    STATE_CHOICES = (
        (STATE_ONLINE,  'Online'),
        (STATE_DRAFT,   'Draft'),
        (STATE_OFFLINE, 'Offline'),
    )

    STATE_DICT = dict(STATE_CHOICES)

    class Content(models.Model):
        title      = models.CharField(max_length=255)
        content    = models.TextField()
        state      = models.PositiveSmallIntegerField(choices=STATE_CHOICES, default=STATE_DRAFT)

        def __unicode__(self):
            return u'Content "%s" (state=%s)' % (self.title, STATE_DICT[self.state])

    print(Content.objects.filter(state=STATE_ONLINE))

by this:

.. code-block:: python

    from extended_choices import Choices

    STATES = Choices(
        ('ONLINE',  1, 'Online'),
        ('DRAFT',   2, 'Draft'),
        ('OFFLINE', 3, 'Offline'),
    )

    class Content(models.Model):
        title      = models.CharField(max_length=255)
        content    = models.TextField()
        state      = models.PositiveSmallIntegerField(choices=STATES, default=STATES.DRAFT)

        def __unicode__(self):
            return u'Content "%s" (state=%s)' % (self.title, STATES.for_value(self.state).display)

    print(Content.objects.filter(state=STATES.ONLINE))


As you can see there is only one declaration for all states with, for each state, in order:

* the pseudo-constant name which can be used (``STATES.ONLINE`` replaces the previous ``STATE_ONLINE``)
* the value to use in the database - which could equally be a string
* the name to be displayed - and you can wrap the text in ``ugettext_lazy()`` if you need i18n

And then, you can use:

* ``STATES``, or ``STATES.choices``, to use with ``choices=`` in fields declarations
* ``STATES.for_constant(constant)``, to get the choice entry from the constant name
* ``STATES.for_value(constant)``, to get the choice entry from the key used in database
* ``STATES.for_display(constant)``, to get the choice entry from the displayable value (can be useful in some case)

Each choice entry obtained by ``for_constant``, ``for_value`` and ``for_display`` return a tuple as
given to the ``Choices`` constructor, but with additional attributes:

.. code-block:: python

    >>> entry = STATES.for_constant('ONLINE')
    >>> entry == ('ONLINE', 1, 'Online')
    True
    >>> entry.constant
    'ONLINE'
    >>> entry.value
    1
    >>> entry.display
    'Online'

These attributes are chainable (with a weird example to see chainability):

.. code-block:: python

    >>> entry.constant.value
    1
    >>> entry.constant.value.value.display.constant.display
    'Online'

To allow this, we had to remove support for ``None`` values. Use empty strings instead.

Note that constants can be accessed via a dict key (``STATES['ONLINE']`` for example) if
you want to fight your IDE that may warn you about undefined attributes.


You can check whether a value is in a ``Choices`` object directly:

.. code-block:: python

    >>> 1 in STATES
    True
    >>> 42 in STATES
    False


You can even iterate on a ``Choices`` objects to get choices as seen by Django:

.. code-block:: python

    >>> for choice in STATES:
    ...     print(choice)
    (1, 'Online')
    (2, 'Draft')
    (3, 'Offline')

To get all choice entries as given to the ``Choices`` object, you can use the ``entries``
attribute:

.. code-block:: python

    >>> for choice_entry in STATES.entries:
    ...     print(choice_entry)
    ('ONLINE',  1, 'Online'),
    ('DRAFT',   2, 'Draft'),
    ('OFFLINE', 3, 'Offline'),

Or the following dicts, using constants, values or display names, as keys, and the matching
choice entry as values:

* ``STATES.constants``
* ``STATES.values``
* ``STATES.displays``


.. code-block:: python

    >>> STATES.constants['ONLINE'] is STATES.for_constant('ONLINE')
    True
    >>> STATES.values[2] is STATES.for_value(2)
    True
    >>> STATES.displays['Offline'] is STATES.for_display('Offline')
    True


If you want these dicts to be ordered, you can pass the dict class to use to the
``Choices`` constructor:

.. code-block:: python

    from collections import OrderedDict
    STATES = Choices(
        ('ONLINE',  1, 'Online'),
        ('DRAFT',   2, 'Draft'),
        ('OFFLINE', 3, 'Offline'),
        dict_class = OrderedDict
    )

Since version ``1.1``, the new ``OrderedChoices`` class is provided, that is exactly that:
a ``Choices`` using ``OrderedDict`` by default for ``dict_class``. You can directly import
it from ``extended_choices``.

You can check if a constant, value, or display name exists:

.. code-block:: python

    >>> STATES.has_constant('ONLINE')
    True
    >>> STATES.has_value(1)
    True
    >>> STATES.has_display('Online')
    True

You can create subsets of choices within the same ``Choices`` instance:

.. code-block:: python

    >>> STATES.add_subset('NOT_ONLINE', ('DRAFT', 'OFFLINE',))
    >>> STATES.NOT_ONLINE
    (2, 'Draft')
    (3, 'Offline')

Now, ``STATES.NOT_ONLINE`` is a real ``Choices`` instance, with a subset of the main ``STATES``
constants.

You can use it to generate choices for when you only want a subset of choices available:

.. code-block:: python

    offline_state = models.PositiveSmallIntegerField(
        choices=STATES.NOT_ONLINE,
        default=STATES.DRAFT
    )

As the subset is a real ``Choices`` instance, you have the same attributes and methods:

.. code-block:: python

    >>> STATES.NOT_ONLINE.for_constant('OFFLINE').value
    3
    >>> STATES.NOT_ONLINE.for_value(1).constant
    Traceback (most recent call last):
    ...
    KeyError: 3
    >>> list(STATES.NOT_ONLINE.constants.keys())
    ['DRAFT', 'OFFLINE']
    >>> STATES.NOT_ONLINE.has_display('Online')
    False

You can create as many subsets as you want, reusing the same constants if needed:

.. code-block:: python

    STATES.add_subset('NOT_OFFLINE', ('ONLINE', 'DRAFT'))

If you want to check membership in a subset you could do:

.. code-block:: python

    def is_online(self):
        # it's an example, we could have just tested with STATES.ONLINE
        return self.state not in STATES.NOT_ONLINE


If you want to filter a queryset on values from a subset, you can use ``values``, but as ``values`` is a dict, ``keys()`` must be user:

.. code-block:: python

    Content.objects.filter(state__in=STATES.NOT_ONLINE.values.keys())

You can add choice entries in many steps using ``add_choices``, possibly creating subsets at
the same time.

To construct the same ``Choices`` as before, we could have done:

.. code-block:: python

    STATES = Choices()
    STATES.add_choices(
        ('ONLINE', 1, 'Online')
    )
    STATES.add_choices(
        ('DRAFT',   2, 'Draft'),
        ('OFFLINE', 3, 'Offline'),
        name='NOT_ONLINE'
    )

You can also pass the ``argument`` to the ``Choices`` constructor to create a subset with all
the choices entries added at the same time (it will call ``add_choices`` with the name and the
entries)

The list of existing subset names is in the ``subsets`` attributes of the parent ``Choices``
object.

If you want a subset of the choices but not save it in the original ``Choices`` object, you can
use ``extract_subset`` instead of ``add_subset``

.. code-block:: python

    >>> subset = STATES.extract_subset('DRAFT', 'OFFLINE')
    >>> subset
    (2, 'Draft')
    (3, 'Offline')


As for a subset created by ``add_subset``, you have a real ``Choices`` object, but not accessible
from the original ``Choices`` object.

Note that in ``extract_subset``, you pass the strings directly, not in a list/tuple as for the
second argument of ``add_subset``.

Additional attributes
---------------------

Each tuple must contain three elements. But you can pass a dict as a fourth one and each entry of this dict will be saved as an attribute
of the choice entry

.. code-block:: python

    >>> PLANETS = Choices(
    ...     ('EARTH', 'earth', 'Earth', {'color': 'blue'}),
    ...     ('MARS', 'mars', 'Mars', {'color': 'red'}),
    ... )
    >>> PLANETS.EARTH.color
    'blue'


Auto display/value
------------------

We provide two classes to eases the writing of your choices, attended you don't need translation on the display value.

AutoChoices
'''''''''''

It's the simpler and faster version: you just past constants and:

- the value saved in database will be constant lower cased
- the display value will be the constant with ``_`` replaced by spaces, and the first letter capitalized

.. code-block:: python

    >>> from extended_choices import AutoChoices
    >>> PLANETS = AutoChoices('EARTH', 'MARS')
    >>> PLANETS.EARTH.value
    'earth'
    >>> PLANETS.MARS.display
    'Mars'

If you want to pass additional attributes, pass a tuple with the dict as a last element:


.. code-block:: python

    >>> PLANETS = AutoChoices(
    ...     ('EARTH', {'color': 'blue'}),
    ...     ('MARS', {'color': 'red'}),
    ... )
    >>> PLANETS.EARTH.value
    'earth'
    >>> PLANETS.EARTH.color
    'blue'


You can change the transform function used to convert the constant to the value to be saved and the display value, by passing
``value_transform`` and ``display_transform`` functions to the constructor.

.. code-block:: python

    >>> PLANETS = AutoChoices(
    ...     'EARTH', 'MARS',
    ...     value_transform=lambda const: 'planet_' + const.lower().
    ...     display_transform=lambda const: 'Planet: ' + const.lower().
    ... )
    >>> PLANETS.EARTH.value
    'planet_earth'
    >>> PLANETS.MARS.display
    'Planet: mars'


If you find yourself repeting these transform functions you can have a base class that defines these function, as class attributes:

.. code-block:: python

    >>> class MyAutoChoices(AutoChoices):
    ...     value_transform=staticmethod(lambda const: const.upper())
    ...     display_transform=staticmethod(lambda const: const.lower())

    >>> PLANETS = MyAutoChoices('EARTH', 'MARS')
    >>> PLANETS.EARTH.value
    'EARTH'
    >>> PLANETS.MARS.dispay
    'mars'

Of course you can still override the functions by passing them to the constructor.

If you want, for an entry, force a specific value, you can do it by simply passing it as a second argument:

.. code-block:: python

    >>> PLANETS = AutoChoices(
    ...     'EARTH',
    ...     ('MARS', 'red-planet'),
    ... )
    >>> PLANETS.MARS.value
    'red-planet'

And then if you want to set the display, pass a third one:

.. code-block:: python

    >>> PLANETS = AutoChoices(
    ...     'EARTH',
    ...     ('MARS', 'red-planet', 'Red planet'),
    ... )
    >>> PLANETS.MARS.value
    'red-planet'
    >>> PLANETS.MARS.display
    'Red planet'


To force a display value but let the db value to be automatically computed, use ``None`` for the second argument:

.. code-block:: python

    >>> PLANETS = AutoChoices(
    ...     'EARTH',
    ...     ('MARS', None, 'Red planet'),
    ... )
    >>> PLANETS.MARS.value
    'mars'
    >>> PLANETS.MARS.display
    'Red planet'


AutoDisplayChoices
''''''''''''''''''

In this version, you have to define the value to save in database. The display value will be composed like in ``AutoChoices``

.. code-block:: python

    >>> from extended_choices import AutoDisplayChoices
    >>> PLANETS = AutoDisplayChoices(
    ...     ('EARTH', 1),
    ...     ('MARS', 2),
    ... )
    >>> PLANETS.EARTH.value
    1
    >>> PLANETS.MARS.display
    'Mars'

If you want to pass additional attributes, pass a tuple with the dict as a last element:


.. code-block:: python

    >>> PLANETS = AutoDisplayChoices(
    ...     ('EARTH', 'earth', {'color': 'blue'}),
    ...     ('MARS', 'mars', {'color': 'red'}),
    ... )
    >>> PLANETS.EARTH.value
    1
    >>> PLANETS.EARTH.display
    'Earth'
    >>> PLANETS.EARTH.color
    'blue'


As in ``AutoChoices``, you can change the transform function for the value to display by passing ``display_transform`` to the
constructor.

If you want, for an entry, force a specific display, you can do it by simply passing it as a third argument:

.. code-block:: python

    >>> PLANETS = AutoChoices(
    ...     ('EARTH', 1),
    ...     ('MARS', 2, 'Red planet'),
    ... )
    >>> PLANETS.MARS.display
    'Red planet'

Notes
-----

* You also have a very basic field (``NamedExtendedChoiceFormField```) in ``extended_choices.fields`` which accept constant names instead of values
* Feel free to read the source to learn more about this little Django app.
* You can declare your choices where you want. My usage is in the ``models.py`` file, just before the class declaration.

Compatibility
-------------

The version ``1.0`` provided a totally new API, and compatibility with the previous one
(``0.4.1``) was removed in ``1.1``. The last version with the compatibility was ``1.0.7``.

If you need this compatibility, you can use a specific version by pinning it in your requirements.

License
-------

Available under the BSD_ License. See the ``LICENSE`` file included

Python/Django versions support
------------------------------


+----------------+-------------------------------------------------+
| Django version | Python versions                                 |
+----------------+-------------------------------------------------+
| 1.8, 1.9, 1.10 | 2.7, 3.4, 3.5                                   |
+----------------+-------------------------------------------------+
| 1.11           | 2.7, 3.4, 3.5, 3.6                              |
+----------------+-------------------------------------------------+
| 2.0            | 3.4, 3.5, 3.6, 3.7                              |
+----------------+-------------------------------------------------+
| 2.1, 2.2       | 3.5, 3.6, 3.7                                   |
+----------------+-------------------------------------------------+


Tests
-----

To run tests from the code source, create a virtualenv or activate one, install Django, then::

    python -m extended_choices.tests


We also provides some quick doctests in the code documentation. To execute them::

    python -m extended_choices


Note: the doctests will work only in python version not display `u` prefix for strings.


Source code
-----------

The source code is available on Github_.


Developing
----------

If you want to participate in the development of this library, you'll need ``Django``
installed in your virtualenv. If you don't have it, simply run::

    pip install -r requirements-dev.txt

Don't forget to run the tests ;)

Feel free to propose a pull request on Github_!

A few minutes after your pull request, tests will be executed on TravisCi_ for all the versions
of python and Django we support.


Documentation
-------------

You can find the documentation on ReadTheDoc_

To update the documentation, you'll need some tools::

    pip install -r requirements-makedoc.txt

Then go to the ``docs`` directory, and run::

    make html

Author
------
Written by Stephane "Twidi" Angel <s.angel@twidi.com> (http://twidi.com), originally for http://www.liberation.fr

.. _choices: http://docs.djangoproject.com/en/1.5/ref/models/fields/#choices
.. _Django: http://www.djangoproject.com/
.. _Github: https://github.com/twidi/django-extended-choices
.. _TravisCi: https://travis-ci.org/twidi/django-extended-choices/pull_requests
.. _ReadTheDoc: http://django-extended-choices.readthedocs.org
.. _BSD: http://opensource.org/licenses/BSD-3-Clause

.. |PyPI Version| image:: https://img.shields.io/pypi/v/django-extended-choices.png
   :target: https://pypi.python.org/pypi/django-extended-choices
   :alt: PyPI Version
.. |Build Status| image:: https://travis-ci.org/twidi/django-extended-choices.png
   :target: https://travis-ci.org/twidi/django-extended-choices
   :alt: Build Status on Travis CI
.. |Doc Status| image:: https://readthedocs.org/projects/django-extended-choices/badge/?version=latest
   :target: http://django-extended-choices.readthedocs.org
   :alt: Documentation Status on ReadTheDoc

.. image:: https://d2weczhvl823v0.cloudfront.net/twidi/django-extended-choices/trend.png
   :alt: Bitdeli badge
   :target: https://bitdeli.com/free