diff --git a/.travis.yml b/.travis.yml index 9580a23..f901aa6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,29 +2,27 @@ language: python cache: pip sudo: false -python: +python: - "2.7" - "3.4" - "3.5" - "3.6" env: - - DJANGO=1.8 - - DJANGO=1.9 - - DJANGO=1.10 - DJANGO=1.11 - DJANGO=2.0 - DJANGO=2.1 + - DJANGO=2.2 matrix: fast_finish: true exclude: - { python: "2.7", env: DJANGO=2.0 } - { python: "2.7", env: DJANGO=2.1 } + - { python: "2.7", env: DJANGO=2.2 } + - { python: "3.4", env: DJANGO=2.1 } - - { python: "3.6", env: DJANGO=1.8 } - - { python: "3.6", env: DJANGO=1.9 } - - { python: "3.6", env: DJANGO=1.10 } + - { python: "3.4", env: DJANGO=2.2 } install: - pip install coverage coveralls tox tox-travis diff --git a/djchoices/choices.py b/djchoices/choices.py index 6175062..5a374cf 100644 --- a/djchoices/choices.py +++ b/djchoices/choices.py @@ -4,6 +4,7 @@ from collections import OrderedDict from django.core.exceptions import ValidationError +from django.db.models import Case, IntegerField, Value, When from django.utils import six from django.utils.deconstruct import deconstructible @@ -195,3 +196,30 @@ def get_choice(cls, value): """ attribute_for_value = cls.attributes[value] return cls._fields[attribute_for_value] + + @classmethod + def get_order_expression(cls, field_name): + """ + Build the Case/When to annotate objects with the choice item order + + Useful if choices represent some access-control mechanism, for example. + + Usage:: + + >>> order = MyChoices.get_order_expression('some_field') + >>> queryset = Model.objects.annotate(some_field_order=order) + >>> for item in queryset: + ... print(item.some_field) + ... print(item.some_field_order) + # first_choice + # 1 + # second_choice + # 2 + """ + whens = [] + for choice_item in cls._fields.values(): + whens.append(When(**{ + field_name: choice_item.value, + "then": Value(choice_item.order) + })) + return Case(*whens, output_field=IntegerField()) diff --git a/djchoices/tests/test_choices.py b/djchoices/tests/test_choices.py index 9d1b9e9..12cedbc 100644 --- a/djchoices/tests/test_choices.py +++ b/djchoices/tests/test_choices.py @@ -1,5 +1,7 @@ import unittest +from django.db.models import Case, IntegerField, Value, When + from djchoices import C, ChoiceItem, DjangoChoices @@ -264,3 +266,14 @@ def test_iteration(self): def test_choices_len(self): self.assertEqual(len(StringTestClass), 4) + + def test_order_annotation(self): + case = OrderedChoices.get_order_expression('dummy') + + expected = Case( + When(dummy='b', then=Value(0)), + When(dummy='a', then=Value(1)), + output_field=IntegerField() + ) + + self.assertEqual(repr(case), repr(expected)) diff --git a/docs/choices.rst b/docs/choices.rst index b7224ee..71ab199 100644 --- a/docs/choices.rst +++ b/docs/choices.rst @@ -230,3 +230,31 @@ Returns the actual ``ChoiceItem`` instance for a given value: This allows you to inspect any ``ChoiceItem`` attributes. + +get_order_expression +++++++++++++++++++++ + +Build the ``Case``/``When`` statement to use in queryset annotations. + +Choices defined get an implicit or explicit order, which can have semantic +value. This ORM expression allows you to map choice values to the order value, +as an integer, on the database level. + +It is then available for subsequent filtering, allowing more work to be done +in the database instead of in Python. + +.. code-block:: python + + >>> class MyChoices(DjangoChoices): + ... first = ChoiceItem('first') + ... second = ChoiceItem('second') + + >>> order = MyChoices.get_order_expression('some_field') + >>> queryset = Model.objects.annotate(some_field_order=order) + >>> for item in queryset: + ... print(item.some_field) + ... print(item.some_field_order) + # first_ + # 1 + # second + # 2 diff --git a/runtests.py b/runtests.py index 52522a9..2fca4f5 100644 --- a/runtests.py +++ b/runtests.py @@ -1,15 +1,15 @@ -try: - import unittest2 as unittest -except ImportError: - import unittest - +import unittest from os import path from sys import stdout +from django.conf import settings + def get_suite(): disc_folder = path.abspath(path.dirname(__file__)) + settings.configure(SECRET_KEY='dummy') + stdout.write("Discovering tests in '%s'..." % disc_folder) suite = unittest.TestSuite() loader = unittest.loader.defaultTestLoader diff --git a/setup.py b/setup.py index 408a015..ae6b444 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ license="MIT", description="Sanity for the django choices functionality.", long_description=readme, - install_requires=['Django>=1.8'], + install_requires=['Django>=1.11'], test_suite='runtests.get_suite', url="https://github.com/bigjason/django-choices", author="Jason Webb", @@ -24,14 +24,11 @@ "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Framework :: Django", - "Framework :: Django :: 1.8", - "Framework :: Django :: 1.9", - "Framework :: Django :: 1.10", "Framework :: Django :: 1.11", "Framework :: Django :: 2.0", "Framework :: Django :: 2.1", + "Framework :: Django :: 2.2", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", diff --git a/tox.ini b/tox.ini index ca8ae0d..c2c7a6d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,30 +1,25 @@ [tox] envlist = - py33-django18, - py{27,34,35,py}-django{18,19,110,111}, - py34-django20, - py{35,36,37}-django{20,21} + py{27,34,35,36,37,py}-django111, + py{34,35,36,37}-django20, + py{35,36,37}-django{21,22}, docs, isort skip_missing_interpreters = true [travis:env] -DJANGO = - 1.8: django18 - 1.9: django19 - 1.10: django110 +DJANGO = 1.11: django111 2.0: django20 2.1: django21 + 2.2: django22 [testenv] deps= - django18: Django>=1.8,<1.9 - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 + django22: Django>=2.2,<3.0 coverage coveralls commands=coverage run --rcfile={toxinidir}/.coveragerc {toxinidir}/setup.py test