Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/djchoices/choices.py: 77%
118 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1from __future__ import absolute_import, unicode_literals
3import re
4from collections import OrderedDict
6from django.core.exceptions import ValidationError
7from django.db.models import Case, IntegerField, Value, When
8from django.utils.deconstruct import deconstructible
10import six
12__all__ = ["ChoiceItem", "DjangoChoices", "C"]
15# Support Functionality (Not part of public API)
18class Labels(dict):
19 def __getattr__(self, name):
20 result = dict.get(self, name, None)
21 if result is not None:
22 return result
23 else:
24 raise AttributeError("Label for field %s was not found." % name)
26 def __setattr__(self, name, value):
27 self[name] = value
30class StaticProp(object):
31 def __init__(self, value):
32 self.value = value
34 def __get__(self, obj, objtype):
35 return self.value
38class Attributes(object):
39 def __init__(self, attrs, fields):
40 self.attrs = attrs
41 self.fields = fields
43 def __get__(self, obj, objtype):
44 if len(self.attrs) != len(self.fields): 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true
45 raise ValueError(
46 "Not all values are unique, it's not possible to map all "
47 "values to the right attribute"
48 )
49 return self.attrs
52# End Support Functionality
55sentinel = object()
58class ChoiceItem(object):
59 """
60 Describes a choice item.
62 The label is usually the field name so label can normally be left blank.
63 Set a label if you need characters that are illegal in a python identifier
64 name (ie: "DVD/Movie").
65 """
67 order = 0
69 def __init__(self, value=sentinel, label=None, order=None, **extra):
70 self.value = value
71 self.label = label
72 self._extra = extra
74 if order is not None: 74 ↛ 75line 74 didn't jump to line 75, because the condition on line 74 was never true
75 self.order = order
76 else:
77 ChoiceItem.order += 1
78 self.order = ChoiceItem.order
80 def __repr__(self):
81 extras = " ".join(
82 [
83 "{key}={value!r}".format(key=key, value=value)
84 for key, value in self._extra.items()
85 ]
86 )
88 return "<{} value={!r} label={!r} order={!r}{extras}>".format(
89 self.__class__.__name__,
90 self.value,
91 self.label,
92 self.order,
93 extras=" " + extras if extras else "",
94 )
96 def __getattr__(self, name):
97 try:
98 return self._extra[name]
99 except KeyError:
100 raise AttributeError(
101 "{!r} object has no attribute {!r}".format(self.__class__, name)
102 )
105# Shorter convenience alias.
106C = ChoiceItem # noqa
109class DjangoChoicesMeta(type):
110 """
111 Metaclass that writes the choices class.
112 """
114 name_clean = re.compile(r"_+")
116 def __iter__(self):
117 for choice in self.choices:
118 yield choice
120 def __len__(self):
121 return len(self.choices)
123 def __new__(cls, name, bases, attrs):
124 fields = {}
125 labels = Labels()
126 values = OrderedDict()
127 attributes = OrderedDict()
128 choices = []
130 # Get all the fields from parent classes.
131 parents = [b for b in bases if isinstance(b, DjangoChoicesMeta)]
132 for kls in parents:
133 for field_name in kls._fields: 133 ↛ 134line 133 didn't jump to line 134, because the loop on line 133 never started
134 fields[field_name] = kls._fields[field_name]
136 # Get all the fields from this class.
137 for field_name in attrs:
138 val = attrs[field_name]
139 if isinstance(val, ChoiceItem):
140 fields[field_name] = val
142 fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order))
144 for field_name in fields:
145 val = fields[field_name]
146 if isinstance(val, ChoiceItem): 146 ↛ 160line 146 didn't jump to line 160, because the condition on line 146 was never false
147 if val.label is not None:
148 label = val.label
149 else:
150 # TODO: mark translatable by default?
151 label = cls.name_clean.sub(" ", field_name)
153 val0 = label if val.value is sentinel else val.value
154 choices.append((val0, label))
155 attrs[field_name] = StaticProp(val0)
156 setattr(labels, field_name, label)
157 values[val0] = label
158 attributes[val0] = field_name
159 else:
160 choices.append((field_name, val.choices))
162 attrs["choices"] = StaticProp(tuple(choices))
163 attrs["labels"] = labels
164 attrs["values"] = values
165 attrs["_fields"] = fields
166 attrs["validator"] = ChoicesValidator(values)
167 attrs["attributes"] = Attributes(attributes, fields)
169 return super(DjangoChoicesMeta, cls).__new__(cls, name, bases, attrs)
172@deconstructible
173class ChoicesValidator(object):
174 def __init__(self, values):
175 self.values = values
177 def __call__(self, value):
178 if value not in self.values:
179 raise ValidationError(
180 "Select a valid choice. %s is not "
181 "one of the available choices." % value
182 )
184 def __eq__(self, other):
185 return isinstance(other, ChoicesValidator) and self.values == other.values
187 def __ne__(self, other):
188 return not (self == other)
191class DjangoChoices(six.with_metaclass(DjangoChoicesMeta)):
192 order = 0
193 choices = ()
194 labels = Labels()
195 values = {}
196 validator = None
198 @classmethod
199 def get_choice(cls, value):
200 """
201 Return the underlying :class:`ChoiceItem` for a given value.
202 """
203 attribute_for_value = cls.attributes[value]
204 return cls._fields[attribute_for_value]
206 @classmethod
207 def get_order_expression(cls, field_name):
208 """
209 Build the Case/When to annotate objects with the choice item order
211 Useful if choices represent some access-control mechanism, for example.
213 Usage::
215 >>> order = MyChoices.get_order_expression('some_field')
216 >>> queryset = Model.objects.annotate(some_field_order=order)
217 >>> for item in queryset:
218 ... print(item.some_field)
219 ... print(item.some_field_order)
220 # first_choice
221 # 1
222 # second_choice
223 # 2
224 """
225 whens = []
226 for choice_item in cls._fields.values():
227 whens.append(
228 When(
229 **{field_name: choice_item.value, "then": Value(choice_item.order)}
230 )
231 )
232 return Case(*whens, output_field=IntegerField())