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# Imports 

2 

3from django import forms 

4from django.utils.translation import gettext_lazy as _ 

5from superdjango.html.library.forms import FieldGroup 

6 

7# Exports 

8 

9__all__ = ( 

10 "CUSTOM_FIELD_MAP", 

11 "CustomFieldUtility", 

12 "DynamicFieldForm", 

13 "RequestEnabledModelForm", 

14 "SearchForm", 

15) 

16 

17# Constants 

18 

19CUSTOM_FIELD_MAP = { 

20 'bool': forms.BooleanField, 

21 'date': forms.DateField, 

22 'datetime': forms.DateTimeField, 

23 'decimal': forms.DecimalField, 

24 'duration': forms.DurationField, 

25 'email': forms.EmailField, 

26 'float': forms.FloatField, 

27 'int': forms.IntegerField, 

28 'ip': forms.GenericIPAddressField, 

29 'list': forms.ChoiceField, 

30 'slug': forms.SlugField, 

31 'text': forms.CharField, 

32 'time': forms.TimeField, 

33 'url': forms.URLField, 

34 'varchar': forms.CharField, 

35} 

36 

37 

38# Classes 

39 

40 

41class CustomFieldUtility(object): 

42 """A utility class for capturing custom field parameters. See :py:class:`DynamicFieldForm`.""" 

43 

44 def __init__(self, name, choices=None, data_type="text", default=None, help_text=None, input_type=None, 

45 is_required=False, label=None, sort_order=1, **kwargs): 

46 self.choices = choices 

47 self.data_type = data_type 

48 self.default = default 

49 self.help_text = help_text 

50 self.input_type = input_type 

51 self.is_required = is_required 

52 self.label = label or name.replace("_", " ") 

53 self.name = name 

54 self.sort_order = sort_order 

55 self._attributes = kwargs 

56 

57 def __getattr__(self, item): 

58 return self._attributes.get(item) 

59 

60 def __repr__(self): 

61 return "<%s %s>" % (self.__class__.__name__, self.name) 

62 

63 def get_choices(self): 

64 """Get valid choices for the field. 

65 

66 :rtype: list[(str, str)] 

67 

68 """ 

69 if self.choices is None: 

70 return list() 

71 

72 if type(self.choices) in (list, tuple): 

73 return self.choices 

74 

75 if type(self.choices) is str: 

76 a = list() 

77 for i in self.choices.split("\n"): 

78 a.append((i.strip(), i.strip())) 

79 

80 return a 

81 

82 # TODO: Raise an error if CustomFieldUtility.choices is not list, tuple, or str? 

83 return list() 

84 

85 

86# Forms 

87 

88 

89class DynamicFieldForm(forms.ModelForm): 

90 """This dynamic form provides support for custom (user-defined) fields and values attached to a model. 

91 

92 For the sake of clarity, an *extra* field is one that is added to the form, while a *custom* field is the 

93 user-defined extra field. 

94 

95 The model is expected to provide a ``JSONField`` named ``custom_values`` in which submitted values may be stored. 

96 

97 The ``get_custom_fields()`` method of the form must be implemented to return a list of available 

98 :py:class:`CustomFieldUtility` instances that are used to add the extra fields to the form. 

99 

100 The HTML for custom fields must be created manually, for example: 

101 

102 .. code-block:: html 

103 

104 {% load htmgel_tags %} 

105 

106 <form method="post"> 

107 {% csrf_token %} 

108 

109 {% for field in form %} 

110 {% html "form_field" %} 

111 {% endfor %} 

112 

113 {% for custom_field in form.get_extra_fields %} 

114 {% html "form_field" field=custom_field %} 

115 {% endfor %} 

116 </form> 

117 

118 """ 

119 

120 def __init__(self, *args, **kwargs): 

121 """Also initialize custom fields.""" 

122 super().__init__(*args, **kwargs) 

123 

124 for field in self.get_custom_fields(): 

125 _kwargs = { 

126 'help_text': field.help_text, 

127 'initial': self._get_extra_value(field), 

128 'label': field.label, 

129 'required': field.is_required 

130 } 

131 self._add_extra_field(field, _kwargs) 

132 

133 def get_custom_fields(self): 

134 """Get the (available) custom attributes. 

135 

136 :rtype: list[CustomFieldUtility] 

137 

138 """ 

139 raise NotImplementedError() 

140 

141 def get_extra_fields(self): 

142 """Get custom (possibly bound) fields for display in form output. 

143 

144 :rtype: list 

145 

146 """ 

147 a = list() 

148 

149 # noinspection PyUnresolvedReferences 

150 for field_name, field_instance in list(self.fields.items()): 

151 

152 # self[field] is how django.forms.form._html_output calls the form to get a bound field. Otherwise, you may 

153 # get a <django.forms.fields.ChoiceField object at 0x7ffdbc054190> in the output. 

154 if field_name.startswith('custom_'): 

155 # noinspection PyUnresolvedReferences 

156 a.append(self[field_name]) 

157 

158 return a 

159 

160 # noinspection PyMethodMayBeStatic 

161 def has_extra_fields(self): 

162 """Indicates whether custom fields are available, especially for use in form output. 

163 

164 :rtype: bool 

165 

166 """ 

167 return True 

168 

169 def save(self, commit=True): 

170 """Overridden to also save custom fields.""" 

171 

172 obj = super().save(commit=commit) 

173 

174 self.save_extra_fields(obj, commit=commit) 

175 

176 return obj 

177 

178 def save_extra_fields(self, record, commit=True): 

179 """Save custom fields. Called by ``save()``. 

180 

181 :param record: The model instance. 

182 :param commit: Indicates the record should be saved. 

183 

184 """ 

185 for field, clean_value in list(self.cleaned_data.items()): 

186 if field.startswith("custom_"): 

187 field_name = field.replace("custom_", "", 1) 

188 record.custom_values[field_name] = clean_value 

189 

190 if commit: 

191 record.save(update_fields=['custom_values']) 

192 

193 def _add_extra_field(self, field, kwargs): 

194 """Add a custom field to the form. 

195 

196 :param field: The dynamic field (attribute) instance to be added. 

197 :type field: CustomFieldUtility 

198 

199 :param kwargs: The keyword arguments to be passed to the field constructor. 

200 :type kwargs: dict 

201 

202 """ 

203 # The form field is based on the attribute's selected data_type. 

204 try: 

205 field_class = CUSTOM_FIELD_MAP[field.data_type] 

206 except KeyError: 

207 raise NameError("Unrecognized data_type %s" % field.data_type) 

208 

209 # Set keyword arguments specific to the attribute's data_type. 

210 if field.data_type == 'decimal': 

211 kwargs['decimal_places'] = field.decimal_places 

212 kwargs['max_digits'] = field.max_digits 

213 elif field.data_type == 'email': 

214 kwargs['max_length'] = None 

215 kwargs['min_length'] = None 

216 elif field.data_type == 'int': 

217 kwargs['widget'] = forms.NumberInput 

218 elif field.data_type == 'list': 

219 kwargs['choices'] = field.get_choices() 

220 elif field.data_type == 'text': 

221 kwargs['widget'] = forms.Textarea 

222 kwargs['max_length'] = field.max_length 

223 elif field.data_type == 'varchar': 

224 kwargs['max_length'] = field.max_length 

225 kwargs['min_length'] = field.min_length 

226 else: 

227 pass 

228 

229 # Override the widget if the field is hidden. 

230 if field.is_hidden: 

231 kwargs['widget'] = forms.HiddenInput 

232 

233 # This is how we know which fields have been added dynamically. 

234 field_name = 'custom_%s' % field.name 

235 

236 # noinspection PyUnresolvedReferences 

237 self.fields[field_name] = field_class(**kwargs) 

238 

239 def _get_extra_value(self, field): 

240 """Get the value of a custom field. 

241 

242 :param field: The custom field. 

243 :type field: CustomFieldUtility 

244 

245 """ 

246 if not self.instance.custom_values: 

247 return field.default 

248 

249 if self.instance and self.instance.todo_type.custom_fields: 

250 if field in self.instance.custom_values.keys(): 

251 return self.instance.custom_values.get(field.name, field.default) 

252 

253 return field.default 

254 

255 

256class RequestEnabledModelForm(forms.ModelForm): 

257 """Incorporates the current request and enables fieldset/tab support. Used by SuperDjango UI. """ 

258 

259 def __init__(self, fieldsets=None, request=None, tabs=None, **kwargs): 

260 """Add support for fieldsets and current request.""" 

261 self.request = request 

262 self._fieldsets = fieldsets 

263 self._tabs = tabs 

264 

265 super().__init__(**kwargs) 

266 

267 @property 

268 def fieldsets(self): 

269 """Alias for ``get_fieldsets()``.""" 

270 return self.get_fieldsets() 

271 

272 def get_fieldsets(self): 

273 """Get the form's fieldsets including field instances. 

274 

275 :rtype: list[superdjango.html.library.Fieldset] 

276 

277 """ 

278 if self._fieldsets is None: 

279 return list() 

280 

281 for fieldset in self._fieldsets: 

282 _fields = list() 

283 for field_name in fieldset.fields: 

284 # Handle superdjango.ui.options.utils.FieldGroup instances. 

285 try: 

286 subfields = getattr(field_name, "fields") 

287 _subfields = list() 

288 for f in subfields: 

289 _subfields.append(self[f]) 

290 

291 fg = FieldGroup(*_subfields, label=field_name.label, size=field_name.size) 

292 _fields.append(fg) 

293 except AttributeError: 

294 _fields.append(self[field_name]) 

295 

296 fieldset._fields = _fields 

297 

298 return self._fieldsets 

299 

300 def get_tabs(self): 

301 """Get the form's tabs including field instances. 

302 

303 :rtype: list[Tab] 

304 

305 """ 

306 if self._tabs is None: 

307 return list() 

308 

309 for tab in self._tabs: 

310 # Inline tabs contain a formset rather than fields. 

311 if tab.inline: 

312 continue 

313 

314 # Build the list of fields from each tab, checking for the existence of field groups. 

315 _fields = list() 

316 for field_name in tab.fields: 

317 # Handle superdjango.ui.options.utils.FieldGroup instances. 

318 try: 

319 subfields = getattr(field_name, "fields") 

320 _subfields = list() 

321 for f in subfields: 

322 _subfields.append(self[f]) 

323 

324 fg = FieldGroup(*_subfields, label=field_name.label, size=field_name.size) 

325 _fields.append(fg) 

326 except AttributeError: 

327 _fields.append(self[field_name]) 

328 

329 # from superdjango.ui.options.utils import FieldGroup 

330 # if isinstance(field_name, FieldGroup): 

331 # subfields = list() 

332 # for f in field_name.fields: 

333 # subfields.append(self[f]) 

334 # else: 

335 # _fields.append(self[field_name]) 

336 

337 tab._fields = _fields 

338 

339 return self._tabs 

340 

341 @property 

342 def has_fieldsets(self): 

343 """Indicates whether the form has defined fieldsets. 

344 

345 :rtype: bool 

346 

347 """ 

348 return self._fieldsets is not None 

349 

350 @property 

351 def has_tabs(self): 

352 """Indicates whether the form has defined tabs. 

353 

354 :rtype: bool 

355 

356 """ 

357 return self._tabs is not None 

358 

359 @property 

360 def tabs(self): 

361 """Alias for ``get_tabs()``.""" 

362 return self.get_tabs() 

363 

364 

365class SearchForm(forms.Form): 

366 """Standard search form, used by UI search views.""" 

367 

368 keywords = forms.CharField( 

369 label=_("Keywords"), 

370 help_text=_("Enter the keyword(s) for which you'd like to search."), 

371 required=True 

372 ) 

373 

374 case_sensitive = forms.BooleanField( 

375 label=_("Match Case"), 

376 help_text=_("Results should match the case of the keywords."), 

377 required=False 

378 ) 

379 

380 exact_matching = forms.BooleanField( 

381 label=_("Exact Match"), 

382 help_text=_("Results must exactly match search terms."), 

383 required=False 

384 )