django crispy forms: Nesting a formset within a form - django

I have a django Formset that I'd like to layout in the middle of another form. I'm using django-crispy-forms to set the layout in the parent form's __init__:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Field, Div
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
self.helper.layout = Layout(
Div(
Div(Field('foo'), css_class='span3'),
Div(Field('bar'), css_class='span4'),
css_class='row'
),
Field('baz', css_class='span1'),
...
)
self.helper.add_input(Submit('submit', 'Submit', css_class='btn btn-primary offset4'))
My template simply renders the form using the {% crispy %} tag.
I'd like to know how I should incorporate the formset. Should I instantiate it in the above init function? How do I refer to it there?
There are other examples of form and formset combos online that have one render after the other serially, but I'm wondering whether I can have more control over how they fit together with crispy's layout.

I solved this without modifying Crispy Forms, by creating a new field type that renders a formset:
from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
class Formset(LayoutObject):
"""
Layout object. It renders an entire formset, as though it were a Field.
Example::
Formset("attached_files_formset")
"""
template = "%s/formset.html" % TEMPLATE_PACK
def __init__(self, formset_name_in_context, template=None):
self.formset_name_in_context = formset_name_in_context
# crispy_forms/layout.py:302 requires us to have a fields property
self.fields = []
# Overrides class variable with an instance level variable
if template:
self.template = template
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
formset = context[self.formset_name_in_context]
return render_to_string(self.template, Context({'wrapper': self,
'formset': formset}))
It needs a template to render the formset, which gives you control over exactly how it's rendered:
{% load crispy_forms_tags %}
<div class="formset">
{% crispy formset %}
<input type="button" name="add" value="Add another" />
</div>
You can use it to embed a formset in your layouts just like any other Crispy layout element:
self.helper.layout = Layout(
MultiField(
"Education",
Formset('education'),
),

A slight modification to the earlier answer by qris.
This update (as suggested by Alejandro) will allow for our custom Formset Layout Object to use a FormHelper object to control how the formset's fields are rendered.
from crispy_forms.layout import LayoutObject
from django.template.loader import render_to_string
class Formset(LayoutObject):
"""
Renders an entire formset, as though it were a Field.
Accepts the names (as a string) of formset and helper as they
are defined in the context
Examples:
Formset('contact_formset')
Formset('contact_formset', 'contact_formset_helper')
"""
template = "forms/formset.html"
def __init__(self, formset_context_name, helper_context_name=None,
template=None, label=None):
self.formset_context_name = formset_context_name
self.helper_context_name = helper_context_name
# crispy_forms/layout.py:302 requires us to have a fields property
self.fields = []
# Overrides class variable with an instance level variable
if template:
self.template = template
def render(self, form, form_style, context, **kwargs):
formset = context.get(self.formset_context_name)
helper = context.get(self.helper_context_name)
# closes form prematurely if this isn't explicitly stated
if helper:
helper.form_tag = False
context.update({'formset': formset, 'helper': helper})
return render_to_string(self.template, context.flatten())
Template (used to render formset):
{% load crispy_forms_tags %}
<div class="formset">
{% if helper %}
{% crispy formset helper %}
{% else %}
{{ formset|crispy }}
{% endif %}
</div>
Now it can be used in any layout just like any other crispy forms layout object.
self.helper.layout = Layout(
Div(
Field('my_field'),
Formset('my_formset'),
Button('Add New', 'add-extra-formset-fields'),
),
)
# or with a helper
self.helper.layout = Layout(
Div(
Field('my_field'),
Formset('my_formset', 'my_formset_helper'),
Button('Add New', 'add-extra-formset-fields'),
),
)

This is currently not supported in crispy-forms. Your only option would be to use |as_crispy_field filter (not documented yet, sorry).
I have started development of this feature for {% crispy %} tag and in a feature branch, it's all explained here: https://github.com/maraujop/django-crispy-forms/issues/144
I'm looking for feedback, so if you are still interested, feel free to post.

Basing on above solution Formset(LayoutObject), you would combine django-dynamic-formset & crispy.
On my order page I have:
order's section part 1
order's inline formset with dynamic-add forms
order's section part N
Now it is simple and clear, ModelForms are:
class OrderTestForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(OrderTestForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_tag = True
self.helper.html5_required = True
self.helper.form_action = 'test_main'
self.helper.layout = Layout(
'product_norms', #section 1
'reference_other', #section 1
# rest of the section 1 fields
Formset('samples', 'helper'), # inline dynamic forms
'checkbox_is_required' # start of section N
# other order sections fields
)
self.helper.add_input(Submit("submit", "Save order"))
Formset helper layout:
class SamplesFormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(SamplesFormSetHelper, self).__init__(*args, **kwargs)
self.form_method = 'post'
self.html5_required = True
self.layout = Layout(
Fieldset('',
'description',
'product', # foreign key
'DELETE', # delete django-dynamic-formset
css_class="formset_row"), # add-rows
)
self.form_tag = False
self.render_required_fields = False
Add/delete inlines, saving order with formset operations work as expected.

Related

Django Crispy Forms CustomLayout with Fieldset

So I want to build a custom layout, that extends LayoutObject for a form that I have.
class NewBookForm(LayoutObject):
template = 'library/layouts/new_book_layout.html'
def __init__(self, fields, template=None, *args, **kwargs):
self.fields = fields
# Overrides class variable with an instance level variable
if template:
self.template = template
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
fields = self.get_rendered_fields(form, form_style, context, template_pack, **kwargs)
template = self.get_template_name(template_pack)
return render_to_string(template, {'fields': fields})
And I'm calling it using
self.helper.layout = Layout(
NewBookForm(Fieldset('book_id', 'name', 'author'))
)
Right now Django is saying "template does not exist."
However, this is not getting me the result I'm looking for.
UPDATE 1:
Now that library/layouts/new_book_layout.html has something like
<div class="w-40 mr-2">
{{name|as_crispy_field}}
{{name.errors.as_ul }}
{{author|as_crispy_field}}
{{author.errors.as_ul }}
</div>
I'm now getting the error:
CrispyError at library/layouts/new_book_layout.html
|as_crispy_field got passed an invalid or inexistent field
and highlighted:
{{name|as_crispy_field}}
That is because once you call get_rendered_fields it returns an string object not a crispy object, so instead of using |as_crispy_field filter, you should use the |safe filter since what your context contains is an HTML string.

Rendering field errors in django-crispy-forms with inline forms

I'm using bootstrap3 as the default template pack in django_crispy_forms, and trying to render a form with the crispy tag:
{% crispy form %}
My form class has the following helper attributes:
class TheForm(forms.Form):
adv_var = forms.CharField(label="variable", max_length=70)
value = forms.FloatField()
def __init__(self, *args, **kwargs):
super(TheForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'form-inline'
self.helper.field_template = 'bootstrap3/layout/inline_field.html'
self.helper.layout = Layout(
'adv_var', 'value',
ButtonHolder(
Submit('submit', 'Start', css_class='button white')
)
)
When posting the form with errors, re-rendering the template does not show the errors even though I can print form._errors in the view and see the list of errors.
If I change the helper.field_template to another value (or remove it to set the default) the errors are displayed above each field - but I don't get the inline display anymore.
How can I use django-crispy-forms to display all errors of this form in a separate div for example?
We use django.contrib.messages to push a generic error string when the form has validation errors, and leave the field errors alone to render inline:
from django.contrib import messages
# ...
if not form.is_valid():
messages.error(request, "Please correct the errors below and resubmit.")
return render(request, template, context)
We then use bootstrap alerts to show all messages, including our generic error, though you could of course mark it up however you wanted.
But if all you want to do is move the errors into a separate block, add them to your request context:
from django.contrib import messages
# ...
if not form.is_valid():
context['form_errors'] = form.errors
return render(request, template, context)
and in your template:
{% crispy form %}
<div id='form-errors'>{{ form_errors }}</div>
You can then fiddle with the crispy form's helper attributes and styles to control the display of the inline errors.
Maybe the easier way is the next because it uses less imports...
.. in views:
if request.method == 'POST':
form = TheForm(request.POST)
if form.is_valid():
form.save()
return redirect('url_name')
else:
form = TheForm()
return render(request, 'form.html', {'form': form})
... and in the form you need only:
{% load crispy_forms_tags %}
{% crispy form %}
... where 'url_name' is defined name of pattern in urlpatterns (urls.py )... that's all you need really...
Crispy is a really smart system. The system knows how can intuitively to show the form errors.
You may want to consider using the FormView Generic View, especially if you are using crispy forms:
app/views.py:
from django.views.generic.edit import FormView
from django.urls import reverse_lazy
from .forms import MyForm
class MyFormView(FormView):
template_name = 'app/myform.html'
form_class = MyForm
success_url = reverse_lazy('success_url')
app/templates/app/myform.html:
{% load crispy_forms_tags %}
{% crispy form %}

django-userena removing mugshot

I am building a site which uses userena for the profile and registration part. The problem is that I am trying to remove the mugshot upload part and the profile privacy(registered,open,closed) from edit profile page so that userena uses gravatar only and the profiles are public for all. But in the template there is just
<fieldset>
<legend>{% trans "Edit Profile" %}</legend>
{{ form.as_p }}
</fieldset>
<input type="submit" value="{% trans "Save changes" %}" />
</form>
I am trying to find out how to edit this or the views to remove the mugshot and privacy from the form but without success. Please help?
Instead of editing userena forms directly you should sub it in your own forms.py file (accounts/forms.py for example) as mentioned in the FAQ and put the url above the userena include. Here is an example where I use crispy-forms to sub class the edit profile form for nice bootstrap forms:
accounts/forms.py
class EditProfileFormExtra(EditProfileForm):
class Meta:
model = get_profile_model()
exclude = ['user', 'mugshot', 'privacy', 'my_custom_field']
def __init__(self, *args, **kwargs):
super(EditProfileFormExtra, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'edit-profile-form'
self.helper.form_class = 'form-horizontal'
self.helper.form_method = 'post'
self.helper.help_text_inline = True
self.helper.add_input(Submit('submit', _('Save'), css_class='green'))
self.helper.layout = Layout(
Field('first_name', placeholder=_("First Name")),
Field('last_name', placeholder=_("Last Name")),
Field('language', css_class="chosen"),
Field('timezone', css_class="chosen"),
)
accounts/urls.py
urlpatterns = patterns(
'',
url(r'^signup/$', 'userena.views.signup', {'signup_form': SignupFormExtra}, name='signup'),
url(r'^signin/$', 'userena.views.signin', {'auth_form': SigninFormExtra}, name='signin'),
url(r'^(?P<username>[\.\w-]+)/edit/$', 'userena.views.profile_edit', {'edit_profile_form': EditProfileFormExtra}, name='edit-profile'),
url(r'^', include('userena.urls')),
)
You can do this with just about any form as you can see in the urls above. Basically it says at this url, use the original modules view, but replace the form argument with my own form.
The best is to remove those two fields by editing the form itself. In the view.py of the userena package, simply change the EditProfileForm by adding 'mugshot' and 'privacy' to the exclude list:
class EditProfileForm(forms.ModelForm):
...
class Meta:
model = get_profile_model()
exclude = ['user', 'mugshot', 'privacy']
If you really want to change the template only, you can iterate through the form instead of using form.as_p. In this case you will have to add the markup for other field parameters (like labels, errors, non-field errors etc) - see an example a here.
{% for field in form %}
{% if field.name != 'mugshot' %}
{{ field }}
{% endif %}
{% endfor %}

Django: Can class-based views accept two forms at a time?

If I have two forms:
class ContactForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
class SocialForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
and wanted to use a class based view, and send both forms to the template, is that even possible?
class TestView(FormView):
template_name = 'contact.html'
form_class = ContactForm
It seems the FormView can only accept one form at a time.
In function based view though I can easily send two forms to my template and retrieve the content of both within the request.POST back.
variables = {'contact_form':contact_form, 'social_form':social_form }
return render(request, 'discussion.html', variables)
Is this a limitation of using class based view (generic views)?
Many Thanks
Here's a scaleable solution. My starting point was this gist,
https://gist.github.com/michelts/1029336
i've enhanced that solution so that multiple forms can be displayed, but either all or an individual can be submitted
https://gist.github.com/jamesbrobb/748c47f46b9bd224b07f
and this is an example usage
class SignupLoginView(MultiFormsView):
template_name = 'public/my_login_signup_template.html'
form_classes = {'login': LoginForm,
'signup': SignupForm}
success_url = 'my/success/url'
def get_login_initial(self):
return {'email':'dave#dave.com'}
def get_signup_initial(self):
return {'email':'dave#dave.com'}
def get_context_data(self, **kwargs):
context = super(SignupLoginView, self).get_context_data(**kwargs)
context.update({"some_context_value": 'blah blah blah',
"some_other_context_value": 'blah'})
return context
def login_form_valid(self, form):
return form.login(self.request, redirect_url=self.get_success_url())
def signup_form_valid(self, form):
user = form.save(self.request)
return form.signup(self.request, user, self.get_success_url())
and the template looks like this
<form class="login" method="POST" action="{% url 'my_view' %}">
{% csrf_token %}
{{ forms.login.as_p }}
<button name='action' value='login' type="submit">Sign in</button>
</form>
<form class="signup" method="POST" action="{% url 'my_view' %}">
{% csrf_token %}
{{ forms.signup.as_p }}
<button name='action' value='signup' type="submit">Sign up</button>
</form>
An important thing to note on the template are the submit buttons. They have to have their 'name' attribute set to 'action' and their 'value' attribute must match the name given to the form in the 'form_classes' dict. This is used to determine which individual form has been submitted.
By default, class-based views only support a single form per view. But there are other ways to accomplish what you need. But again, this cannot handle both forms at the same time. This will also work with most of the class-based views as well as regular forms.
views.py
class MyClassView(UpdateView):
template_name = 'page.html'
form_class = myform1
second_form_class = myform2
success_url = '/'
def get_context_data(self, **kwargs):
context = super(MyClassView, self).get_context_data(**kwargs)
if 'form' not in context:
context['form'] = self.form_class(request=self.request)
if 'form2' not in context:
context['form2'] = self.second_form_class(request=self.request)
return context
def get_object(self):
return get_object_or_404(Model, pk=self.request.session['value_here'])
def form_invalid(self, **kwargs):
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'form' in request.POST:
form_class = self.get_form_class()
form_name = 'form'
else:
form_class = self.second_form_class
form_name = 'form2'
form = self.get_form(form_class)
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(**{form_name: form})
template
<form method="post">
{% csrf_token %}
.........
<input type="submit" name="form" value="Submit" />
</form>
<form method="post">
{% csrf_token %}
.........
<input type="submit" name="form2" value="Submit" />
</form>
Its is possible for one class-based view to accept two forms at a time.
view.py
class TestView(FormView):
template_name = 'contact.html'
def get(self, request, *args, **kwargs):
contact_form = ContactForm()
contact_form.prefix = 'contact_form'
social_form = SocialForm()
social_form.prefix = 'social_form'
# Use RequestContext instead of render_to_response from 3.0
return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form}))
def post(self, request, *args, **kwargs):
contact_form = ContactForm(self.request.POST, prefix='contact_form')
social_form = SocialForm(self.request.POST, prefix='social_form ')
if contact_form.is_valid() and social_form.is_valid():
### do something
return HttpResponseRedirect(>>> redirect url <<<)
else:
return self.form_invalid(contact_form,social_form , **kwargs)
def form_invalid(self, contact_form, social_form, **kwargs):
contact_form.prefix='contact_form'
social_form.prefix='social_form'
return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form}))
forms.py
from django import forms
from models import Social, Contact
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Button, Layout, Field, Div
from crispy_forms.bootstrap import (FormActions)
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
helper = FormHelper()
helper.form_tag = False
class SocialForm(forms.Form):
class Meta:
model = Social
helper = FormHelper()
helper.form_tag = False
HTML
Take one outer form class and set action as TestView Url
{% load crispy_forms_tags %}
<form action="/testview/" method="post">
<!----- render your forms here -->
{% crispy contact_form %}
{% crispy social_form%}
<input type='submit' value="Save" />
</form>
Good Luck
I have used a following generic view based on TemplateView:
def merge_dicts(x, y):
"""
Given two dicts, merge them into a new dict as a shallow copy.
"""
z = x.copy()
z.update(y)
return z
class MultipleFormView(TemplateView):
"""
View mixin that handles multiple forms / formsets.
After the successful data is inserted ``self.process_forms`` is called.
"""
form_classes = {}
def get_context_data(self, **kwargs):
context = super(MultipleFormView, self).get_context_data(**kwargs)
forms_initialized = {name: form(prefix=name)
for name, form in self.form_classes.items()}
return merge_dicts(context, forms_initialized)
def post(self, request):
forms_initialized = {
name: form(prefix=name, data=request.POST)
for name, form in self.form_classes.items()}
valid = all([form_class.is_valid()
for form_class in forms_initialized.values()])
if valid:
return self.process_forms(forms_initialized)
else:
context = merge_dicts(self.get_context_data(), forms_initialized)
return self.render_to_response(context)
def process_forms(self, form_instances):
raise NotImplemented
This has the advantage that it is reusable and all the validation is done on the forms themselves.
It is then used as follows:
class AddSource(MultipleFormView):
"""
Custom view for processing source form and seed formset
"""
template_name = 'add_source.html'
form_classes = {
'source_form': forms.SourceForm,
'seed_formset': forms.SeedFormset,
}
def process_forms(self, form_instances):
pass # saving forms etc
It is not a limitation of class-based views. Generic FormView just is not designed to accept two forms (well, it's generic). You can subclass it or write your own class-based view to accept two forms.
Use django-superform
This is a pretty neat way to thread a composed form as a single object to outside callers, such as the Django class based views.
from django_superform import FormField, SuperForm
class MyClassForm(SuperForm):
form1 = FormField(FormClass1)
form2 = FormField(FormClass2)
In the view, you can use form_class = MyClassForm
In the form __init__() method, you can access the forms using: self.forms['form1']
There is also a SuperModelForm and ModelFormField for model-forms.
In the template, you can access the form fields using: {{ form.form1.field }}. I would recommend aliasing the form using {% with form1=form.form1 %} to avoid rereading/reconstructing the form all the time.
Resembles #james answer (I had a similar starting point), but it doesn't need to receive a form name via POST data. Instead, it uses autogenerated prefixes to determine which form(s) received POST data, assign the data, validate these forms, and finally send them to the appropriate form_valid method. If there is only 1 bound form it sends that single form, else it sends a {"name": bound_form_instance} dictionary.
It is compatible with forms.Form or other "form behaving" classes that can be assigned a prefix (ex. django formsets), but haven't made a ModelForm variant yet, tho you could use a model form with this View (see edit below). It can handle forms in different tags, multiple forms in one tag, or a combination of both.
The code is hosted on github (https://github.com/AlexECX/django_MultiFormView). There are some usage guidelines and a little demo covering some use cases. The goal was to have a class that feels as close as possible like the FormView.
Here is an example with a simple use case:
views.py
class MultipleFormsDemoView(MultiFormView):
template_name = "app_name/demo.html"
initials = {
"contactform": {"message": "some initial data"}
}
form_classes = [
ContactForm,
("better_name", SubscriptionForm),
]
# The order is important! and you need to provide an
# url for every form_class.
success_urls = [
reverse_lazy("app_name:contact_view"),
reverse_lazy("app_name:subcribe_view"),
]
# Or, if it is the same url:
#success_url = reverse_lazy("app_name:some_view")
def get_contactform_initial(self, form_name):
initial = super().get_initial(form_name)
# Some logic here? I just wanted to show it could be done,
# initial data is assigned automatically from self.initials anyway
return initial
def contactform_form_valid(self, form):
title = form.cleaned_data.get('title')
print(title)
return super().form_valid(form)
def better_name_form_valid(self, form):
email = form.cleaned_data.get('email')
print(email)
if "Somebody once told me the world" is "gonna roll me":
return super().form_valid(form)
else:
return HttpResponse("Somebody once told me the world is gonna roll me")
template.html
{% extends "base.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ forms.better_name }}
<input type="submit" value="Subscribe">
</form>
<form method="post">
{% csrf_token %}
{{ forms.contactform }}
<input type="submit" value="Send">
</form>
{% endblock content %}
EDIT - about ModelForms
Welp, after looking into ModelFormView I realised it wouldn't be that easy to create a MultiModelFormView, I would probably need to rewrite SingleObjectMixin as well. In the mean time, you can use a ModelForm as long as you add an 'instance' keyword argument with a model instance.
def get_bookform_form_kwargs(self, form_name):
kwargs = super().get_form_kwargs(form_name)
kwargs['instance'] = Book.objects.get(title="I'm Batman")
return kwargs

Remove Labels in a Django Crispy Forms

Does anybody know if there is a correct way to remove labels in a crispy form?
I got as far as this:
self.fields['field'].label = ""
But it's not a very nice solution.
Just do:
self.helper.form_show_labels = False
To remove all labels.
Works with Boostrap ( see documentation )
In your form :
from crispy_forms.helper import FormHelper
from django import forms
class MyForm(forms.Form):
[...]
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_show_labels = False
In your template:
<form method='POST' action=''>{% csrf_token %}
{% crispy form %}
<input type='submit' value='Submit' class='btn btn-default'>
</form>
You could edit the field.html template:
https://github.com/maraujop/django-crispy-forms/blob/dev/crispy_forms/templates/bootstrap/field.html#L7
Add a FormHelper attribute to your form that controls the label rendering and use it in that template if. Custom FormHelper attributes are not yet officially documented, because I haven't had time, but I talked about them in a keynote I gave, here are the slides:
https://speakerdeck.com/u/maraujop/p/django-crispy-forms
The solution below lets you remove a label from both a regular or crispy control. Not only does the label text disappear, but the space used by the label is also removed so you don't end up with a blank label taking up space and messing up your layout.
The code below works in django 2.1.1.
# this class would go in forms.py
class SectionForm(forms.ModelForm):
# add a custom field for calculation if desired
txt01 = forms.CharField(required=False)
def __init__(self, *args, **kwargs):
''' remove any labels here if desired
'''
super(SectionForm, self).__init__(*args, **kwargs)
# remove the label of a non-linked/calculated field (txt01 added at top of form)
self.fields['txt01'].label = ''
# you can also remove labels of built-in model properties
self.fields['name'].label = ''
class Meta:
model = Section
fields = "__all__"
I'm not clear what the problem the OP had with the code snippet he showed, except that he wasn't putting the line of code in the right place. This seems like the best and simplest solution.
if you are only to remove some labels from input, then explicitly don't give a label name in model definition, i.e:
field = models.IntegerField("",null=True)
To Remove All Labels:
self.helper.form_show_labels = False
To Show Specific Lable when all False :
HTML('<span>Your Label</span>')
To Disable Label for Specific field when all is True
self.fields['fieldName'].label = True
Example:
Row(
HTML('<span> Upolad Government ID (Adhar/PAN/Driving Licence)</span>'),
Column('IdProof',css_class='form-group col-md-12 mb-0'),
css_class='form-row'
),