bornhack-website/src/program/multiform.py

227 lines
7.7 KiB
Python

# Copied from https://github.com/fusionbox/django-betterforms/blob/master/betterforms/multiform.py
#
# From https://github.com/fusionbox/django-betterforms/blob/master/LICENSE
#
# Copyright (c) 2013, Fusionbox, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# - Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from collections import OrderedDict
from functools import reduce
from itertools import chain
from operator import add
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.forms.utils import ErrorList
from django.utils.safestring import mark_safe
class MultiForm(object):
"""
A container that allows you to treat multiple forms as one form. This is
great for using more than one form on a page that share the same submit
button. MultiForm imitates the Form API so that it is invisible to anybody
else that you are using a MultiForm.
"""
form_classes = {}
def __init__(self, data=None, files=None, *args, **kwargs):
# Some things, such as the WizardView expect these to exist.
self.data, self.files = data, files
kwargs.update(
data=data,
files=files,
)
self.initials = kwargs.pop("initial", None)
if self.initials is None:
self.initials = {}
self.forms = OrderedDict()
self.crossform_errors = []
for key, form_class in self.form_classes.items():
fargs, fkwargs = self.get_form_args_kwargs(key, args, kwargs)
self.forms[key] = form_class(*fargs, **fkwargs)
def get_form_args_kwargs(self, key, args, kwargs):
"""
Returns the args and kwargs for initializing one of our form children.
"""
fkwargs = kwargs.copy()
prefix = kwargs.get("prefix")
if prefix is None:
prefix = key
else:
prefix = "{0}__{1}".format(key, prefix)
fkwargs.update(
initial=self.initials.get(key),
prefix=prefix,
)
return args, fkwargs
def __str__(self):
return self.as_table()
def __getitem__(self, key):
return self.forms[key]
@property
def errors(self):
errors = {}
for form_name in self.forms:
form = self.forms[form_name]
for field_name in form.errors:
errors[form.add_prefix(field_name)] = form.errors[field_name]
if self.crossform_errors:
errors[NON_FIELD_ERRORS] = self.crossform_errors
return errors
@property
def fields(self):
fields = []
for form_name in self.forms:
form = self.forms[form_name]
for field_name in form.fields:
fields += [form.add_prefix(field_name)]
return fields
def __iter__(self):
# TODO: Should the order of the fields be controllable from here?
return chain.from_iterable(self.forms.values())
@property
def is_bound(self):
return any(form.is_bound for form in self.forms.values())
def clean(self):
"""
Raises any ValidationErrors required for cross form validation. Should
return a dict of cleaned_data objects for any forms whose data should
be overridden.
"""
return self.cleaned_data
def add_crossform_error(self, e):
self.crossform_errors.append(e)
def is_valid(self):
forms_valid = all(form.is_valid() for form in self.forms.values())
try:
self.cleaned_data = self.clean()
except ValidationError as e:
self.add_crossform_error(e)
return forms_valid and not self.crossform_errors
def non_field_errors(self):
form_errors = (
form.non_field_errors()
for form in self.forms.values()
if hasattr(form, "non_field_errors")
)
return ErrorList(chain(self.crossform_errors, *form_errors))
def as_table(self):
return mark_safe("".join(form.as_table() for form in self.forms.values()))
def as_ul(self):
return mark_safe("".join(form.as_ul() for form in self.forms.values()))
def as_p(self):
return mark_safe("".join(form.as_p() for form in self.forms.values()))
def is_multipart(self):
return any(form.is_multipart() for form in self.forms.values())
@property
def media(self):
return reduce(add, (form.media for form in self.forms.values()))
def hidden_fields(self):
# copy implementation instead of delegating in case we ever
# want to override the field ordering.
return [field for field in self if field.is_hidden]
def visible_fields(self):
return [field for field in self if not field.is_hidden]
@property
def cleaned_data(self):
return OrderedDict(
(key, form.cleaned_data)
for key, form in self.forms.items()
if form.is_valid()
)
@cleaned_data.setter
def cleaned_data(self, data):
for key, value in data.items():
child_form = self[key]
if hasattr(child_form, "forms"):
for formlet, formlet_data in zip(child_form.forms, value):
formlet.cleaned_data = formlet_data
else:
child_form.cleaned_data = value
class MultiModelForm(MultiForm):
"""
MultiModelForm adds ModelForm support on top of MultiForm. That simply
means that it includes support for the instance parameter in initialization
and adds a save method.
"""
def __init__(self, *args, **kwargs):
self.instances = kwargs.pop("instance", None)
if self.instances is None:
self.instances = {}
super(MultiModelForm, self).__init__(*args, **kwargs)
def get_form_args_kwargs(self, key, args, kwargs):
fargs, fkwargs = super(MultiModelForm, self).get_form_args_kwargs(
key, args, kwargs
)
try:
# If we only pass instance when there was one specified, we make it
# possible to use non-ModelForms together with ModelForms.
fkwargs["instance"] = self.instances[key]
except KeyError:
pass
return fargs, fkwargs
def save(self, commit=True):
objects = OrderedDict(
(key, form.save(commit)) for key, form in self.forms.items()
)
if any(hasattr(form, "save_m2m") for form in self.forms.values()):
def save_m2m():
for form in self.forms.values():
if hasattr(form, "save_m2m"):
form.save_m2m()
self.save_m2m = save_m2m
return objects