update librarian
[redakcja.git] / apps / forms_builder / forms / forms.py
1 from __future__ import unicode_literals
2 from future.builtins import int, range, str
3
4 from datetime import date, datetime
5 from os.path import join, split
6 from uuid import uuid4
7
8 import django
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 _
16
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
21
22
23 fs = FileSystemStorage(location=settings.UPLOAD_ROOT)
24
25 ##############################
26 # Each type of export filter #
27 ##############################
28
29 # Text matches
30 FILTER_CHOICE_CONTAINS = "1"
31 FILTER_CHOICE_DOESNT_CONTAIN = "2"
32
33 # Exact matches
34 FILTER_CHOICE_EQUALS = "3"
35 FILTER_CHOICE_DOESNT_EQUAL = "4"
36
37 # Greater/less than
38 FILTER_CHOICE_BETWEEN = "5"
39
40 # Multiple values
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"
45
46 ##########################
47 # Export filters grouped #
48 ##########################
49
50 # Text fields
51 TEXT_FILTER_CHOICES = (
52     ("", _("Nothing")),
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")),
57 )
58
59 # Choices with single value entries
60 CHOICE_FILTER_CHOICES = (
61     ("", _("Nothing")),
62     (FILTER_CHOICE_CONTAINS_ANY, _("Equals any")),
63     (FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't equal any")),
64 )
65
66 # Choices with multiple value entries
67 MULTIPLE_FILTER_CHOICES = (
68     ("", _("Nothing")),
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")),
73 )
74
75 # Dates
76 DATE_FILTER_CHOICES = (
77     ("", _("Nothing")),
78     (FILTER_CHOICE_BETWEEN, _("Is between")),
79 )
80
81 # The filter function for each filter type
82 FILTER_FUNCS = {
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(),
87     FILTER_CHOICE_EQUALS:
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)
95         ),
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)),
104 }
105
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)
115
116
117 class FormForForm(forms.ModelForm):
118     field_entry_model = FieldEntry
119
120     class Meta:
121         model = FormEntry
122         exclude = ("form", "entry_time")
123
124     def __init__(self, form, context, *args, **kwargs):
125         """
126         Dynamically add each of the form fields for the given form model
127         instance and its related field model instances.
128         """
129         self.form = form
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.
134         field_entries = {}
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
157             #
158             #   Initial value for field, in order of preference:
159             #
160             # - If a form model instance is given (eg we're editing a
161             #   form response), then use the instance's value for the
162             #   field.
163             # - If the developer has provided an explicit "initial"
164             #   dict, use it.
165             # - The default value for the field instance as given in
166             #   the admin.
167             #
168             initial_val = None
169             try:
170                 initial_val = field_entries[field.id]
171             except KeyError:
172                 try:
173                     initial_val = initial[field_key]
174                 except KeyError:
175                     initial_val = Template(field.default).render(context)
176             if initial_val:
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)
183
184             if field.field_type == fields.DOB:
185                 now = datetime.now()
186                 years = list(range(now.year, now.year - 120, -1))
187                 self.fields[field_key].widget.years = years
188
189             # Add identifying CSS classes to the field.
190             css_class = field_class.__name__.lower()
191             if field.required:
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
200
201     def save(self, **kwargs):
202         """
203         Get/create a FormEntry instance and assign submitted values to
204         related FieldEntry instances for each form field.
205         """
206         entry = super(FormForForm, self).save(commit=False)
207         entry.form = self.form
208         entry.entry_time = now()
209         entry.save()
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
222                 field_entry.save()
223             else:
224                 new = {"entry": entry, "field_id": field.id, "value": value}
225                 new_entry_fields.append(self.field_entry_model(**new))
226         if new_entry_fields:
227             if django.VERSION >= (1, 4, 0):
228                 self.field_entry_model.objects.bulk_create(new_entry_fields)
229             else:
230                 for field_entry in new_entry_fields:
231                     field_entry.save()
232         return entry
233
234     def email_to(self):
235         """
236         Return the value entered for the first field of type EmailField.
237         """
238         for field in self.form_fields:
239             if field.is_a(fields.EMAIL):
240                 return self.cleaned_data[field.slug]
241         return None
242
243
244 class EntriesForm(forms.Form):
245     """
246     Form with a set of fields dynamically assigned that can be used to
247     filter entries for the given ``forms.models.Form`` instance.
248     """
249
250     def __init__(self, form, request, formentry_model=FormEntry,
251                  fieldentry_model=FieldEntry, *args, **kwargs):
252         """
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.
259         """
260         self.form = form
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")))
277                 else:
278                     choices = field.get_choices()
279                 contains_field = forms.MultipleChoiceField(label=" ",
280                     choices=choices, widget=forms.CheckboxSelectMultiple(),
281                     required=False)
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(),
290                     required=False)
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)
300             else:
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)
315
316     def __iter__(self):
317         """
318         Yield pairs of include checkbox / filters for each field.
319         """
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:]
325
326     def posted_data(self, field):
327         """
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
331         a form submission.
332         """
333         try:
334             return self.cleaned_data[field]
335         except (AttributeError, KeyError):
336             return field.endswith("_export")
337
338     def columns(self):
339         """
340         Returns the list of selected column names.
341         """
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)
346         return fields
347
348     def rows(self, csv=False):
349         """
350         Returns each row based on the selected criteria.
351         """
352
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
356         # their values.
357         field_indexes = {}
358         file_field_ids = []
359         date_field_ids = []
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:
370             num_columns += 1
371
372         # Get the field entries for the given form and filter by entry_time
373         # if specified.
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))
383
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.
387         current_entry = None
388         current_row = None
389         valid_row = True
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:
394                     if not csv:
395                         current_row.insert(0, current_entry)
396                     yield current_row
397                 current_entry = field_entry.entry_id
398                 current_row = [""] * num_columns
399                 valid_row = True
400                 if include_entry_time:
401                     current_row[-1] = field_entry.entry.entry_time
402             field_value = field_entry.value or ""
403             # Check for filter.
404             field_id = field_entry.field_id
405             filter_type = self.posted_data("field_%s_filter" % field_id)
406             filter_args = None
407             if filter_type:
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)]
411                 else:
412                     field_name = "field_%s_contains" % field_id
413                     filter_args = self.posted_data(field_name)
414                     if filter_args:
415                         filter_args = [filter_args]
416             if filter_args:
417                 # Convert dates before checking filter.
418                 if field_id in date_field_ids:
419                     try:
420                         y, m, d = field_value.split(" ")[0].split("-")
421                     except ValueError:
422                         filter_args.append(field_value)
423                     else:
424                         dte = date(int(y), int(m), int(d))
425                         filter_args.append(dte)
426                 else:
427                     filter_args.append(field_value)
428                 filter_func = FILTER_FUNCS[filter_type]
429                 if not filter_func(*filter_args):
430                     valid_row = False
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)
435                 if not csv:
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.
439             try:
440                 current_row[field_indexes[field_id]] = field_value
441             except KeyError:
442                 pass
443         # Output the final row.
444         if valid_row and current_row is not None:
445             if not csv:
446                 current_row.insert(0, current_entry)
447             yield current_row