2020-02-07 17:21:34 +00:00
|
|
|
# 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.
|
|
|
|
|
2020-02-12 12:10:41 +00:00
|
|
|
from collections import OrderedDict
|
2020-02-07 17:21:34 +00:00
|
|
|
from functools import reduce
|
2020-02-12 12:10:41 +00:00
|
|
|
from itertools import chain
|
2020-02-07 17:21:34 +00:00
|
|
|
from operator import add
|
|
|
|
|
2020-02-12 12:10:41 +00:00
|
|
|
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
|
|
|
from django.forms.utils import ErrorList
|
2020-02-07 17:21:34 +00:00
|
|
|
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.
|
|
|
|
"""
|
2020-02-07 17:46:34 +00:00
|
|
|
|
2020-02-07 17:21:34 +00:00
|
|
|
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(
|
2020-02-07 17:46:34 +00:00
|
|
|
data=data, files=files,
|
2020-02-07 17:21:34 +00:00
|
|
|
)
|
|
|
|
|
2020-02-07 17:46:34 +00:00
|
|
|
self.initials = kwargs.pop("initial", None)
|
2020-02-07 17:21:34 +00:00
|
|
|
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()
|
2020-02-07 17:46:34 +00:00
|
|
|
prefix = kwargs.get("prefix")
|
2020-02-07 17:21:34 +00:00
|
|
|
if prefix is None:
|
|
|
|
prefix = key
|
|
|
|
else:
|
2020-02-07 17:46:34 +00:00
|
|
|
prefix = "{0}__{1}".format(key, prefix)
|
2020-02-07 17:21:34 +00:00
|
|
|
fkwargs.update(
|
2020-02-07 17:46:34 +00:00
|
|
|
initial=self.initials.get(key), prefix=prefix,
|
2020-02-07 17:21:34 +00:00
|
|
|
)
|
|
|
|
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 = (
|
2020-02-07 17:46:34 +00:00
|
|
|
form.non_field_errors()
|
|
|
|
for form in self.forms.values()
|
|
|
|
if hasattr(form, "non_field_errors")
|
2020-02-07 17:21:34 +00:00
|
|
|
)
|
|
|
|
return ErrorList(chain(self.crossform_errors, *form_errors))
|
|
|
|
|
|
|
|
def as_table(self):
|
2020-02-07 17:46:34 +00:00
|
|
|
return mark_safe("".join(form.as_table() for form in self.forms.values()))
|
2020-02-07 17:21:34 +00:00
|
|
|
|
|
|
|
def as_ul(self):
|
2020-02-07 17:46:34 +00:00
|
|
|
return mark_safe("".join(form.as_ul() for form in self.forms.values()))
|
2020-02-07 17:21:34 +00:00
|
|
|
|
|
|
|
def as_p(self):
|
2020-02-07 17:46:34 +00:00
|
|
|
return mark_safe("".join(form.as_p() for form in self.forms.values()))
|
2020-02-07 17:21:34 +00:00
|
|
|
|
|
|
|
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)
|
2020-02-07 17:46:34 +00:00
|
|
|
for key, form in self.forms.items()
|
|
|
|
if form.is_valid()
|
2020-02-07 17:21:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
@cleaned_data.setter
|
|
|
|
def cleaned_data(self, data):
|
|
|
|
for key, value in data.items():
|
|
|
|
child_form = self[key]
|
2020-02-07 17:46:34 +00:00
|
|
|
if hasattr(child_form, "forms"):
|
2020-02-07 17:21:34 +00:00
|
|
|
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.
|
|
|
|
"""
|
2020-02-07 17:46:34 +00:00
|
|
|
|
2020-02-07 17:21:34 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
2020-02-07 17:46:34 +00:00
|
|
|
self.instances = kwargs.pop("instance", None)
|
2020-02-07 17:21:34 +00:00
|
|
|
if self.instances is None:
|
|
|
|
self.instances = {}
|
|
|
|
super(MultiModelForm, self).__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
def get_form_args_kwargs(self, key, args, kwargs):
|
2020-02-07 17:46:34 +00:00
|
|
|
fargs, fkwargs = super(MultiModelForm, self).get_form_args_kwargs(
|
|
|
|
key, args, kwargs
|
|
|
|
)
|
2020-02-07 17:21:34 +00:00
|
|
|
try:
|
|
|
|
# If we only pass instance when there was one specified, we make it
|
|
|
|
# possible to use non-ModelForms together with ModelForms.
|
2020-02-07 17:46:34 +00:00
|
|
|
fkwargs["instance"] = self.instances[key]
|
2020-02-07 17:21:34 +00:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
return fargs, fkwargs
|
|
|
|
|
|
|
|
def save(self, commit=True):
|
|
|
|
objects = OrderedDict(
|
2020-02-07 17:46:34 +00:00
|
|
|
(key, form.save(commit)) for key, form in self.forms.items()
|
2020-02-07 17:21:34 +00:00
|
|
|
)
|
|
|
|
|
2020-02-07 17:46:34 +00:00
|
|
|
if any(hasattr(form, "save_m2m") for form in self.forms.values()):
|
|
|
|
|
2020-02-07 17:21:34 +00:00
|
|
|
def save_m2m():
|
|
|
|
for form in self.forms.values():
|
2020-02-07 17:46:34 +00:00
|
|
|
if hasattr(form, "save_m2m"):
|
2020-02-07 17:21:34 +00:00
|
|
|
form.save_m2m()
|
2020-02-07 17:46:34 +00:00
|
|
|
|
2020-02-07 17:21:34 +00:00
|
|
|
self.save_m2m = save_m2m
|
|
|
|
|
|
|
|
return objects
|