Package: django-shapeshifter
# django-shapeshifterA common problem in Django is how to have a view, especially a class-based viewthat can display and process multiple forms at once. `django-shapeshifter` aimsto make this problem much more trivial.Right now, `django-shapeshifter` can handle any (well, theoretically) number offorms in a single view. A view class is provided for multiple standard formsor model forms. To mix and match these form types, you'll need to do a littleextra work. Here's how to use the package:## Installation`$ pip install django-shapeshifter`You should not need to add `shapeshifter` to your `INSTALLED_APPS`.## UsageYou use `django-shapeshifter` just like you use Django's built-in class-basedviews. You should be able to use the provided views with most mixins you'realready using in your project, such as `LoginRequiredMixin`. Certain mixins may have to be refactored, such as `SuccessMessageMixin`, which is trigged on the `form_valid()` method.Let's look at using the view with a few standard forms:*interests/views.py*```pythonfrom django.urls import reverse_lazyfrom shapeshifter.views import MultiFormViewfrom . import formsclass InterestFormsView(MultiFormView):form_classes = (forms.ContactForm, forms.InterestsForm, forms.GDPRForm)template_name = 'interests/forms.html'success_url = reverse_lazy('interests:thanks')```But what do you need to do in the template? The view's context will containa new member, `forms`, that you can iterate over to display each form:*interests/templates/interests/forms.html*```html{% extends 'layout.html' %}{% block content %}<h3>Please fill out your interests below!</h3><form method="POST">{% csrf_token %}{% for form in forms %}{{ form.as_p }}{% endfor %}<input type="submit" value="Save" /></form>{% endblock content %}```This will generate a template with all three forms, in succession, inside of asingle `<form>` tag. **All of the forms must be submitted together.** Aftersubmission, Django will fill each form in with the appropriate submitted data,validate them, and then redirect to your `success_url`.But with just the above code, nothing will happen with the form data. To controlthat, you need to override the `forms_valid` method in your view. Here's whatthat might look like:*interests/views.py*```pythonclass InterestsFormView(MultiFormView):...def forms_valid(self):forms = self.get_forms()contact_form = forms['contactform']interest_form = forms['interestsform']gdpr = forms['gdprform']if not gdpr.data['accept']:messages.error("You must accept the GDPR terms.")return HttpResponseRedirect(reverse_lazy('interests:forms'))salesforce_client.send(zip(contact_form.data.items(),interest_form.data.items()))return super().forms_valid()```The above code isn't meant to be a complete example but should give you an ideaof what would be done to handle the form data.### What about model forms?All of the above code is valid for model forms, too, with one exception. Formodel forms, instead of extending `MultiFormView`, you'll extend`MultiModelFormView`. There are two major differences between the classes butthe most important one is that `forms_valid` will call `form.save()` on eachform. Here is an example allowing a user to edit their `User` `first_name` and `last_name`, and their first `Profile` `name` on one form:*my_app/models.py*```pythonfrom django.contrib.auth.models import Userclass Profile(models.Model):name = models.CharField(max_length=255)user = models.ForeignKey(User, related_name='profiles', on_delete=models.CASCADE)```*my_app/forms.py*```pythonfrom django.contrib.auth.models import Userfrom .models import Profileclass UserForm(forms.ModelForm):class Meta:model = Userfields = ['first_name','last_name',]class ProfileForm(forms.ModelForm):class Meta:model = Groupfields = ['name',]labels = {'name': 'Profile Name',}```*my_app/views.py*```pythonfrom shapeshifter.views import MultiModelFormViewfrom shapeshifter.mixins import MultiSuccessMessageMixinfrom .forms import UserForm, ProfileFormclass UserUpdateView(LoginRequiredMixin, MultiSuccessMessageMixin, MultiModelFormView):form_classes = (UserForm, ProfileForm)template_name = 'my_app/forms.html'success_url = reverse_lazy('home')def get_instances(self):instances = {'userform': self.request.user,'profileform': profile_instance = Profile.objects.filter(user=self.request.user,).first(),}return instances```### What if I want to mix model and standard form?That's fine! You will have to override `forms_valid` in your view to handle theprocessing of each form but everything else should work exactly the same.## API`MultiFormView` (and `MultiModelFormView` by inheritance) extends Django's`TemplateView`. Additionally it adds a few methods for the instantiation andprocessing of the forms. Any and all of these can be overwritten to customizethe behavior of your views.Below is each attribute and their default value, and each method with itssignature and return value.### Attributes* `initial = {}` - Initial values for each form. Should be a `dict` formattedwith the following format:```pythoninitial = {'contactform': {'name': 'Katherine Johnson'}}```where `ContactForm` is the class name of the form you're providing initialvalues for.* `form_classes = None` - a list or tuple of `Form` (or `ModelForm` if using`MultiModelFormView`) classes. **Do not instantiate the class, just provide thename**).* `success_url = None` - the URL to redirect users to once the forms are all filled incorrectly. This can be a URL or a `reverse_lazy` instance.### Methods* `get_form_classes(self)` - Returns the view's `form_classes` attribute.Override this method if you need to dynamically set the forms that should beincluded in the view.* `get_forms(self) -> dict` - Instantiates each form, using the `kwargs` from`get_form_kwargs` and returns them all as a dict with the key being a standardizedversion of the form's class name. Override this if you need to change how theforms are instantiated.* `get_form_class_name(self, form_class) -> str` - Converts the form's classname into a lowercase string. `ContactForm` will become `contactform`. You canoverride this to provide for a different standardized name for your forms.* `get_form_kwargs(self, form_class) -> dict` - Returns a dict of keywordarguments for the form's creation. Prefixes each form with the lowercased classname, provides any `initial` arguments for the form, and, if the view wasrequested as either `POST` or `PUT`, provides both `data` and `files` to theform. For `MultiModelFormView`, this method also provides the `instance` for theform. Override this method to add or change the form kwargs.* `validate_forms(self) -> bool` - Calls `form.is_valid()` for each form andreturns the result for the entire set of forms. Override this method if yourforms require any special validation steps.* `forms_valid(self)` - This method is called if all forms pass validation. In`MultiFormView`, this method simply redirects to the `success_url`. For themodel-based version, `MultiModelFormView`, this method calls `form.save()` oneach form and then redirects. Override this method to change what happenswhen the forms are all valid.* `forms_invalid(self)` - If any of the forms fail their validation check, thismethod is executed. By default, it re-renders the view, presenting the formswith their errors. You can override this method if you need something else tohappen when not all forms are valid.### `MultiModelFormView`'s extra attributes and methodsAs mentioned above, a few things are handled differently in `MultiModelFormView`.* `instances = {}` - This attribute should be a dict with lowercase form classnames as keys. The values should be the instance to use for the form.* `get_instances(self)` - Returns the value of `instances`. Override this ifyou need to dynamically fetch the instances for the forms.### `MultiSuccessMessageMixin` attributes and methods* `success_message = None` - A string containing a success message to add through Django's messages framework.* `get_success_message(self)` - A method which returns the success message. Defaults to self.success_message.* `forms_valid(self)` - Returns the response after adding the success message.## ContributingThank you for your interest, time, and energy! Contributions are alwayswelcome and will be reviewed as quickly as possible (that said, we're allvolunteers with other jobs/responsibilities so it might be awhile).Please fork this repository and make your changes in the `shapeshifter` package.Be sure to add a test for any functionality changes. Once all tests pass, youcan submit a pull request with your changes, the rationale behind them, andany special steps the maintainers will need to take to test your changes orreplicate the bug you're fixing. **Be sure to include adding your name to thefollowing list of contributors!**### Contributors* Kenneth Love* Lacey Williams Henschel* Tim Allen## What's with the name? And the version?The original name was already taken so a new one had to be found. Since thispackage deals with multiple forms, `shapeshifter` was a good pun (shapeshifterscan take on many forms).The version number is based on the date of release.