Source code for mezzanine.forms.forms
from __future__ import unicode_literals
from future.builtins import int, range, str
from datetime import date, datetime
from os.path import join, split
from uuid import uuid4
from django import forms
try:
from django.forms.widgets import SelectDateWidget
except ImportError:
# Django 1.8
from django.forms.extras.widgets import SelectDateWidget
from django.core.files.storage import FileSystemStorage
from django.urls import reverse
from django.template import Template
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from django.utils.timezone import now
from mezzanine.conf import settings
from mezzanine.forms import fields
from mezzanine.forms.models import FormEntry, FieldEntry
from mezzanine.utils.email import split_addresses as split_choices
fs = FileSystemStorage(location=settings.FORMS_UPLOAD_ROOT)
##############################
# Each type of export filter #
##############################
# Text matches
FILTER_CHOICE_CONTAINS = "1"
FILTER_CHOICE_DOESNT_CONTAIN = "2"
# Exact matches
FILTER_CHOICE_EQUALS = "3"
FILTER_CHOICE_DOESNT_EQUAL = "4"
# Greater/less than
FILTER_CHOICE_BETWEEN = "5"
# Multiple values
FILTER_CHOICE_CONTAINS_ANY = "6"
FILTER_CHOICE_CONTAINS_ALL = "7"
FILTER_CHOICE_DOESNT_CONTAIN_ANY = "8"
FILTER_CHOICE_DOESNT_CONTAIN_ALL = "9"
##########################
# Export filters grouped #
##########################
# Text fields
TEXT_FILTER_CHOICES = (
("", _("Nothing")),
(FILTER_CHOICE_CONTAINS, _("Contains")),
(FILTER_CHOICE_DOESNT_CONTAIN, _("Doesn't contain")),
(FILTER_CHOICE_EQUALS, _("Equals")),
(FILTER_CHOICE_DOESNT_EQUAL, _("Doesn't equal")),
)
# Choices with single value entries
CHOICE_FILTER_CHOICES = (
("", _("Nothing")),
(FILTER_CHOICE_CONTAINS_ANY, _("Equals any")),
(FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't equal any")),
)
# Choices with multiple value entries
MULTIPLE_FILTER_CHOICES = (
("", _("Nothing")),
(FILTER_CHOICE_CONTAINS_ANY, _("Contains any")),
(FILTER_CHOICE_CONTAINS_ALL, _("Contains all")),
(FILTER_CHOICE_DOESNT_CONTAIN_ANY, _("Doesn't contain any")),
(FILTER_CHOICE_DOESNT_CONTAIN_ALL, _("Doesn't contain all")),
)
# Dates
DATE_FILTER_CHOICES = (
("", _("Nothing")),
(FILTER_CHOICE_BETWEEN, _("Is between")),
)
# The filter function for each filter type
FILTER_FUNCS = {
FILTER_CHOICE_CONTAINS:
lambda val, field: val.lower() in field.lower(),
FILTER_CHOICE_DOESNT_CONTAIN:
lambda val, field: val.lower() not in field.lower(),
FILTER_CHOICE_EQUALS:
lambda val, field: val.lower() == field.lower(),
FILTER_CHOICE_DOESNT_EQUAL:
lambda val, field: val.lower() != field.lower(),
FILTER_CHOICE_BETWEEN:
lambda val_from, val_to, field: (
(not val_from or val_from <= field) and
(not val_to or val_to >= field)
),
FILTER_CHOICE_CONTAINS_ANY:
lambda val, field: set(val) & set(split_choices(field)),
FILTER_CHOICE_CONTAINS_ALL:
lambda val, field: set(val) == set(split_choices(field)),
FILTER_CHOICE_DOESNT_CONTAIN_ANY:
lambda val, field: not set(val) & set(split_choices(field)),
FILTER_CHOICE_DOESNT_CONTAIN_ALL:
lambda val, field: set(val) != set(split_choices(field)),
}
# Export form fields for each filter type grouping
text_filter_field = forms.ChoiceField(label=" ", required=False,
choices=TEXT_FILTER_CHOICES)
choice_filter_field = forms.ChoiceField(label=" ", required=False,
choices=CHOICE_FILTER_CHOICES)
multiple_filter_field = forms.ChoiceField(label=" ", required=False,
choices=MULTIPLE_FILTER_CHOICES)
date_filter_field = forms.ChoiceField(label=" ", required=False,
choices=DATE_FILTER_CHOICES)
[docs]class FormForForm(forms.ModelForm):
"""
Form with a set of fields dynamically assigned, directly based on the
given ``forms.models.Form`` instance.
"""
class Meta:
model = FormEntry
exclude = ("form", "entry_time")
def __init__(self, form, context, *args, **kwargs):
"""
Dynamically add each of the form fields for the given form model
instance and its related field model instances.
"""
self.form = form
self.form_fields = form.fields.visible()
initial = kwargs.pop("initial", {})
# If a FormEntry instance is given to edit, populate initial
# with its field values.
field_entries = {}
if kwargs.get("instance"):
for field_entry in kwargs["instance"].fields.all():
field_entries[field_entry.field_id] = field_entry.value
super(FormForForm, self).__init__(*args, **kwargs)
# Create the form fields.
for field in self.form_fields:
field_key = "field_%s" % field.id
field_class = fields.CLASSES[field.field_type]
field_widget = fields.WIDGETS.get(field.field_type)
field_args = {"label": field.label, "required": field.required,
"help_text": field.help_text}
arg_names = field_class.__init__.__code__.co_varnames
if "max_length" in arg_names:
field_args["max_length"] = settings.FORMS_FIELD_MAX_LENGTH
if "choices" in arg_names:
choices = list(field.get_choices())
if (field.field_type == fields.SELECT and
field.default not in [c[0] for c in choices]):
choices.insert(0, ("", field.placeholder_text))
field_args["choices"] = choices
if field_widget is not None:
field_args["widget"] = field_widget
#
# Initial value for field, in order of preference:
#
# - If a form model instance is given (eg we're editing a
# form response), then use the instance's value for the
# field.
# - If the developer has provided an explicit "initial"
# dict, use it.
# - The default value for the field instance as given in
# the admin.
#
initial_val = None
try:
initial_val = field_entries[field.id]
except KeyError:
try:
initial_val = initial[field_key]
except KeyError:
initial_val = str(Template(field.default).render(context))
if initial_val:
if field.is_a(*fields.MULTIPLE):
initial_val = split_choices(initial_val)
elif field.field_type == fields.CHECKBOX:
initial_val = initial_val != "False"
self.initial[field_key] = initial_val
self.fields[field_key] = field_class(**field_args)
if field.field_type == fields.DOB:
_now = datetime.now()
years = list(range(_now.year, _now.year - 120, -1))
self.fields[field_key].widget.years = years
# Add identifying type attr to the field for styling.
setattr(self.fields[field_key], "type",
field_class.__name__.lower())
if (field.required and settings.FORMS_USE_HTML5 and
field.field_type != fields.CHECKBOX_MULTIPLE):
self.fields[field_key].widget.attrs["required"] = ""
if field.placeholder_text and not field.default:
text = field.placeholder_text
self.fields[field_key].widget.attrs["placeholder"] = text
[docs] def save(self, **kwargs):
"""
Create a ``FormEntry`` instance and related ``FieldEntry``
instances for each form field.
"""
entry = super(FormForForm, self).save(commit=False)
entry.form = self.form
entry.entry_time = now()
entry.save()
entry_fields = entry.fields.values_list("field_id", flat=True)
new_entry_fields = []
for field in self.form_fields:
field_key = "field_%s" % field.id
value = self.cleaned_data[field_key]
if value and self.fields[field_key].widget.needs_multipart_form:
value = fs.save(join("forms", str(uuid4()), value.name), value)
if isinstance(value, list):
value = ", ".join([v.strip() for v in value])
if field.id in entry_fields:
field_entry = entry.fields.get(field_id=field.id)
field_entry.value = value
field_entry.save()
else:
new = {"entry": entry, "field_id": field.id, "value": value}
new_entry_fields.append(FieldEntry(**new))
if new_entry_fields:
FieldEntry.objects.bulk_create(new_entry_fields)
return entry
[docs] def email_to(self):
"""
Return the value entered for the first field of type
``forms.EmailField``.
"""
for field in self.form_fields:
if issubclass(fields.CLASSES[field.field_type], forms.EmailField):
return self.cleaned_data["field_%s" % field.id]
return None
[docs]class EntriesForm(forms.Form):
"""
Form with a set of fields dynamically assigned that can be used to
filter entries for the given ``forms.models.Form`` instance.
"""
def __init__(self, form, request, *args, **kwargs):
"""
Iterate through the fields of the ``forms.models.Form`` instance and
create the form fields required to control including the field in
the export (with a checkbox) or filtering the field which differs
across field types. User a list of checkboxes when a fixed set of
choices can be chosen from, a pair of date fields for date ranges,
and for all other types provide a textbox for text search.
"""
self.form = form
self.request = request
self.form_fields = form.fields.all()
self.entry_time_name = str(FormEntry._meta.get_field(
"entry_time").verbose_name)
super(EntriesForm, self).__init__(*args, **kwargs)
for field in self.form_fields:
field_key = "field_%s" % field.id
# Checkbox for including in export.
self.fields["%s_export" % field_key] = forms.BooleanField(
label=field.label, initial=True, required=False)
if field.is_a(*fields.CHOICES):
# A fixed set of choices to filter by.
if field.is_a(fields.CHECKBOX):
choices = ((True, _("Checked")), (False, _("Not checked")))
else:
choices = field.get_choices()
contains_field = forms.MultipleChoiceField(label=" ",
choices=choices, widget=forms.CheckboxSelectMultiple(),
required=False)
self.fields["%s_filter" % field_key] = choice_filter_field
self.fields["%s_contains" % field_key] = contains_field
elif field.is_a(*fields.MULTIPLE):
# A fixed set of choices to filter by, with multiple
# possible values in the entry field.
contains_field = forms.MultipleChoiceField(label=" ",
choices=field.get_choices(),
widget=forms.CheckboxSelectMultiple(),
required=False)
self.fields["%s_filter" % field_key] = multiple_filter_field
self.fields["%s_contains" % field_key] = contains_field
elif field.is_a(*fields.DATES):
# A date range to filter by.
self.fields["%s_filter" % field_key] = date_filter_field
self.fields["%s_from" % field_key] = forms.DateField(
label=" ", widget=SelectDateWidget(), required=False)
self.fields["%s_to" % field_key] = forms.DateField(
label=_("and"), widget=SelectDateWidget(), required=False)
else:
# Text box for search term to filter by.
contains_field = forms.CharField(label=" ", required=False)
self.fields["%s_filter" % field_key] = text_filter_field
self.fields["%s_contains" % field_key] = contains_field
# Add ``FormEntry.entry_time`` as a field.
field_key = "field_0"
self.fields["%s_export" % field_key] = forms.BooleanField(initial=True,
label=FormEntry._meta.get_field("entry_time").verbose_name,
required=False)
self.fields["%s_filter" % field_key] = date_filter_field
self.fields["%s_from" % field_key] = forms.DateField(
label=" ", widget=SelectDateWidget(), required=False)
self.fields["%s_to" % field_key] = forms.DateField(
label=_("and"), widget=SelectDateWidget(), required=False)
def __iter__(self):
"""
Yield pairs of include checkbox / filters for each field.
"""
for field_id in [f.id for f in self.form_fields] + [0]:
prefix = "field_%s_" % field_id
fields = [f for f in super(EntriesForm, self).__iter__()
if f.name.startswith(prefix)]
yield fields[0], fields[1], fields[2:]
[docs] def columns(self):
"""
Returns the list of selected column names.
"""
fields = [f.label for f in self.form_fields
if self.cleaned_data["field_%s_export" % f.id]]
if self.cleaned_data["field_0_export"]:
fields.append(self.entry_time_name)
return fields
[docs] def rows(self, csv=False):
"""
Returns each row based on the selected criteria.
"""
# Store the index of each field against its ID for building each
# entry row with columns in the correct order. Also store the IDs of
# fields with a type of FileField or Date-like for special handling of
# their values.
field_indexes = {}
file_field_ids = []
date_field_ids = []
for field in self.form_fields:
if self.cleaned_data["field_%s_export" % field.id]:
field_indexes[field.id] = len(field_indexes)
if field.is_a(fields.FILE):
file_field_ids.append(field.id)
elif field.is_a(*fields.DATES):
date_field_ids.append(field.id)
num_columns = len(field_indexes)
include_entry_time = self.cleaned_data["field_0_export"]
if include_entry_time:
num_columns += 1
# Get the field entries for the given form and filter by entry_time
# if specified.
field_entries = FieldEntry.objects.filter(
entry__form=self.form).order_by(
"-entry__id").select_related("entry")
if self.cleaned_data["field_0_filter"] == FILTER_CHOICE_BETWEEN:
time_from = self.cleaned_data["field_0_from"]
time_to = self.cleaned_data["field_0_to"]
if time_from and time_to:
field_entries = field_entries.filter(
entry__entry_time__range=(time_from, time_to))
# Loop through each field value ordered by entry, building up each
# entry as a row. Use the ``valid_row`` flag for marking a row as
# invalid if it fails one of the filtering criteria specified.
current_entry = None
current_row = None
valid_row = True
for field_entry in field_entries:
if field_entry.entry_id != current_entry:
# New entry, write out the current row and start a new one.
if valid_row and current_row is not None:
if not csv:
current_row.insert(0, current_entry)
yield current_row
current_entry = field_entry.entry_id
current_row = [""] * num_columns
valid_row = True
if include_entry_time:
current_row[-1] = field_entry.entry.entry_time
field_value = field_entry.value or ""
# Check for filter.
field_id = field_entry.field_id
filter_type = self.cleaned_data.get("field_%s_filter" % field_id)
filter_args = None
if filter_type:
if filter_type == FILTER_CHOICE_BETWEEN:
f, t = "field_%s_from" % field_id, "field_%s_to" % field_id
filter_args = [self.cleaned_data[f], self.cleaned_data[t]]
else:
field_name = "field_%s_contains" % field_id
filter_args = self.cleaned_data[field_name]
if filter_args:
filter_args = [filter_args]
if filter_args:
# Convert dates before checking filter.
if field_id in date_field_ids:
y, m, d = field_value.split(" ")[0].split("-")
dte = date(int(y), int(m), int(d))
filter_args.append(dte)
else:
filter_args.append(field_value)
filter_func = FILTER_FUNCS[filter_type]
if not filter_func(*filter_args):
valid_row = False
# Create download URL for file fields.
if field_entry.value and field_id in file_field_ids:
url = reverse("admin:form_file", args=(field_entry.id,))
field_value = self.request.build_absolute_uri(url)
if not csv:
parts = (field_value, split(field_entry.value)[1])
field_value = mark_safe("<a href=\"%s\">%s</a>" % parts)
# Only use values for fields that were selected.
try:
current_row[field_indexes[field_id]] = field_value
except KeyError:
pass
# Output the final row.
if valid_row and current_row is not None:
if not csv:
current_row.insert(0, current_entry)
yield current_row