mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
Add option-groups capability to Choices.
This commit is contained in:
parent
32f839b577
commit
2e44f1c2c0
4 changed files with 135 additions and 29 deletions
|
|
@ -4,6 +4,8 @@ CHANGES
|
|||
master (unreleased)
|
||||
-------------------
|
||||
|
||||
* `Choices` now accepts option-groupings. Fixes GH-14.
|
||||
|
||||
* `Choices` can now be added to other `Choices` or to any iterable, and can be
|
||||
compared for equality with itself. Thanks Tony Aldridge. (Merge of GH-76.)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ Choices
|
|||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices('draft', 'published')
|
||||
# ...
|
||||
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
|
||||
|
||||
A ``Choices`` object is initialized with any number of choices. In the
|
||||
|
|
@ -34,7 +33,6 @@ representation. In this case you can provide choices as two-tuples:
|
|||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices(('draft', _('draft')), ('published', _('published')))
|
||||
# ...
|
||||
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
|
||||
|
||||
But what if your database representation of choices is constrained in
|
||||
|
|
@ -52,9 +50,20 @@ the third is the human-readable version:
|
|||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices((0, 'draft', _('draft')), (1, 'published', _('published')))
|
||||
# ...
|
||||
status = models.IntegerField(choices=STATUS, default=STATUS.draft)
|
||||
|
||||
Option groups can also be used with ``Choices``; in that case each
|
||||
argument is a tuple consisting of the option group name and a list of
|
||||
options, where each option in the list is either a string, a two-tuple,
|
||||
or a triple as outlined above. For example::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices(('Visible', ['new', 'archived']), ('Invisible', ['draft', 'deleted']))
|
||||
|
||||
Choices can be concatenated with the ``+`` operator, both to other Choices
|
||||
instances and other iterable objects that could be converted into Choices:
|
||||
|
||||
|
|
@ -66,7 +75,6 @@ instances and other iterable objects that could be converted into Choices:
|
|||
|
||||
class Article(models.Model):
|
||||
STATUS = GENERIC_CHOICES + [(2, 'featured', _('featured'))]
|
||||
# ...
|
||||
status = models.IntegerField(choices=STATUS, default=STATUS.draft)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,66 +34,111 @@ class Choices(object):
|
|||
identifier, the database representation itself is available as an
|
||||
attribute on the ``Choices`` object, returning itself.)
|
||||
|
||||
Option groups can also be used with ``Choices``; in that case each
|
||||
argument is a tuple consisting of the option group name and a list
|
||||
of options, where each option in the list is either a string, a
|
||||
two-tuple, or a triple as outlined above.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *choices):
|
||||
self._full = []
|
||||
self._choices = []
|
||||
self._choice_dict = {}
|
||||
for choice in self.equalize(choices):
|
||||
self._full.append(choice)
|
||||
self._choices.append((choice[0], choice[2]))
|
||||
self._choice_dict[choice[1]] = choice[0]
|
||||
# list of choices expanded to triples - can include optgroups
|
||||
self._triples = []
|
||||
# list of choices as (db, human-readable) - can include optgroups
|
||||
self._doubles = []
|
||||
# dictionary mapping Python identifier to db representation
|
||||
self._mapping = {}
|
||||
# set of db representations
|
||||
self._db_values = set()
|
||||
|
||||
self._process(choices)
|
||||
|
||||
|
||||
def _store(self, triple, triple_collector, double_collector):
|
||||
self._mapping[triple[1]] = triple[0]
|
||||
self._db_values.add(triple[0])
|
||||
triple_collector.append(triple)
|
||||
double_collector.append((triple[0], triple[2]))
|
||||
|
||||
|
||||
def _process(self, choices, triple_collector=None, double_collector=None):
|
||||
if triple_collector is None:
|
||||
triple_collector = self._triples
|
||||
if double_collector is None:
|
||||
double_collector = self._doubles
|
||||
|
||||
store = lambda c: self._store(c, triple_collector, double_collector)
|
||||
|
||||
def equalize(self, choices):
|
||||
for choice in choices:
|
||||
if isinstance(choice, (list, tuple)):
|
||||
if len(choice) == 3:
|
||||
yield choice
|
||||
store(choice)
|
||||
elif len(choice) == 2:
|
||||
yield (choice[0], choice[0], choice[1])
|
||||
if isinstance(choice[1], (list, tuple)):
|
||||
# option group
|
||||
group_name = choice[0]
|
||||
subchoices = choice[1]
|
||||
tc = []
|
||||
triple_collector.append((group_name, tc))
|
||||
dc = []
|
||||
double_collector.append((group_name, dc))
|
||||
self._process(subchoices, tc, dc)
|
||||
else:
|
||||
store((choice[0], choice[0], choice[1]))
|
||||
else:
|
||||
raise ValueError("Choices can't handle a list/tuple of length %s, only 2 or 3"
|
||||
% len(choice))
|
||||
raise ValueError(
|
||||
"Choices can't take a list of length %s, only 2 or 3"
|
||||
% len(choice)
|
||||
)
|
||||
else:
|
||||
yield (choice, choice, choice)
|
||||
store((choice, choice, choice))
|
||||
|
||||
|
||||
def __len__(self):
|
||||
return len(self._choices)
|
||||
return len(self._doubles)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._choices)
|
||||
return iter(self._doubles)
|
||||
|
||||
|
||||
def __getattr__(self, attname):
|
||||
try:
|
||||
return self._choice_dict[attname]
|
||||
return self._mapping[attname]
|
||||
except KeyError:
|
||||
raise AttributeError(attname)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._choices[index]
|
||||
return self._doubles[index]
|
||||
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
other = other._full
|
||||
other = other._triples
|
||||
else:
|
||||
other = list(other)
|
||||
return Choices(*(self._full + other))
|
||||
return Choices(*(self._triples + other))
|
||||
|
||||
|
||||
def __radd__(self, other):
|
||||
# radd is never called for matching types, so we don't check here
|
||||
other = list(other)
|
||||
return Choices(*(other + self._full))
|
||||
return Choices(*(other + self._triples))
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self._full == other._full
|
||||
return self._triples == other._triples
|
||||
return False
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (self.__class__.__name__,
|
||||
', '.join(("%s" % repr(i) for i in self._full)))
|
||||
return '%s(%s)' % (
|
||||
self.__class__.__name__,
|
||||
', '.join(("%s" % repr(i) for i in self._triples))
|
||||
)
|
||||
|
||||
|
||||
def __contains__(self, item):
|
||||
if item in self._choice_dict.values():
|
||||
return True
|
||||
return item in self._db_values
|
||||
|
|
|
|||
|
|
@ -222,10 +222,12 @@ class ChoicesTests(TestCase):
|
|||
with self.assertRaises(ValueError):
|
||||
Choices(('a',))
|
||||
|
||||
|
||||
def test_contains_value(self):
|
||||
self.assertTrue('PUBLISHED' in self.STATUS)
|
||||
self.assertTrue('DRAFT' in self.STATUS)
|
||||
|
||||
|
||||
def test_doesnt_contain_value(self):
|
||||
self.assertFalse('UNPUBLISHED' in self.STATUS)
|
||||
|
||||
|
|
@ -245,6 +247,17 @@ class ChoicesTests(TestCase):
|
|||
self.assertEqual(('DRAFT',) + Choices('PUBLISHED'), self.STATUS)
|
||||
|
||||
|
||||
def test_option_groups(self):
|
||||
c = Choices(('group a', ['one', 'two']), ['group b', ('three',)])
|
||||
self.assertEqual(
|
||||
list(c),
|
||||
[
|
||||
('group a', [('one', 'one'), ('two', 'two')]),
|
||||
('group b', [('three', 'three')]),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class LabelChoicesTests(ChoicesTests):
|
||||
def setUp(self):
|
||||
self.STATUS = Choices(
|
||||
|
|
@ -302,6 +315,7 @@ class LabelChoicesTests(ChoicesTests):
|
|||
('DELETED', 'DELETED', 'DELETED'),
|
||||
)))
|
||||
|
||||
|
||||
def test_contains_value(self):
|
||||
self.assertTrue('PUBLISHED' in self.STATUS)
|
||||
self.assertTrue('DRAFT' in self.STATUS)
|
||||
|
|
@ -309,9 +323,11 @@ class LabelChoicesTests(ChoicesTests):
|
|||
# and the internal representation are both DELETED.
|
||||
self.assertTrue('DELETED' in self.STATUS)
|
||||
|
||||
|
||||
def test_doesnt_contain_value(self):
|
||||
self.assertFalse('UNPUBLISHED' in self.STATUS)
|
||||
|
||||
|
||||
def test_doesnt_contain_display_value(self):
|
||||
self.assertFalse('is draft' in self.STATUS)
|
||||
|
||||
|
|
@ -333,6 +349,21 @@ class LabelChoicesTests(ChoicesTests):
|
|||
)
|
||||
|
||||
|
||||
def test_option_groups(self):
|
||||
c = Choices(
|
||||
('group a', [(1, 'one'), (2, 'two')]),
|
||||
['group b', ((3, 'three'),)]
|
||||
)
|
||||
self.assertEqual(
|
||||
list(c),
|
||||
[
|
||||
('group a', [(1, 'one'), (2, 'two')]),
|
||||
('group b', [(3, 'three')]),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
||||
class IdentifierChoicesTests(ChoicesTests):
|
||||
def setUp(self):
|
||||
self.STATUS = Choices(
|
||||
|
|
@ -367,20 +398,25 @@ class IdentifierChoicesTests(ChoicesTests):
|
|||
(2, 'DELETED', 'is deleted'),
|
||||
)))
|
||||
|
||||
|
||||
def test_contains_value(self):
|
||||
self.assertTrue(0 in self.STATUS)
|
||||
self.assertTrue(1 in self.STATUS)
|
||||
self.assertTrue(2 in self.STATUS)
|
||||
|
||||
|
||||
def test_doesnt_contain_value(self):
|
||||
self.assertFalse(3 in self.STATUS)
|
||||
|
||||
|
||||
def test_doesnt_contain_display_value(self):
|
||||
self.assertFalse('is draft' in self.STATUS)
|
||||
|
||||
|
||||
def test_doesnt_contain_python_attr(self):
|
||||
self.assertFalse('PUBLISHED' in self.STATUS)
|
||||
|
||||
|
||||
def test_equality(self):
|
||||
self.assertEqual(self.STATUS, Choices(
|
||||
(0, 'DRAFT', 'is draft'),
|
||||
|
|
@ -388,6 +424,7 @@ class IdentifierChoicesTests(ChoicesTests):
|
|||
(2, 'DELETED', 'is deleted')
|
||||
))
|
||||
|
||||
|
||||
def test_inequality(self):
|
||||
self.assertNotEqual(self.STATUS, [
|
||||
(0, 'DRAFT', 'is draft'),
|
||||
|
|
@ -429,6 +466,20 @@ class IdentifierChoicesTests(ChoicesTests):
|
|||
)
|
||||
|
||||
|
||||
def test_option_groups(self):
|
||||
c = Choices(
|
||||
('group a', [(1, 'ONE', 'one'), (2, 'TWO', 'two')]),
|
||||
['group b', ((3, 'THREE', 'three'),)]
|
||||
)
|
||||
self.assertEqual(
|
||||
list(c),
|
||||
[
|
||||
('group a', [(1, 'one'), (2, 'two')]),
|
||||
('group b', [(3, 'three')]),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class InheritanceManagerTests(TestCase):
|
||||
def setUp(self):
|
||||
self.child1 = InheritanceManagerTestChild1.objects.create()
|
||||
|
|
|
|||
Loading…
Reference in a new issue