Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django crispy forms: Nesting a formset within a form

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.

like image 392
Neil Avatar asked Mar 01 '13 12:03

Neil


3 Answers

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'),
    ),
like image 107
qris Avatar answered Oct 02 '22 03:10

qris


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'),
    ),
)
like image 29
ch00kz Avatar answered Oct 02 '22 01:10

ch00kz


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.

like image 27
maraujop Avatar answered Oct 02 '22 02:10

maraujop