Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# (c) 2018-2020 

2# MPIB <https://www.mpib-berlin.mpg.de/>, 

3# MPI-CBS <https://www.cbs.mpg.de/>, 

4# MPIP <http://www.psych.mpg.de/> 

5# 

6# This file is part of Castellum. 

7# 

8# Castellum is free software; you can redistribute it and/or modify it 

9# under the terms of the GNU Affero General Public License as published 

10# by the Free Software Foundation; either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# Castellum is distributed in the hope that it will be useful, but 

14# WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

16# Affero General Public License for more details. 

17# 

18# You should have received a copy of the GNU Affero General Public 

19# License along with Castellum. If not, see 

20# <http://www.gnu.org/licenses/>. 

21 

22import datetime 

23 

24from django import forms 

25from django.db import models 

26from django.utils.translation import gettext_lazy as _ 

27 

28from dateutil.relativedelta import relativedelta 

29 

30from castellum.utils.forms import DateField 

31from castellum.utils.forms import IntegerChoiceField 

32from castellum.utils.forms import IntegerMultipleChoiceField 

33 

34from .fields import AgeField 

35 

36ANSWER_DECLINED = "ANSWER_DECLINED" 

37 

38 

39class BaseAttributeField: 

40 available_operators = NotImplemented 

41 form_class = NotImplemented 

42 filter_form_class = None 

43 statistics_buckets = [] 

44 

45 def __init__(self, description): 

46 self.description = description 

47 

48 def formfield(self, form_class=None, **kwargs): 

49 form_class = form_class or self.form_class 

50 defaults = { 

51 'label': self.description.label, 

52 'help_text': self.description.help_text, 

53 'required': False, 

54 } 

55 defaults.update(kwargs) 

56 return form_class(**defaults) 

57 

58 def filter_formfield(self, **kwargs): 

59 return self.formfield(self.filter_form_class, **kwargs) 

60 

61 def _filter_to_q(self, operator, value): 

62 field_name = 'attributeset__data__' + self.description.json_key 

63 key = '{}__{}'.format(field_name, operator) 

64 return models.Q(**{key: value}) 

65 

66 def filter_to_q(self, operator, value, include_unknown=True): 

67 field_name = 'attributeset__data__' + self.description.json_key 

68 inverse = operator.startswith('!') 

69 if inverse: 69 ↛ 70line 69 didn't jump to line 70, because the condition on line 69 was never true

70 operator = operator[1:] 

71 q = self._filter_to_q(operator, value) 

72 q_unknown = ( 

73 models.Q(**{field_name: ''}) | 

74 models.Q(**{field_name: None}) | 

75 models.Q(**{field_name + '__isnull': True}) 

76 ) 

77 

78 if inverse: 78 ↛ 79line 78 didn't jump to line 79, because the condition on line 78 was never true

79 if include_unknown: 

80 return ~q 

81 else: 

82 return ~q & ~q_unknown 

83 else: 

84 if include_unknown: 84 ↛ 87line 84 didn't jump to line 87, because the condition on line 84 was never false

85 return q | q_unknown 

86 else: 

87 return q 

88 

89 def from_json(self, value): 

90 return value 

91 

92 def get_display(self, value): 

93 if value is None: 

94 return '—' 

95 elif value is ANSWER_DECLINED: 

96 return _('Declined to answer') 

97 else: 

98 return self.from_json(value) 

99 

100 def get_filter_display(self, operator, value): 

101 operator = dict(self.available_operators)[operator] 

102 return '{} {} {}'.format(self.description.filter_label, operator, value) 

103 

104 def get_statistics_buckets(self): 

105 return self.statistics_buckets 

106 

107 def get_statistics_bucket(self, value): 

108 return value 

109 

110 

111class DateAttributeField(BaseAttributeField): 

112 form_class = DateField 

113 available_operators = [ 

114 ("lt", _("before")), 

115 ("gt", _("after")), 

116 ] 

117 

118 def from_json(self, value): 

119 if value in [None, ANSWER_DECLINED]: 

120 return value 

121 dt = datetime.datetime.strptime(value, '%Y-%m-%d') 

122 return dt.date() 

123 

124 

125class NumberAttributeField(BaseAttributeField): 

126 form_class = forms.IntegerField 

127 available_operators = [ 

128 ("lt", _("<")), 

129 ("gt", _(">")), 

130 ("exact", _("==")), 

131 ("!exact", _("!=")), 

132 ("lte", _("<=")), 

133 ("gte", _(">=")), 

134 ] 

135 

136 

137class BooleanAttributeField(BaseAttributeField): 

138 form_class = forms.NullBooleanField 

139 available_operators = [ 

140 ("exact", _("is")), 

141 ] 

142 statistics_buckets = [ 

143 (True, _('Yes')), 

144 (False, _('No')), 

145 ] 

146 

147 

148class TextAttributeField(BaseAttributeField): 

149 form_class = forms.CharField 

150 available_operators = [ 

151 ("exact", _("is")), 

152 ("!exact", _("is not")), 

153 ("icontains", _("contains")), 

154 ] 

155 

156 

157class ChoiceAttributeField(BaseAttributeField): 

158 form_class = IntegerChoiceField 

159 available_operators = [ 

160 ("exact", _("is")), 

161 ("!exact", _("is not")), 

162 ] 

163 

164 def formfield(self, form_class=None, **kwargs): 

165 qs = self.description.attributechoice_set.translated() 

166 choices = [(None, _('Unknown'))] + list(qs.values_list('pk', 'translations__label')) 

167 return super().formfield(form_class=form_class, choices=choices, **kwargs) 

168 

169 def get_display(self, value): 

170 if value in [None, ANSWER_DECLINED]: 

171 return super().get_display(value) 

172 choice = self.description.attributechoice_set.get(pk=value) 

173 return choice.label 

174 

175 def get_filter_display(self, operator, value): 

176 value = self.get_display(value) 

177 return super().get_filter_display(operator, value) 

178 

179 def get_statistics_buckets(self): 

180 return [ 

181 (choice.pk, choice.label) for choice in self.description.attributechoice_set.all() 

182 ] 

183 

184 

185class MultipleChoiceAttributeField(ChoiceAttributeField): 

186 form_class = IntegerMultipleChoiceField 

187 filter_form_class = IntegerChoiceField 

188 available_operators = [ 

189 ("contains", _("contains")), 

190 ("!contains", _("does not contain")), 

191 ] 

192 

193 def get_display(self, value): 

194 if not value: 

195 return super().get_display(None) 

196 if value == ANSWER_DECLINED: 

197 return super().get_display(value) 

198 if not isinstance(value, list): 

199 value = [value] 

200 s = super() 

201 return ', '.join(s.get_display(v) for v in value) 

202 

203 

204class OrderedChoiceAttributeField(ChoiceAttributeField): 

205 available_operators = ChoiceAttributeField.available_operators + [ 

206 ("gte", _("is at least")), 

207 ("lte", _("is at most")), 

208 ] 

209 

210 def _filter_to_q(self, operator, value): 

211 if operator in ['gte', 'lte']: 

212 threshold = self.description.attributechoice_set.get(pk=value) 

213 allowed_choices = self.description.attributechoice_set.filter(**{ 

214 'order__' + operator: threshold.order 

215 }) 

216 # JSONField cannot use `in` lookups 

217 q = models.Q() 

218 for choice in allowed_choices: 

219 q |= models.Q(**{'attributeset__data__' + self.description.json_key: choice.pk}) 

220 return q 

221 return super()._filter_to_q(operator, value) 

222 

223 

224class AgeAttributeField(DateAttributeField): 

225 SCALES = { 

226 'years': _('years'), 

227 'months': _('months'), 

228 'days': _('days'), 

229 } 

230 

231 filter_form_class = AgeField 

232 available_operators = [ 

233 ("lt", _("older than")), 

234 ("gt", _("younger than")), 

235 ] 

236 statistics_buckets = [ 

237 ('<18', '<18'), 

238 ('18-25', '18-25'), 

239 ('26-30', '26-30'), 

240 ('31-35', '31-35'), 

241 ('36-40', '36-40'), 

242 ('41-45', '41-45'), 

243 ('46-50', '46-50'), 

244 ('51-55', '51-55'), 

245 ('56-60', '56-60'), 

246 ('61-65', '61-65'), 

247 ('66-70', '66-70'), 

248 ('>70', '>70'), 

249 ] 

250 

251 def _filter_to_q(self, operator, value): 

252 period, scale = value 

253 try: 

254 date = datetime.date.today() - relativedelta(**{scale: period}) 

255 except (ValueError, OverflowError): 

256 date = datetime.date.min 

257 return super()._filter_to_q(operator, date) 

258 

259 def get_filter_display(self, operator, value): 

260 period, scale = value 

261 value = '{} {}'.format(period, self.SCALES[scale]) 

262 return super().get_filter_display(operator, value) 

263 

264 def get_statistics_bucket(self, value): 

265 today = datetime.date.today() 

266 if today < value + relativedelta(years=18): 

267 return '<18' 

268 if today < value + relativedelta(years=25): 

269 return '18-25' 

270 for y in range(26, 70, 5): 

271 if today < value + relativedelta(years=y + 5): 

272 return '{}-{}'.format(y, y + 4) 

273 return '>70'