AndrewIngram / django-extra-views

Django's class-based generic views are awesome, let's have more of them.
MIT License
1.39k stars 172 forks source link

UpdateWithInlinesView not working with crispy_forms #216

Open kishalaykundu opened 4 years ago

kishalaykundu commented 4 years ago

I am using 'extra_views' to create and update a model 'Patient' and its related model 'PatientAddress'. I am also using 'crispy_forms' to beautify the html form. CreateWithInlinesView works fine. The UpdateWithInlinesView however does not seem to play nice with crispy_forms. I have attached the requisite files, but I give a description of the problem below:

In 'update.html', I have the following lines:

DOES NOT WORK

` {% csrf_token %}
{{ form.name|as_crispy_field }}
{{ form.phone|as_crispy_field }}
{% for formset in inlines %} {% for addr in formset %}
{{ addr.line_1|as_crispy_field }}
{{ addr.line_2|as_crispy_field }}
{% endfor %} {{ formset.management_form }} {% endfor %}

WORKS:

` {% csrf_token %}
{{ form.name|as_crispy_field }}
{{ form.phone|as_crispy_field }}
{% for formset in inlines %} {{ formset }} {% endfor %}

The first form comes back to the update page whereas the second form works but is not at all aesthetically nice. I am not quite sure what to do with this or how to proceed from here. I apologize in advance if this is not the appropriate place to post about this.

code.zip

danizen commented 3 years ago

I have this use case working lbasically like this:

    <form method="POST" action="{% url 'app-name:url-name' slug=object.slug %}">
        <div class="form-horizontal">
            {% crispy form %}
        </div>
        {% crispy formset %}
    </form>

I do basically the same thing again and again (e.g. the pattern above is repeated many times in my application).

The trick to making it work with InlineFormSet is to declare a form helper:

from crispy_forms.helper import FormHelper

class SomeFormSetHelper(FormHelper):
    """A crispy helper for the SomeInlineFormSet"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.form_tag = False
        self.disable_csrf = True
        self.template = 'table_inline_formset.html'
        self.layout = Layout(
            Field('field1', hidden=True),
            'field2',
            Field('field3', css_class='repeat-date'),
            Field('field4', css_class='repeat-user'),
            'field5'
        )

Then in the method construct_formset or construct_inlines, depending on which view you are using, you can associate the helper with the formset:

        formset.helper = SomeFormSetHelper()
        for form in formset:
            # munge fields if needed, an example:
            form.fields['DELETE'].widget.attrs['title'] = 'Delete on Save'
        return formset

Looks like I build a custom table version of inline formset, table_inline_formset.html to achieve what I wanted in a table view. I think the template below was about not showing the empty formset and then using jquery to duplicate it with a button - but my application is a little old now.

{% load crispy_forms_tags %}
{% load crispy_forms_utils %}
{% load crispy_forms_field %}
{% load crispy_forms_filters %}

{% specialspaceless %}
{% if formset_tag %}
<form {{ flat_attrs|safe }} method="{{ form_method }}" {% if formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
{% endif %}
    {% if formset_method|lower == 'post' and not disable_csrf %}
        {% csrf_token %}
    {% endif %}

    {% if formset.non_form_errors %}
        {{ formset|as_crispy_errors }}
    {% endif %}

    <div>
        {{ formset.management_form|crispy }}
    </div>

    <table{% if form_id %} id="{{ form_id }}_table"{% endif%} class="table table-striped table-condensed">
        <thead>
            {% if formset.readonly and not formset.queryset.exists %}
            {% else %}
                <tr>
                    {% for field in formset.empty_form %}
                        {% if field.label and not field.is_hidden %}
                            <th for="{{ field.auto_id }}" class="control-label {% if field.field.required %}requiredField{% endif %}">
                                {{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
                            </th>
                        {% endif %}
                    {% endfor %}
                </tr>
            {% endif %}
        </thead>

        <tbody>
            <tr class="hidden empty-form">
                {% for field in formset.empty_form %}
                    {% include 'bootstrap3/field.html' with tag="td" form_show_labels=False %}
                {% endfor %}
            </tr>

            {% for form in formset %}
                {% if form_show_errors and not form.is_extra %}
                    {% include "bootstrap3/errors.html" %}
                {% endif %}

                <tr>
                    {% for field in form %}
                        {% include 'bootstrap3/field.html' with tag="td" form_show_labels=False %}
                    {% endfor %}
                </tr>
            {% endfor %}
        </tbody>
    </table>

    {% include "bootstrap3/inputs.html" %}

{% if formset_tag %}</form>{% endif %}
{% endspecialspaceless %}
oesah commented 9 months ago

In case someone needs this, here is my solution:

class MyFormHelper(FormHelper):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = FormHelper()
        self.helper.form_tag = False
        self.helper.layout = Layout(
            "field",
        )

class PlaceMetaInline(InlineFormSetFactory):
    ...
    def construct_formset(self):
        formset = super().construct_formset()
        formset.helper = forms.MyFormHelper().helper
        return formset

# In template
{% for inline_form in inlines %}
  {% crispy inline_form inline_form.helper %}
{% endfor %}