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

1from __future__ import absolute_import, unicode_literals 

2 

3import re 

4from collections import OrderedDict 

5 

6from django.core.exceptions import ValidationError 

7from django.db.models import Case, IntegerField, Value, When 

8from django.utils.deconstruct import deconstructible 

9 

10import six 

11 

12__all__ = ["ChoiceItem", "DjangoChoices", "C"] 

13 

14 

15# Support Functionality (Not part of public API) 

16 

17 

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) 

25 

26 def __setattr__(self, name, value): 

27 self[name] = value 

28 

29 

30class StaticProp(object): 

31 def __init__(self, value): 

32 self.value = value 

33 

34 def __get__(self, obj, objtype): 

35 return self.value 

36 

37 

38class Attributes(object): 

39 def __init__(self, attrs, fields): 

40 self.attrs = attrs 

41 self.fields = fields 

42 

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 

50 

51 

52# End Support Functionality 

53 

54 

55sentinel = object() 

56 

57 

58class ChoiceItem(object): 

59 """ 

60 Describes a choice item. 

61 

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 """ 

66 

67 order = 0 

68 

69 def __init__(self, value=sentinel, label=None, order=None, **extra): 

70 self.value = value 

71 self.label = label 

72 self._extra = extra 

73 

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 

79 

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 ) 

87 

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 ) 

95 

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 ) 

103 

104 

105# Shorter convenience alias. 

106C = ChoiceItem # noqa 

107 

108 

109class DjangoChoicesMeta(type): 

110 """ 

111 Metaclass that writes the choices class. 

112 """ 

113 

114 name_clean = re.compile(r"_+") 

115 

116 def __iter__(self): 

117 for choice in self.choices: 

118 yield choice 

119 

120 def __len__(self): 

121 return len(self.choices) 

122 

123 def __new__(cls, name, bases, attrs): 

124 fields = {} 

125 labels = Labels() 

126 values = OrderedDict() 

127 attributes = OrderedDict() 

128 choices = [] 

129 

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] 

135 

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 

141 

142 fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order)) 

143 

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) 

152 

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)) 

161 

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) 

168 

169 return super(DjangoChoicesMeta, cls).__new__(cls, name, bases, attrs) 

170 

171 

172@deconstructible 

173class ChoicesValidator(object): 

174 def __init__(self, values): 

175 self.values = values 

176 

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 ) 

183 

184 def __eq__(self, other): 

185 return isinstance(other, ChoicesValidator) and self.values == other.values 

186 

187 def __ne__(self, other): 

188 return not (self == other) 

189 

190 

191class DjangoChoices(six.with_metaclass(DjangoChoicesMeta)): 

192 order = 0 

193 choices = () 

194 labels = Labels() 

195 values = {} 

196 validator = None 

197 

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] 

205 

206 @classmethod 

207 def get_order_expression(cls, field_name): 

208 """ 

209 Build the Case/When to annotate objects with the choice item order 

210 

211 Useful if choices represent some access-control mechanism, for example. 

212 

213 Usage:: 

214 

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())