diff --git a/AUTHORS.rst b/AUTHORS.rst index 1c56aa2..609d55e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,4 +1,5 @@ | ad-m +| Adam Barnes | Alejandro Varas | Alex Orange | Alexey Evseev diff --git a/CHANGES.rst b/CHANGES.rst index 24a97ff..abd1a8e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ CHANGES ======= +3.3.0 (2019.08.19) +------------------ +- Added `Choices.subset`. + 3.2.0 (2019.06.21) ------------------- - Catch `AttributeError` for deferred abstract fields, fixes GH-331. @@ -413,4 +417,3 @@ CHANGES ----- * Added ``QueryManager`` - diff --git a/docs/utilities.rst b/docs/utilities.rst index b763ba0..3cbbdc7 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -84,6 +84,27 @@ instances and other iterable objects that could be converted into Choices: STATUS = GENERIC_CHOICES + [(2, 'featured', _('featured'))] status = models.IntegerField(choices=STATUS, default=STATUS.draft) +Should you wish to provide a subset of choices for a field, for +instance, you have a form class to set some model instance to a failed +state, and only wish to show the user the failed outcomes from which to +select, you can use the ``subset`` method: + +.. code-block:: python + + from model_utils import Choices + + OUTCOMES = Choices( + (0, 'success', _('Successful')), + (1, 'user_cancelled', _('Cancelled by the user')), + (2, 'admin_cancelled', _('Cancelled by an admin')), + ) + FAILED_OUTCOMES = OUTCOMES.subset('user_cancelled', 'admin_cancelled') + +The ``choices`` attribute on the model field can then be set to +``FAILED_OUTCOMES``, thus allowing the subset to be defined in close +proximity to the definition of all the choices, and reused elsewhere as +required. + Field Tracker ============= diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 2fa87c0..9a87de5 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices # noqa:F401 from .tracker import FieldTracker, ModelTracker # noqa:F401 -__version__ = '3.2.0' +__version__ = '3.3.0' diff --git a/model_utils/choices.py b/model_utils/choices.py index 6339503..31d5aa1 100644 --- a/model_utils/choices.py +++ b/model_utils/choices.py @@ -142,3 +142,17 @@ class Choices(object): def __deepcopy__(self, memo): return self.__class__(*copy.deepcopy(self._triples, memo)) + + def subset(self, *new_identifiers): + identifiers = set(self._identifier_map.keys()) + + if not identifiers.issuperset(new_identifiers): + raise ValueError( + 'The following identifiers are not present: %s' % + identifiers.symmetric_difference(new_identifiers), + ) + + return self.__class__(*[ + choice for choice in self._triples + if choice[1] in new_identifiers + ]) diff --git a/tests/test_choices.py b/tests/test_choices.py index 986670c..cb5bec9 100644 --- a/tests/test_choices.py +++ b/tests/test_choices.py @@ -279,3 +279,30 @@ class IdentifierChoicesTests(ChoicesTests): ('group b', [(3, 'three')]), ], ) + + +class SubsetChoicesTest(TestCase): + + def setUp(self): + self.choices = Choices( + (0, 'a', 'A'), + (1, 'b', 'B'), + ) + + def test_nonexistent_identifiers_raise(self): + with self.assertRaises(ValueError): + self.choices.subset('a', 'c') + + def test_solo_nonexistent_identifiers_raise(self): + with self.assertRaises(ValueError): + self.choices.subset('c') + + def test_empty_subset_passes(self): + subset = self.choices.subset() + + self.assertEqual(subset, Choices()) + + def test_subset_returns_correct_subset(self): + subset = self.choices.subset('a') + + self.assertEqual(subset, Choices((0, 'a', 'A')))