Kishalay Kundu
Kishalay Kundu

Reputation: 11

UpdateWithInlinesView for django-extra-views not working with crispy_forms

I have a Django app with django-extra-views to create and maintain a pair of related models. The set up is as follows:

# models.py
class ModelA(models.Model):
    # fields
    def get_absolute_url(self):
        return reverse('model_a:detail', kwargs={'pk': self.id})

class ModelB(models.Model):
    model_a = models.OneToOneField(ModelA, on_delete=models.CASCADE)
    # other fields

I have two corresponding views and forms:

# views.py
class ModelBView(InlineFormSetFactory):
    model = ModelB
    form_class = ModelBForm
    prefix = 'model_b'

class ModelACreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateWithInlinesView):
    model = ModelA
    inlines = [ModelBView, ]
    permission_required = ('add_model_a')
    template_name = 'apps/model_a/create.html'
    form = ModelAForm
    fields = '__all__'

    def get_success_url(self):
        return self.object.get_absolute_url()


class ModelAUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateWithInlinesView):
    model = ModelA
    inlines = [ModelBView, ]
    permission_required = ('change_model_a')
    template_name = 'apps/model_a/update.html'
    form = ModelAForm
    fields = '__all__'

    def get_success_url(self):
        return self.object.get_absolute_url()
# forms.py
class ModelAForm(forms.ModelForm):
    class Meta:
        model = ModelA
        fields = [
             # some fields
        ]
        initial = [...]


class ModelBForm(forms.ModelForm):
    class Meta:
        model = ModelB
        fields = [
            # some fields
        ]

The trouble happens in the template files. The CreateWithInlinesView works, while UpdateWithInlinesView does not, when paired with crispy_forms. Following is the code that DOES NOT work. When I press the 'Save' button, it brings me back to the update page and when I check the details page, things are unchanged:

<form class="form-horizontal" method="POST">
    {% csrf_token %}
    <div class="form-row">
        <div class="form-group col-md-8 mb-0">
            {{ form.name|as_crispy_field }}
        </div>
        <div class="form-group col-md-4 mb-0">
            {{ form.phone|as_crispy_field }}
        </div>
    </div>

    {% for formset in inlines %} {% for addr in formset %}
    <div class="form-row">
        <div class="form-group col-md-6 mb-0">
            {{ addr.field_1|as_crispy_field }}
        </div>
        <div class="form-group col-md-6 mb-0">
            {{ addr.field_2|as_crispy_field }}
        </div>
    </div>

    {% endfor %} {{ formset.management_form }} {% endfor %}

    <div class="control-group text-right">
        <div class="controls">
            <button type="submit" class="btn btn-default btn-person"><i class="fas fa-save"></i> Save</button>
        </div>
    </div>

</form>

There is a version that DOES WORK. But it involves me not using crispy_forms and is aesthically unappealing. When I press the 'Save' button, it updates and redirects to the "details" page. Below is the code (the difference is the bottom half):

<form class="form-horizontal" method="POST">
    {% csrf_token %}
    <div class="form-row">
        <div class="form-group col-md-8 mb-0">
            {{ form.name|as_crispy_field }}
        </div>
        <div class="form-group col-md-4 mb-0">
            {{ form.phone|as_crispy_field }}
        </div>
    </div>

    {% for formset in inlines %} {{ formset }} {% endfor %}

    <div class="control-group text-right">
        <div class="controls">
            <button type="submit" class="btn btn-default btn-person"><i class="fas fa-save"></i> Save</button>
        </div>
    </div>

</form>

I don't know how to proceed on this one and would love someone's help. Thank you.

Upvotes: 1

Views: 857

Answers (1)

Sanurag Dharme
Sanurag Dharme

Reputation: 11

This is basically happening because you didn't include id (hidden field) in the formset. To check error you can write the below method in UpdateWithInlinesView.

def forms_invalid(self, form, inlines):
    for formset in inlines:
        for errors in formset.errors:
            for _, error in errors.items():
                print(error[0])
    return self.render_to_response(
        self.get_context_data(request=self.request, form=form, inlines=inlines))

This will print

id: 'This field is required'

To get rid of this, you have to include hidden fields as follows:

{% for formset in inlines %} 
    {{ formset.management_form|crispy }} 
    {{ formset|as_crispy_errors }}
    {% for addr in formset %}
        {% for hidden in addr.hidden_fields %}
            {{ hidden }}
        {% endfor %}
        <div class="form-row">
            <div class="form-group col-md-6 mb-0">
                {{ addr.field_1|as_crispy_field }}
            </div>
            <div class="form-group col-md-6 mb-0">
                {{ addr.field_2|as_crispy_field }}
            </div>
        </div>
    {% endfor %} 
{% endfor %}

Or you can try below one also. In this we are directly passing the id field (hidden by default). I am using this one

{% for formset in inlines %} 
    {{ formset.management_form|crispy }} 
    {{ formset|as_crispy_errors }}
    {% for addr in formset %}
        {{ addr.id }}
        <div class="form-row">
            <div class="form-group col-md-6 mb-0">
                {{ addr.field_1|as_crispy_field }}
            </div>
            <div class="form-group col-md-6 mb-0">
                {{ addr.field_2|as_crispy_field }}
            </div>
        </div>
    {% endfor %} 
{% endfor %}

Upvotes: 1

Related Questions