Login

Model Choices Helper

Author:
pmclanahan
Posted:
January 26, 2010
Language:
Python
Version:
1.1
Score:
0 (after 0 ratings)

This is my attempt at a convenience class for Django model choices which will use a [Small]IntegerField for storage. It's very similar to jacobian's version, but I wanted to be able to use simple attributes for access to the integer values.

It's not technically dependent on Django, but it's probably not a datatype that would be useful for much else. Feel free to do so however if you have a use-case.

>>> statuses = Choices(
...     ('live', 'Live'),
...     ('draft', 'Draft'),
...     ('hidden', 'Not Live'),
... )
>>> statuses.live
0
>>> statuses.hidden
2
>>> statuses.get_choices()
((0, 'Live'), (1, 'Draft'), (2, 'Not Live'))

This is then useful for use in a model field with the choices attribute.

>>> from django.db import models
>>> class Entry(models.Model):
...     STATUSES = Choices(
...         ('live', 'Live'),
...         ('draft', 'Draft'),
...         ('hidden', 'Not Live'),
...     )
...     status = models.SmallIntegerField(choices=STATUSES.get_choices(),
...                                       default=STATUSES.live)

It's also useful later when you need to filter by your choices.

>>> live_entries = Entry.objects.filter(status=Entries.STATUSES.live)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python

class Choices(object):
    """
    Convenience class for Django model choices which will use a [Small]IntegerField
    for storage.
    
        >>> statuses = Choices(
        ...     ('live', 'Live'),
        ...     ('draft', 'Draft'),
        ...     ('hidden', 'Not Live'),
        ... )
        >>> statuses.live
        0
        >>> statuses.hidden
        2
        >>> statuses.get_choices()
        ((0, 'Live'), (1, 'Draft'), (2, 'Not Live'))
    
    This is then useful for use in a model field with the choices attribute.
    
        >>> from django.db import models
        >>> class Entry(models.Model):
        ...     STATUSES = Choices(
        ...         ('live', 'Live'),
        ...         ('draft', 'Draft'),
        ...         ('hidden', 'Not Live'),
        ...     )
        ...     status = models.SmallIntegerField(choices=STATUSES.get_choices(),
        ...                                       default=STATUSES.live)
        
    It's also useful later when you need to filter by your choices.
    
        >>> live_entries = Entry.objects.filter(status=Entries.STATUSES.live)
                                              
    """
    
    def __init__(self, *args):
        super(Choices, self).__init__()
        self.__dict__['_keys'], self.__dict__['_values'] = zip(*args)
    
    def __getattr__(self, name):
        # have to iterate manually to avoid conversion to a list for < 2.6 compat
        for i, k in enumerate(self._keys):
            if k == name:
                return i
        raise AttributeError("No attribute %r." % name)
    
    def __setattr__(self, name, value):
        raise AttributeError("%r object does not support attribute assignment" % self.__class__.__name__)
    
    def __delattr__(self, name):
        raise AttributeError("%r object does not support attribute deletion" % self.__class__.__name__)
    
    def __getitem__(self, name):
        try:
            return self._values[getattr(self, name)]
        except AttributeError:
            raise KeyError(name)
    
    def __contains__(self, name):
        return name in self._keys
    
    def __repr__(self):
        return repr(self.items())
    
    def items(self):
        return tuple(zip(self._keys, self._values))
    
    def keys(self):
        # no need to copy since _keys is a tuple
        return self._keys
    
    def values(self):
        # no need to copy since _values is a tuple
        return self._values
    
    def get_choices(self):
        return tuple(enumerate(self._values))

# tests

if __name__ == '__main__':
    import unittest
    
    class ChoicesTests(unittest.TestCase):
        
        def setUp(self):
            self.c = Choices(
                ('stuff', 'Stuff'),
                ('whatnot', 'What-not'),
                ('things', 'Awesome Things'),
            ) 
        
        def test_choice_attributes(self):
            self.assertEqual(self.c.stuff, 0)
            self.assertEqual(self.c.whatnot, 1)
            self.assertEqual(self.c.things, 2)
            
        def test_choice_values(self):
            self.assertEqual(self.c['stuff'], 'Stuff')
            self.assertEqual(self.c['whatnot'], 'What-not')
            self.assertEqual(self.c['things'], 'Awesome Things')
        
        def test_model_choices(self):
            correct_choices = (
                (0, 'Stuff'),
                (1, 'What-not'),
                (2, 'Awesome Things'),
            )
            self.assertEqual(self.c.get_choices(), correct_choices)
        
        def test_missing_attrs(self):
            self.assertRaises(AttributeError, lambda: self.c.missing)
            self.assertRaises(KeyError, lambda: self.c['missing'])
        
        def _del_attr(self):
            del self.c.stuff
        
        def _del_item(self):
            del self.c['stuff']
        
        def _set_attr(self):
            self.c.stuff = 'other stuff'
        
        def _set_item(self):
            self.c['stuff'] = 'other stuff'
        
        def test_immutable(self):
            self.assertRaises(AttributeError, self._del_attr)
            self.assertRaises(AttributeError, self._set_attr)
            self.assertRaises(TypeError, self._del_item)
            self.assertRaises(TypeError, self._set_item)
            
    unittest.main()

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

pmclanahan (on January 27, 2010):

I updated the implementation to avoid the dependency on Django and to make the resulting instances immutable.

#

Piaume (on February 15, 2010):

It should be nice to support grouped choices. Thus we'd do something like that:

>>> CHOICES = Choices(
...    ('some_group', 'Some group', (
...            ('foo', 'Foo choice'),
...            ('bar', 'Bar choice')
...        )
...    ),
...    ('another_group', 'Another group', (
...            ('baz', 'Baz choice'),
...        )
...    )
... )


>>> CHOICES.get_choices()
(('group_1', ((0, 'foo'), (1, 'bar'))), ('group_2', ((2, 'baz'),)))

>>> CHOICES.baz # When the attribute is a choice, return its ID
2

>>> # Example usage: SomeModel.objects.filter(field__in=CHOICES.some_group)
>>> CHOICES.some_group # When the attribute is a group, return a tuple of its choices'IDs
(0, 1)
>>> # Example usage:
>>> SomeModel.objects.filter(field__in=CHOICES.some_group)

>>> CHOICES.get_group_display(CHOICES.baz)
'Groupe 2'

#

Piaume (on February 15, 2010):

Sorry I've made a mistake above:

CHOICES.get_group_display(CHOICES.baz) 'Another group'

Instead of:

CHOICES.get_group_display(CHOICES.baz) 'Groupe 2'

#

Please login first before commenting.