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
3from django import forms
4from django.utils.translation import gettext_lazy as _
5from superdjango.html.library.forms import FieldGroup
7# Exports
9__all__ = (
10 "CUSTOM_FIELD_MAP",
11 "CustomFieldUtility",
12 "DynamicFieldForm",
13 "RequestEnabledModelForm",
14 "SearchForm",
15)
17# Constants
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}
38# Classes
41class CustomFieldUtility(object):
42 """A utility class for capturing custom field parameters. See :py:class:`DynamicFieldForm`."""
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
57 def __getattr__(self, item):
58 return self._attributes.get(item)
60 def __repr__(self):
61 return "<%s %s>" % (self.__class__.__name__, self.name)
63 def get_choices(self):
64 """Get valid choices for the field.
66 :rtype: list[(str, str)]
68 """
69 if self.choices is None:
70 return list()
72 if type(self.choices) in (list, tuple):
73 return self.choices
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()))
80 return a
82 # TODO: Raise an error if CustomFieldUtility.choices is not list, tuple, or str?
83 return list()
86# Forms
89class DynamicFieldForm(forms.ModelForm):
90 """This dynamic form provides support for custom (user-defined) fields and values attached to a model.
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.
95 The model is expected to provide a ``JSONField`` named ``custom_values`` in which submitted values may be stored.
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.
100 The HTML for custom fields must be created manually, for example:
102 .. code-block:: html
104 {% load htmgel_tags %}
106 <form method="post">
107 {% csrf_token %}
109 {% for field in form %}
110 {% html "form_field" %}
111 {% endfor %}
113 {% for custom_field in form.get_extra_fields %}
114 {% html "form_field" field=custom_field %}
115 {% endfor %}
116 </form>
118 """
120 def __init__(self, *args, **kwargs):
121 """Also initialize custom fields."""
122 super().__init__(*args, **kwargs)
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)
133 def get_custom_fields(self):
134 """Get the (available) custom attributes.
136 :rtype: list[CustomFieldUtility]
138 """
139 raise NotImplementedError()
141 def get_extra_fields(self):
142 """Get custom (possibly bound) fields for display in form output.
144 :rtype: list
146 """
147 a = list()
149 # noinspection PyUnresolvedReferences
150 for field_name, field_instance in list(self.fields.items()):
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])
158 return a
160 # noinspection PyMethodMayBeStatic
161 def has_extra_fields(self):
162 """Indicates whether custom fields are available, especially for use in form output.
164 :rtype: bool
166 """
167 return True
169 def save(self, commit=True):
170 """Overridden to also save custom fields."""
172 obj = super().save(commit=commit)
174 self.save_extra_fields(obj, commit=commit)
176 return obj
178 def save_extra_fields(self, record, commit=True):
179 """Save custom fields. Called by ``save()``.
181 :param record: The model instance.
182 :param commit: Indicates the record should be saved.
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
190 if commit:
191 record.save(update_fields=['custom_values'])
193 def _add_extra_field(self, field, kwargs):
194 """Add a custom field to the form.
196 :param field: The dynamic field (attribute) instance to be added.
197 :type field: CustomFieldUtility
199 :param kwargs: The keyword arguments to be passed to the field constructor.
200 :type kwargs: dict
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)
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
229 # Override the widget if the field is hidden.
230 if field.is_hidden:
231 kwargs['widget'] = forms.HiddenInput
233 # This is how we know which fields have been added dynamically.
234 field_name = 'custom_%s' % field.name
236 # noinspection PyUnresolvedReferences
237 self.fields[field_name] = field_class(**kwargs)
239 def _get_extra_value(self, field):
240 """Get the value of a custom field.
242 :param field: The custom field.
243 :type field: CustomFieldUtility
245 """
246 if not self.instance.custom_values:
247 return field.default
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)
253 return field.default
256class RequestEnabledModelForm(forms.ModelForm):
257 """Incorporates the current request and enables fieldset/tab support. Used by SuperDjango UI. """
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
265 super().__init__(**kwargs)
267 @property
268 def fieldsets(self):
269 """Alias for ``get_fieldsets()``."""
270 return self.get_fieldsets()
272 def get_fieldsets(self):
273 """Get the form's fieldsets including field instances.
275 :rtype: list[superdjango.html.library.Fieldset]
277 """
278 if self._fieldsets is None:
279 return list()
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])
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])
296 fieldset._fields = _fields
298 return self._fieldsets
300 def get_tabs(self):
301 """Get the form's tabs including field instances.
303 :rtype: list[Tab]
305 """
306 if self._tabs is None:
307 return list()
309 for tab in self._tabs:
310 # Inline tabs contain a formset rather than fields.
311 if tab.inline:
312 continue
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])
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])
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])
337 tab._fields = _fields
339 return self._tabs
341 @property
342 def has_fieldsets(self):
343 """Indicates whether the form has defined fieldsets.
345 :rtype: bool
347 """
348 return self._fieldsets is not None
350 @property
351 def has_tabs(self):
352 """Indicates whether the form has defined tabs.
354 :rtype: bool
356 """
357 return self._tabs is not None
359 @property
360 def tabs(self):
361 """Alias for ``get_tabs()``."""
362 return self.get_tabs()
365class SearchForm(forms.Form):
366 """Standard search form, used by UI search views."""
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 )
374 case_sensitive = forms.BooleanField(
375 label=_("Match Case"),
376 help_text=_("Results should match the case of the keywords."),
377 required=False
378 )
380 exact_matching = forms.BooleanField(
381 label=_("Exact Match"),
382 help_text=_("Results must exactly match search terms."),
383 required=False
384 )