1 from __future__ import unicode_literals
2 from future.builtins import int, range, str
4 from datetime import date, datetime
5 from os.path import join, split
9 from django import forms
10 from django.forms.extras import SelectDateWidget
11 from django.core.files.storage import FileSystemStorage
12 from django.core.urlresolvers import reverse
13 from django.template import Template
14 from django.utils.safestring import mark_safe
15 from django.utils.translation import ugettext_lazy as _
17 from forms_builder.forms import fields
18 from forms_builder.forms.models import FormEntry, FieldEntry
19 from forms_builder.forms import settings
20 from forms_builder.forms.utils import now, split_choices
23 fs = FileSystemStorage(location=settings.UPLOAD_ROOT)
25 ##############################
26 # Each type of export filter #
27 ##############################
30 FILTER_CHOICE_CONTAINS = "1"
31 FILTER_CHOICE_DOESNT_CONTAIN = "2"
34 FILTER_CHOICE_EQUALS = "3"
35 FILTER_CHOICE_DOESNT_EQUAL = "4"
38 FILTER_CHOICE_BETWEEN = "5"
41 FILTER_CHOICE_CONTAINS_ANY = "6"
42 FILTER_CHOICE_CONTAINS_ALL = "7"
43 FILTER_CHOICE_DOESNT_CONTAIN_ANY = "8"
44 FILTER_CHOICE_DOESNT_CONTAIN_ALL = "9"
46 ##########################
47 # Export filters grouped #
48 ##########################
51 TEXT_FILTER_CHOICES = (
53 (FILTER_CHOICE_CONTAINS, _("Contains")),
54 (FILTER_CHOICE_DOESNT_CONTAIN, _("Doesn't contain")),
55 (FILTER_CHOICE_EQUALS, _("Equals")),
56 (FILTER_CHOICE_DOESNT_EQUAL, _("Doesn't equal")),
59 # Choices with single value entries
60 CHOICE_FILTER_CHOICES = (
62 (FILTER_CHOICE_CONTAINS_ANY, _("Equals any")),
63 (FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't equal any")),
66 # Choices with multiple value entries
67 MULTIPLE_FILTER_CHOICES = (
69 (FILTER_CHOICE_CONTAINS_ANY, _("Contains any")),
70 (FILTER_CHOICE_CONTAINS_ALL, _("Contains all")),
71 (FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't contain any")),
72 (FILTER_CHOICE_DOESNT_CONTAIN_ALL, _("Doesn't contain all")),
76 DATE_FILTER_CHOICES = (
78 (FILTER_CHOICE_BETWEEN, _("Is between")),
81 # The filter function for each filter type
83 FILTER_CHOICE_CONTAINS:
84 lambda val, field: val.lower() in field.lower(),
85 FILTER_CHOICE_DOESNT_CONTAIN:
86 lambda val, field: val.lower() not in field.lower(),
88 lambda val, field: val.lower() == field.lower(),
89 FILTER_CHOICE_DOESNT_EQUAL:
90 lambda val, field: val.lower() != field.lower(),
91 FILTER_CHOICE_BETWEEN:
92 lambda val_from, val_to, field: (
93 (not val_from or val_from <= field) and
94 (not val_to or val_to >= field)
96 FILTER_CHOICE_CONTAINS_ANY:
97 lambda val, field: set(val) & set(split_choices(field)),
98 FILTER_CHOICE_CONTAINS_ALL:
99 lambda val, field: set(val) == set(split_choices(field)),
100 FILTER_CHOICE_DOESNT_CONTAIN_ANY:
101 lambda val, field: not set(val) & set(split_choices(field)),
102 FILTER_CHOICE_DOESNT_CONTAIN_ALL:
103 lambda val, field: set(val) != set(split_choices(field)),
106 # Export form fields for each filter type grouping
107 text_filter_field = forms.ChoiceField(label=" ", required=False,
108 choices=TEXT_FILTER_CHOICES)
109 choice_filter_field = forms.ChoiceField(label=" ", required=False,
110 choices=CHOICE_FILTER_CHOICES)
111 multiple_filter_field = forms.ChoiceField(label=" ", required=False,
112 choices=MULTIPLE_FILTER_CHOICES)
113 date_filter_field = forms.ChoiceField(label=" ", required=False,
114 choices=DATE_FILTER_CHOICES)
117 class FormForForm(forms.ModelForm):
118 field_entry_model = FieldEntry
122 exclude = ("form", "entry_time")
124 def __init__(self, form, context, *args, **kwargs):
126 Dynamically add each of the form fields for the given form model
127 instance and its related field model instances.
130 self.form_fields = form.fields.visible()
131 initial = kwargs.pop("initial", {})
132 # If a FormEntry instance is given to edit, stores it's field
133 # values for using as initial data.
135 if kwargs.get("instance"):
136 for field_entry in kwargs["instance"].fields.all():
137 field_entries[field_entry.field_id] = field_entry.value
138 super(FormForForm, self).__init__(*args, **kwargs)
139 # Create the form fields.
140 for field in self.form_fields:
141 field_key = field.slug
142 field_class = fields.CLASSES[field.field_type]
143 field_widget = fields.WIDGETS.get(field.field_type)
144 field_args = {"label": field.label, "required": field.required,
145 "help_text": field.help_text}
146 arg_names = field_class.__init__.__code__.co_varnames
147 if "max_length" in arg_names:
148 field_args["max_length"] = settings.FIELD_MAX_LENGTH
149 if "choices" in arg_names:
150 choices = list(field.get_choices())
151 if (field.field_type == fields.SELECT and
152 field.default not in [c[0] for c in choices]):
153 choices.insert(0, ("", field.placeholder_text))
154 field_args["choices"] = choices
155 if field_widget is not None:
156 field_args["widget"] = field_widget
158 # Initial value for field, in order of preference:
160 # - If a form model instance is given (eg we're editing a
161 # form response), then use the instance's value for the
163 # - If the developer has provided an explicit "initial"
165 # - The default value for the field instance as given in
170 initial_val = field_entries[field.id]
173 initial_val = initial[field_key]
175 initial_val = Template(field.default).render(context)
177 if field.is_a(*fields.MULTIPLE):
178 initial_val = split_choices(initial_val)
179 if field.field_type == fields.CHECKBOX:
180 initial_val = initial_val != "False"
181 self.initial[field_key] = initial_val
182 self.fields[field_key] = field_class(**field_args)
184 if field.field_type == fields.DOB:
186 years = list(range(now.year, now.year - 120, -1))
187 self.fields[field_key].widget.years = years
189 # Add identifying CSS classes to the field.
190 css_class = field_class.__name__.lower()
192 css_class += " required"
193 if (settings.USE_HTML5 and
194 field.field_type != fields.CHECKBOX_MULTIPLE):
195 self.fields[field_key].widget.attrs["required"] = ""
196 self.fields[field_key].widget.attrs["class"] = css_class
197 if field.placeholder_text and not field.default:
198 text = field.placeholder_text
199 self.fields[field_key].widget.attrs["placeholder"] = text
201 def save(self, **kwargs):
203 Get/create a FormEntry instance and assign submitted values to
204 related FieldEntry instances for each form field.
206 entry = super(FormForForm, self).save(commit=False)
207 entry.form = self.form
208 entry.entry_time = now()
210 entry_fields = entry.fields.values_list("field_id", flat=True)
211 new_entry_fields = []
212 for field in self.form_fields:
213 field_key = field.slug
214 value = self.cleaned_data[field_key]
215 if value and self.fields[field_key].widget.needs_multipart_form:
216 value = fs.save(join("forms", str(uuid4()), value.name), value)
217 if isinstance(value, list):
218 value = ", ".join([v.strip() for v in value])
219 if field.id in entry_fields:
220 field_entry = entry.fields.get(field_id=field.id)
221 field_entry.value = value
224 new = {"entry": entry, "field_id": field.id, "value": value}
225 new_entry_fields.append(self.field_entry_model(**new))
227 if django.VERSION >= (1, 4, 0):
228 self.field_entry_model.objects.bulk_create(new_entry_fields)
230 for field_entry in new_entry_fields:
236 Return the value entered for the first field of type EmailField.
238 for field in self.form_fields:
239 if field.is_a(fields.EMAIL):
240 return self.cleaned_data[field.slug]
244 class EntriesForm(forms.Form):
246 Form with a set of fields dynamically assigned that can be used to
247 filter entries for the given ``forms.models.Form`` instance.
250 def __init__(self, form, request, formentry_model=FormEntry,
251 fieldentry_model=FieldEntry, *args, **kwargs):
253 Iterate through the fields of the ``forms.models.Form`` instance and
254 create the form fields required to control including the field in
255 the export (with a checkbox) or filtering the field which differs
256 across field types. User a list of checkboxes when a fixed set of
257 choices can be chosen from, a pair of date fields for date ranges,
258 and for all other types provide a textbox for text search.
261 self.request = request
262 self.formentry_model = formentry_model
263 self.fieldentry_model = fieldentry_model
264 self.form_fields = form.fields.all()
265 self.entry_time_name = str(self.formentry_model._meta.get_field(
266 "entry_time").verbose_name)
267 super(EntriesForm, self).__init__(*args, **kwargs)
268 for field in self.form_fields:
269 field_key = "field_%s" % field.id
270 # Checkbox for including in export.
271 self.fields["%s_export" % field_key] = forms.BooleanField(
272 label=field.label, initial=True, required=False)
273 if field.is_a(*fields.CHOICES):
274 # A fixed set of choices to filter by.
275 if field.is_a(fields.CHECKBOX):
276 choices = ((True, _("Checked")), (False, _("Not checked")))
278 choices = field.get_choices()
279 contains_field = forms.MultipleChoiceField(label=" ",
280 choices=choices, widget=forms.CheckboxSelectMultiple(),
282 self.fields["%s_filter" % field_key] = choice_filter_field
283 self.fields["%s_contains" % field_key] = contains_field
284 elif field.is_a(*fields.MULTIPLE):
285 # A fixed set of choices to filter by, with multiple
286 # possible values in the entry field.
287 contains_field = forms.MultipleChoiceField(label=" ",
288 choices=field.get_choices(),
289 widget=forms.CheckboxSelectMultiple(),
291 self.fields["%s_filter" % field_key] = multiple_filter_field
292 self.fields["%s_contains" % field_key] = contains_field
293 elif field.is_a(*fields.DATES):
294 # A date range to filter by.
295 self.fields["%s_filter" % field_key] = date_filter_field
296 self.fields["%s_from" % field_key] = forms.DateField(
297 label=" ", widget=SelectDateWidget(), required=False)
298 self.fields["%s_to" % field_key] = forms.DateField(
299 label=_("and"), widget=SelectDateWidget(), required=False)
301 # Text box for search term to filter by.
302 contains_field = forms.CharField(label=" ", required=False)
303 self.fields["%s_filter" % field_key] = text_filter_field
304 self.fields["%s_contains" % field_key] = contains_field
305 # Add ``FormEntry.entry_time`` as a field.
306 field_key = "field_0"
307 label = self.formentry_model._meta.get_field("entry_time").verbose_name
308 self.fields["%s_export" % field_key] = forms.BooleanField(
309 initial=True, label=label, required=False)
310 self.fields["%s_filter" % field_key] = date_filter_field
311 self.fields["%s_from" % field_key] = forms.DateField(
312 label=" ", widget=SelectDateWidget(), required=False)
313 self.fields["%s_to" % field_key] = forms.DateField(
314 label=_("and"), widget=SelectDateWidget(), required=False)
318 Yield pairs of include checkbox / filters for each field.
320 for field_id in [f.id for f in self.form_fields] + [0]:
321 prefix = "field_%s_" % field_id
322 fields = [f for f in super(EntriesForm, self).__iter__()
323 if f.name.startswith(prefix)]
324 yield fields[0], fields[1], fields[2:]
326 def posted_data(self, field):
328 Wrapper for self.cleaned_data that returns True on
329 field_id_export fields when the form hasn't been posted to,
330 to facilitate show/export URLs that export all entries without
334 return self.cleaned_data[field]
335 except (AttributeError, KeyError):
336 return field.endswith("_export")
340 Returns the list of selected column names.
342 fields = [f.label for f in self.form_fields
343 if self.posted_data("field_%s_export" % f.id)]
344 if self.posted_data("field_0_export"):
345 fields.append(self.entry_time_name)
348 def rows(self, csv=False):
350 Returns each row based on the selected criteria.
353 # Store the index of each field against its ID for building each
354 # entry row with columns in the correct order. Also store the IDs of
355 # fields with a type of FileField or Date-like for special handling of
360 for field in self.form_fields:
361 if self.posted_data("field_%s_export" % field.id):
362 field_indexes[field.id] = len(field_indexes)
363 if field.is_a(fields.FILE):
364 file_field_ids.append(field.id)
365 elif field.is_a(*fields.DATES):
366 date_field_ids.append(field.id)
367 num_columns = len(field_indexes)
368 include_entry_time = self.posted_data("field_0_export")
369 if include_entry_time:
372 # Get the field entries for the given form and filter by entry_time
374 model = self.fieldentry_model
375 field_entries = model.objects.filter(entry__form=self.form
376 ).order_by("-entry__id").select_related("entry")
377 if self.posted_data("field_0_filter") == FILTER_CHOICE_BETWEEN:
378 time_from = self.posted_data("field_0_from")
379 time_to = self.posted_data("field_0_to")
380 if time_from and time_to:
381 field_entries = field_entries.filter(
382 entry__entry_time__range=(time_from, time_to))
384 # Loop through each field value ordered by entry, building up each
385 # entry as a row. Use the ``valid_row`` flag for marking a row as
386 # invalid if it fails one of the filtering criteria specified.
390 for field_entry in field_entries:
391 if field_entry.entry_id != current_entry:
392 # New entry, write out the current row and start a new one.
393 if valid_row and current_row is not None:
395 current_row.insert(0, current_entry)
397 current_entry = field_entry.entry_id
398 current_row = [""] * num_columns
400 if include_entry_time:
401 current_row[-1] = field_entry.entry.entry_time
402 field_value = field_entry.value or ""
404 field_id = field_entry.field_id
405 filter_type = self.posted_data("field_%s_filter" % field_id)
408 if filter_type == FILTER_CHOICE_BETWEEN:
409 f, t = "field_%s_from" % field_id, "field_%s_to" % field_id
410 filter_args = [self.posted_data(f), self.posted_data(t)]
412 field_name = "field_%s_contains" % field_id
413 filter_args = self.posted_data(field_name)
415 filter_args = [filter_args]
417 # Convert dates before checking filter.
418 if field_id in date_field_ids:
420 y, m, d = field_value.split(" ")[0].split("-")
422 filter_args.append(field_value)
424 dte = date(int(y), int(m), int(d))
425 filter_args.append(dte)
427 filter_args.append(field_value)
428 filter_func = FILTER_FUNCS[filter_type]
429 if not filter_func(*filter_args):
431 # Create download URL for file fields.
432 if field_entry.value and field_id in file_field_ids:
433 url = reverse("admin:form_file", args=(field_entry.id,))
434 field_value = self.request.build_absolute_uri(url)
436 parts = (field_value, split(field_entry.value)[1])
437 field_value = mark_safe("<a href=\"%s\">%s</a>" % parts)
438 # Only use values for fields that were selected.
440 current_row[field_indexes[field_id]] = field_value
443 # Output the final row.
444 if valid_row and current_row is not None:
446 current_row.insert(0, current_entry)