forms.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. from __future__ import unicode_literals
  2. from future.builtins import int, range, str
  3. from datetime import date, datetime
  4. from os.path import join, split
  5. from uuid import uuid4
  6. from django import forms
  7. from django.forms.extras import SelectDateWidget
  8. from django.core.files.storage import FileSystemStorage
  9. from django.core.urlresolvers import reverse
  10. from django.template import Template
  11. from django.utils.safestring import mark_safe
  12. from django.utils.translation import ugettext as _
  13. from django.utils.timezone import now
  14. from mezzanine.conf import settings
  15. from mezzanine.forms import fields
  16. from mezzanine.forms.models import FormEntry, FieldEntry
  17. from mezzanine.utils.email import split_addresses as split_choices
  18. fs = FileSystemStorage(location=settings.FORMS_UPLOAD_ROOT)
  19. ##############################
  20. # Each type of export filter #
  21. ##############################
  22. # Text matches
  23. FILTER_CHOICE_CONTAINS = "1"
  24. FILTER_CHOICE_DOESNT_CONTAIN = "2"
  25. # Exact matches
  26. FILTER_CHOICE_EQUALS = "3"
  27. FILTER_CHOICE_DOESNT_EQUAL = "4"
  28. # Greater/less than
  29. FILTER_CHOICE_BETWEEN = "5"
  30. # Multiple values
  31. FILTER_CHOICE_CONTAINS_ANY = "6"
  32. FILTER_CHOICE_CONTAINS_ALL = "7"
  33. FILTER_CHOICE_DOESNT_CONTAIN_ANY = "8"
  34. FILTER_CHOICE_DOESNT_CONTAIN_ALL = "9"
  35. ##########################
  36. # Export filters grouped #
  37. ##########################
  38. # Text fields
  39. TEXT_FILTER_CHOICES = (
  40. ("", _("Nothing")),
  41. (FILTER_CHOICE_CONTAINS, _("Contains")),
  42. (FILTER_CHOICE_DOESNT_CONTAIN, _("Doesn't contain")),
  43. (FILTER_CHOICE_EQUALS, _("Equals")),
  44. (FILTER_CHOICE_DOESNT_EQUAL, _("Doesn't equal")),
  45. )
  46. # Choices with single value entries
  47. CHOICE_FILTER_CHOICES = (
  48. ("", _("Nothing")),
  49. (FILTER_CHOICE_CONTAINS_ANY, _("Equals any")),
  50. (FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't equal any")),
  51. )
  52. # Choices with multiple value entries
  53. MULTIPLE_FILTER_CHOICES = (
  54. ("", _("Nothing")),
  55. (FILTER_CHOICE_CONTAINS_ANY, _("Contains any")),
  56. (FILTER_CHOICE_CONTAINS_ALL, _("Contains all")),
  57. (FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't contain any")),
  58. (FILTER_CHOICE_DOESNT_CONTAIN_ALL, _("Doesn't contain all")),
  59. )
  60. # Dates
  61. DATE_FILTER_CHOICES = (
  62. ("", _("Nothing")),
  63. (FILTER_CHOICE_BETWEEN, _("Is between")),
  64. )
  65. # The filter function for each filter type
  66. FILTER_FUNCS = {
  67. FILTER_CHOICE_CONTAINS:
  68. lambda val, field: val.lower() in field.lower(),
  69. FILTER_CHOICE_DOESNT_CONTAIN:
  70. lambda val, field: val.lower() not in field.lower(),
  71. FILTER_CHOICE_EQUALS:
  72. lambda val, field: val.lower() == field.lower(),
  73. FILTER_CHOICE_DOESNT_EQUAL:
  74. lambda val, field: val.lower() != field.lower(),
  75. FILTER_CHOICE_BETWEEN:
  76. lambda val_from, val_to, field: (
  77. (not val_from or val_from <= field) and
  78. (not val_to or val_to >= field)
  79. ),
  80. FILTER_CHOICE_CONTAINS_ANY:
  81. lambda val, field: set(val) & set(split_choices(field)),
  82. FILTER_CHOICE_CONTAINS_ALL:
  83. lambda val, field: set(val) == set(split_choices(field)),
  84. FILTER_CHOICE_DOESNT_CONTAIN_ANY:
  85. lambda val, field: not set(val) & set(split_choices(field)),
  86. FILTER_CHOICE_DOESNT_CONTAIN_ALL:
  87. lambda val, field: set(val) != set(split_choices(field)),
  88. }
  89. # Export form fields for each filter type grouping
  90. text_filter_field = forms.ChoiceField(label=" ", required=False,
  91. choices=TEXT_FILTER_CHOICES)
  92. choice_filter_field = forms.ChoiceField(label=" ", required=False,
  93. choices=CHOICE_FILTER_CHOICES)
  94. multiple_filter_field = forms.ChoiceField(label=" ", required=False,
  95. choices=MULTIPLE_FILTER_CHOICES)
  96. date_filter_field = forms.ChoiceField(label=" ", required=False,
  97. choices=DATE_FILTER_CHOICES)
  98. class FormForForm(forms.ModelForm):
  99. """
  100. Form with a set of fields dynamically assigned, directly based on the
  101. given ``forms.models.Form`` instance.
  102. """
  103. class Meta:
  104. model = FormEntry
  105. exclude = ("form", "entry_time")
  106. def __init__(self, form, context, *args, **kwargs):
  107. """
  108. Dynamically add each of the form fields for the given form model
  109. instance and its related field model instances.
  110. """
  111. self.form = form
  112. self.form_fields = form.fields.visible()
  113. initial = kwargs.pop("initial", {})
  114. # If a FormEntry instance is given to edit, populate initial
  115. # with its field values.
  116. field_entries = {}
  117. if kwargs.get("instance"):
  118. for field_entry in kwargs["instance"].fields.all():
  119. field_entries[field_entry.field_id] = field_entry.value
  120. super(FormForForm, self).__init__(*args, **kwargs)
  121. # Create the form fields.
  122. for field in self.form_fields:
  123. field_key = "field_%s" % field.id
  124. field_class = fields.CLASSES[field.field_type]
  125. field_widget = fields.WIDGETS.get(field.field_type)
  126. field_args = {"label": field.label, "required": field.required,
  127. "help_text": field.help_text}
  128. arg_names = field_class.__init__.__code__.co_varnames
  129. if "max_length" in arg_names:
  130. field_args["max_length"] = settings.FORMS_FIELD_MAX_LENGTH
  131. if "choices" in arg_names:
  132. choices = list(field.get_choices())
  133. if (field.field_type == fields.SELECT and
  134. field.default not in [c[0] for c in choices]):
  135. choices.insert(0, ("", field.placeholder_text))
  136. field_args["choices"] = choices
  137. if field_widget is not None:
  138. field_args["widget"] = field_widget
  139. #
  140. # Initial value for field, in order of preference:
  141. #
  142. # - If a form model instance is given (eg we're editing a
  143. # form response), then use the instance's value for the
  144. # field.
  145. # - If the developer has provided an explicit "initial"
  146. # dict, use it.
  147. # - The default value for the field instance as given in
  148. # the admin.
  149. #
  150. initial_val = None
  151. try:
  152. initial_val = field_entries[field.id]
  153. except KeyError:
  154. try:
  155. initial_val = initial[field_key]
  156. except KeyError:
  157. initial_val = Template(field.default).render(context)
  158. if initial_val:
  159. if field.is_a(*fields.MULTIPLE):
  160. initial_val = split_choices(initial_val)
  161. elif field.field_type == fields.CHECKBOX:
  162. initial_val = initial_val != "False"
  163. self.initial[field_key] = initial_val
  164. self.fields[field_key] = field_class(**field_args)
  165. if field.field_type == fields.DOB:
  166. _now = datetime.now()
  167. years = list(range(_now.year, _now.year - 120, -1))
  168. self.fields[field_key].widget.years = years
  169. # Add identifying type attr to the field for styling.
  170. setattr(self.fields[field_key], "type",
  171. field_class.__name__.lower())
  172. if (field.required and settings.FORMS_USE_HTML5 and
  173. field.field_type != fields.CHECKBOX_MULTIPLE):
  174. self.fields[field_key].widget.attrs["required"] = ""
  175. if field.placeholder_text and not field.default:
  176. text = field.placeholder_text
  177. self.fields[field_key].widget.attrs["placeholder"] = text
  178. def save(self, **kwargs):
  179. """
  180. Create a ``FormEntry`` instance and related ``FieldEntry``
  181. instances for each form field.
  182. """
  183. entry = super(FormForForm, self).save(commit=False)
  184. entry.form = self.form
  185. entry.entry_time = now()
  186. entry.save()
  187. entry_fields = entry.fields.values_list("field_id", flat=True)
  188. new_entry_fields = []
  189. for field in self.form_fields:
  190. field_key = "field_%s" % field.id
  191. value = self.cleaned_data[field_key]
  192. if value and self.fields[field_key].widget.needs_multipart_form:
  193. value = fs.save(join("forms", str(uuid4()), value.name), value)
  194. if isinstance(value, list):
  195. value = ", ".join([v.strip() for v in value])
  196. if field.id in entry_fields:
  197. field_entry = entry.fields.get(field_id=field.id)
  198. field_entry.value = value
  199. field_entry.save()
  200. else:
  201. new = {"entry": entry, "field_id": field.id, "value": value}
  202. new_entry_fields.append(FieldEntry(**new))
  203. if new_entry_fields:
  204. FieldEntry.objects.bulk_create(new_entry_fields)
  205. return entry
  206. def email_to(self):
  207. """
  208. Return the value entered for the first field of type
  209. ``forms.EmailField``.
  210. """
  211. for field in self.form_fields:
  212. if issubclass(fields.CLASSES[field.field_type], forms.EmailField):
  213. return self.cleaned_data["field_%s" % field.id]
  214. return None
  215. class EntriesForm(forms.Form):
  216. """
  217. Form with a set of fields dynamically assigned that can be used to
  218. filter entries for the given ``forms.models.Form`` instance.
  219. """
  220. def __init__(self, form, request, *args, **kwargs):
  221. """
  222. Iterate through the fields of the ``forms.models.Form`` instance and
  223. create the form fields required to control including the field in
  224. the export (with a checkbox) or filtering the field which differs
  225. across field types. User a list of checkboxes when a fixed set of
  226. choices can be chosen from, a pair of date fields for date ranges,
  227. and for all other types provide a textbox for text search.
  228. """
  229. self.form = form
  230. self.request = request
  231. self.form_fields = form.fields.all()
  232. self.entry_time_name = str(FormEntry._meta.get_field(
  233. "entry_time").verbose_name)
  234. super(EntriesForm, self).__init__(*args, **kwargs)
  235. for field in self.form_fields:
  236. field_key = "field_%s" % field.id
  237. # Checkbox for including in export.
  238. self.fields["%s_export" % field_key] = forms.BooleanField(
  239. label=field.label, initial=True, required=False)
  240. if field.is_a(*fields.CHOICES):
  241. # A fixed set of choices to filter by.
  242. if field.is_a(fields.CHECKBOX):
  243. choices = ((True, _("Checked")), (False, _("Not checked")))
  244. else:
  245. choices = field.get_choices()
  246. contains_field = forms.MultipleChoiceField(label=" ",
  247. choices=choices, widget=forms.CheckboxSelectMultiple(),
  248. required=False)
  249. self.fields["%s_filter" % field_key] = choice_filter_field
  250. self.fields["%s_contains" % field_key] = contains_field
  251. elif field.is_a(*fields.MULTIPLE):
  252. # A fixed set of choices to filter by, with multiple
  253. # possible values in the entry field.
  254. contains_field = forms.MultipleChoiceField(label=" ",
  255. choices=field.get_choices(),
  256. widget=forms.CheckboxSelectMultiple(),
  257. required=False)
  258. self.fields["%s_filter" % field_key] = multiple_filter_field
  259. self.fields["%s_contains" % field_key] = contains_field
  260. elif field.is_a(*fields.DATES):
  261. # A date range to filter by.
  262. self.fields["%s_filter" % field_key] = date_filter_field
  263. self.fields["%s_from" % field_key] = forms.DateField(
  264. label=" ", widget=SelectDateWidget(), required=False)
  265. self.fields["%s_to" % field_key] = forms.DateField(
  266. label=_("and"), widget=SelectDateWidget(), required=False)
  267. else:
  268. # Text box for search term to filter by.
  269. contains_field = forms.CharField(label=" ", required=False)
  270. self.fields["%s_filter" % field_key] = text_filter_field
  271. self.fields["%s_contains" % field_key] = contains_field
  272. # Add ``FormEntry.entry_time`` as a field.
  273. field_key = "field_0"
  274. self.fields["%s_export" % field_key] = forms.BooleanField(initial=True,
  275. label=FormEntry._meta.get_field("entry_time").verbose_name,
  276. required=False)
  277. self.fields["%s_filter" % field_key] = date_filter_field
  278. self.fields["%s_from" % field_key] = forms.DateField(
  279. label=" ", widget=SelectDateWidget(), required=False)
  280. self.fields["%s_to" % field_key] = forms.DateField(
  281. label=_("and"), widget=SelectDateWidget(), required=False)
  282. def __iter__(self):
  283. """
  284. Yield pairs of include checkbox / filters for each field.
  285. """
  286. for field_id in [f.id for f in self.form_fields] + [0]:
  287. prefix = "field_%s_" % field_id
  288. fields = [f for f in super(EntriesForm, self).__iter__()
  289. if f.name.startswith(prefix)]
  290. yield fields[0], fields[1], fields[2:]
  291. def columns(self):
  292. """
  293. Returns the list of selected column names.
  294. """
  295. fields = [f.label for f in self.form_fields
  296. if self.cleaned_data["field_%s_export" % f.id]]
  297. if self.cleaned_data["field_0_export"]:
  298. fields.append(self.entry_time_name)
  299. return fields
  300. def rows(self, csv=False):
  301. """
  302. Returns each row based on the selected criteria.
  303. """
  304. # Store the index of each field against its ID for building each
  305. # entry row with columns in the correct order. Also store the IDs of
  306. # fields with a type of FileField or Date-like for special handling of
  307. # their values.
  308. field_indexes = {}
  309. file_field_ids = []
  310. date_field_ids = []
  311. for field in self.form_fields:
  312. if self.cleaned_data["field_%s_export" % field.id]:
  313. field_indexes[field.id] = len(field_indexes)
  314. if field.is_a(fields.FILE):
  315. file_field_ids.append(field.id)
  316. elif field.is_a(*fields.DATES):
  317. date_field_ids.append(field.id)
  318. num_columns = len(field_indexes)
  319. include_entry_time = self.cleaned_data["field_0_export"]
  320. if include_entry_time:
  321. num_columns += 1
  322. # Get the field entries for the given form and filter by entry_time
  323. # if specified.
  324. field_entries = FieldEntry.objects.filter(
  325. entry__form=self.form).order_by(
  326. "-entry__id").select_related("entry")
  327. if self.cleaned_data["field_0_filter"] == FILTER_CHOICE_BETWEEN:
  328. time_from = self.cleaned_data["field_0_from"]
  329. time_to = self.cleaned_data["field_0_to"]
  330. if time_from and time_to:
  331. field_entries = field_entries.filter(
  332. entry__entry_time__range=(time_from, time_to))
  333. # Loop through each field value ordered by entry, building up each
  334. # entry as a row. Use the ``valid_row`` flag for marking a row as
  335. # invalid if it fails one of the filtering criteria specified.
  336. current_entry = None
  337. current_row = None
  338. valid_row = True
  339. for field_entry in field_entries:
  340. if field_entry.entry_id != current_entry:
  341. # New entry, write out the current row and start a new one.
  342. if valid_row and current_row is not None:
  343. if not csv:
  344. current_row.insert(0, current_entry)
  345. yield current_row
  346. current_entry = field_entry.entry_id
  347. current_row = [""] * num_columns
  348. valid_row = True
  349. if include_entry_time:
  350. current_row[-1] = field_entry.entry.entry_time
  351. field_value = field_entry.value or ""
  352. # Check for filter.
  353. field_id = field_entry.field_id
  354. filter_type = self.cleaned_data.get("field_%s_filter" % field_id)
  355. filter_args = None
  356. if filter_type:
  357. if filter_type == FILTER_CHOICE_BETWEEN:
  358. f, t = "field_%s_from" % field_id, "field_%s_to" % field_id
  359. filter_args = [self.cleaned_data[f], self.cleaned_data[t]]
  360. else:
  361. field_name = "field_%s_contains" % field_id
  362. filter_args = self.cleaned_data[field_name]
  363. if filter_args:
  364. filter_args = [filter_args]
  365. if filter_args:
  366. # Convert dates before checking filter.
  367. if field_id in date_field_ids:
  368. y, m, d = field_value.split(" ")[0].split("-")
  369. dte = date(int(y), int(m), int(d))
  370. filter_args.append(dte)
  371. else:
  372. filter_args.append(field_value)
  373. filter_func = FILTER_FUNCS[filter_type]
  374. if not filter_func(*filter_args):
  375. valid_row = False
  376. # Create download URL for file fields.
  377. if field_entry.value and field_id in file_field_ids:
  378. url = reverse("admin:form_file", args=(field_entry.id,))
  379. field_value = self.request.build_absolute_uri(url)
  380. if not csv:
  381. parts = (field_value, split(field_entry.value)[1])
  382. field_value = mark_safe("<a href=\"%s\">%s</a>" % parts)
  383. # Only use values for fields that were selected.
  384. try:
  385. current_row[field_indexes[field_id]] = field_value
  386. except KeyError:
  387. pass
  388. # Output the final row.
  389. if valid_row and current_row is not None:
  390. if not csv:
  391. current_row.insert(0, current_entry)
  392. yield current_row