forked from bigjason/django-choices
/
choices.py
161 lines (116 loc) · 4.31 KB
/
choices.py
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from __future__ import absolute_import, unicode_literals
import re
from collections import OrderedDict
from django.core.exceptions import ValidationError
from django.utils import six
from django.utils.deconstruct import deconstructible
__all__ = ["ChoiceItem", "DjangoChoices", "C"]
# Support Functionality (Not part of public API)
class Labels(dict):
def __getattr__(self, name):
result = dict.get(self, name, None)
if result is not None:
return result
else:
raise AttributeError("Label for field %s was not found." % name)
def __setattr__(self, name, value):
self[name] = value
class StaticProp(object):
def __init__(self, value):
self.value = value
def __get__(self, obj, objtype):
return self.value
class Attributes(object):
def __init__(self, attrs, fields):
self.attrs = attrs
self.fields = fields
def __get__(self, obj, objtype):
if len(self.attrs) != len(self.fields):
raise ValueError(
'Not all values are unique, it\'s not possible to map all '
'values to the right attribute'
)
return self.attrs
# End Support Functionality
sentinel = object()
class ChoiceItem(object):
"""
Describes a choice item.
The label is usually the field name so label can normally be left blank.
Set a label if you need characters that are illegal in a python identifier
name (ie: "DVD/Movie").
"""
order = 0
def __init__(self, value=sentinel, label=None, order=None):
self.value = value
self.label = label
if order:
self.order = order
else:
ChoiceItem.order += 1
self.order = ChoiceItem.order
# Shorter convenience alias.
C = ChoiceItem # noqa
class DjangoChoicesMeta(type):
"""
Metaclass that writes the choices class.
"""
name_clean = re.compile(r"_+")
def __new__(cls, name, bases, attrs):
fields = {}
labels = Labels()
values = OrderedDict()
attributes = OrderedDict()
choices = []
# Get all the fields from parent classes.
parents = [b for b in bases if isinstance(b, DjangoChoicesMeta)]
for kls in parents:
for field_name in kls._fields:
fields[field_name] = kls._fields[field_name]
# Get all the fields from this class.
for field_name in attrs:
val = attrs[field_name]
if isinstance(val, ChoiceItem):
fields[field_name] = val
fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order))
for field_name in fields:
val = fields[field_name]
if isinstance(val, ChoiceItem):
if val.label is not None:
label = val.label
else:
# TODO: mark translatable by default?
label = cls.name_clean.sub(" ", field_name)
val0 = label if val.value is sentinel else val.value
choices.append((val0, label))
attrs[field_name] = StaticProp(val0)
setattr(labels, field_name, label)
values[val0] = label
attributes[val0] = field_name
else:
choices.append((field_name, val.choices))
attrs["choices"] = StaticProp(tuple(choices))
attrs["labels"] = labels
attrs["values"] = values
attrs["_fields"] = fields
attrs["validator"] = ChoicesValidator(values)
attrs["attributes"] = Attributes(attributes, fields)
return super(DjangoChoicesMeta, cls).__new__(cls, name, bases, attrs)
@deconstructible
class ChoicesValidator(object):
def __init__(self, values):
self.values = values
def __call__(self, value):
if value not in self.values:
raise ValidationError('Select a valid choice. %s is not '
'one of the available choices.' % value)
def __eq__(self, other):
return isinstance(other, ChoicesValidator) and self.values == other.values
def __ne__(self, other):
return not (self == other)
class DjangoChoices(six.with_metaclass(DjangoChoicesMeta)):
order = 0
choices = ()
labels = Labels()
values = {}
validator = None