inlineformset_factory not updating database after

Hi,

I’m trying to evaluate using Inline formsets for a project, but at the moment I’m struggling to save data from the updated formset into the database.

In principle, I used the same logic to create a formset, as it seems to be the same

Form

class LeadEntryForm(forms.ModelForm):
    class Meta:
        model = LeadEntry
        fields = ('lead_id','date','revenue','probability')
        widgets = {'date': DateInput()}

This view creates the Formset for a given Lead:

@login_required
def data_form_forecast(request):
    # gets the project number
    lead_id = request.GET.get('project_id')
    #Gets the lead associated with the LeadEntry (Child Object)
    lead = get_object_or_404(Leads, pk=lead_id)
    # Create an inline formset using Leads the parent model and LeadEntry as child
    FormSet = inlineformset_factory(Leads, LeadEntry, form=LeadEntryForm, extra=4)
    if request.method == "POST":
        formset = FormSet(request.POST,instance=lead)
        if formset.is_valid():
            formset.save()
            return redirect('dashboard')

    context = {
        'formset': FormSet(instance=lead, queryset=LeadEntry.objects.none())
    }
    return render(request, "account/lead_forecast.html", context)

This is the form to create the formset

<h1> Add Lead to Sales Forecast </h1>
  <h3>Enter Payment Details</h3>
  <br>

  <div class="container-fluid" >
        <form method="POST" enctype="multipart/form-data" >
            {% csrf_token %}
            {{ formset.management_form }}
            {% for form in formset %}
            <div class="row justify-content-left">
                <div class="col-sm-2">
                {{ form.date|as_crispy_field }}
                </div>
                <div class="col-sm-2">
                {{ form.revenue|as_crispy_field }}
                </div>
                <div class="col-sm-1">
                {{ form.probability|as_crispy_field }}
                </div>
            </div>
            <hr>
            {% endfor %}
            <div class="row justify-content-center">
                <div class="col-sm">
                    <button type="submit" class="buttons" > Submit </button>
                </div>
            </div>
        </form>
    </div>

This functionality works perfectly fine.

I created the update formset in a similar way, and I do see the data entered in the other form, but the form is not submitting the data.

@login_required
def update_forecast(request):
    ## gets the project number
    lead_id = request.GET.get('project_id')
    # Gets the lead queryset
    lead = get_object_or_404(Leads,pk=lead_id)
    #Create an inline formset using Leads the parent model and LeadEntry the child model
    FormSet = inlineformset_factory(Leads,LeadEntry,form=LeadEntryForm,extra=0)
    if request.method == "POST":
        formset = FormSet(request.POST,instance=lead)
        if formset.is_valid():
            form.save()
            return redirect('dashboard')

    context = {
        'formset':FormSet(instance=lead)
    }
    return render(request,"account/leadentry_update.html",context)

HTML view for updating an entry

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Client Information {% endblock %}

{% block content %}
  <h1> Update Forecast  </h1>
  <h3> Use the form below to update your forecast </h3>
  <br>

    <div class="container-fluid" >
        <form method="POST" enctype="multipart/form-data" >
            {% csrf_token %}
            {{ formset.management_form }}
            {% for form in formset %}
            <div class="row justify-content-left">
                <div class="col-sm-2">
                {{ form.date|as_crispy_field }}
                </div>
                <div class="col-sm-2">
                {{ form.revenue|as_crispy_field }}
                </div>
                <div class="col-sm-1">
                {{ form.probability|as_crispy_field }}
                </div>
            </div>
            <hr>
            {% endfor %}
            <div class="row justify-content-center">
                <div class="col-sm">
                    <button type="submit" class="buttons" > Submit </button>
                </div>
            </div>
        </form>
    </div>

I understand that is better to use a Class-based view, and I’m working towards that route, but I want to understand a function-based view, before jumping into class-based ( Django Class-Based Views and Inline Formset Example (github.com)

When I try to update the lead, there is no error shown, I can see a post request in the terminal:

[07/Dec/2021 10:17:58] “POST /account/update_forecast/?project_id=17 HTTP/1.1” 200 11682

And for example, if one of the fields has a value of 0.2, and I try to change it to 0.4, upon hitting submit the value goes back to 0.2

Thanks
Francisco

Upon submission, is it redirecting you to the dashboard page, or is it keeping you on the same page?

If you’re being redirected, then the formset was valid and for some reason isn’t being saved.

If you’re staying on the same page, then the if formset.is_valid() condition is False.

One item that may help is that you’re not handling the case where the formset is not valid. If the formset isn’t valid, you’re exiting that if condition, and then creating a new instance of FormSet to apply to the context to re-render the form.

Also note that in the examples at Creating forms from models | Django documentation | Django, they do not include the foreign key to the parent object in the form. Assuming that the field lead_id is the FK to Lead from LeadEntry, you may want to omit that.

Side note: As a blanket statement of a general rule, this is not true. There are a number of people who hold the exact opposite view. See Process flow in Class-based views - I just don't get it - #4 by adamchainz. CBVs are a tool - appropriate and useful in a number of circumstances, but not all.

Ken,

Thanks for the guidance, at the moment, after clicking submit, I’m staying on the same page, so I’m not been redirected to another view, indeed, the formset is somehow deemed invalid.

Regards,
FCS

Ken,

A little bit of refactoring to see the failure message if the form is not valid, which is certainly the case, and I also did a more Django view to handle the ID number.

@login_required
def update_forecast(request,lead_id):
    # Gets the lead queryset
    lead = get_object_or_404(Leads,pk=lead_id)
    #Create an inline formset using Leads the parent model and LeadEntry the child model
    FormSet = inlineformset_factory(Leads,LeadEntry,form=LeadEntryForm,extra=0)
    if request.method == "POST":
        formset = FormSet(request.POST,instance=lead)
        if formset.is_valid():
            form.save()
            return HttpResponse("Success")
        else:
            return HttpResponse("Failure")

    context = {
        'formset':FormSet(instance=lead)
    }
    return render(request,"account/leadentry_update.html",context)

Unfortunately, no data is posted to the DB

So what’s happening at this point? Are you getting a “Failure” response? If so, what are the errors? (You’re sending an HttpResponse, but you’re still not displaying / printing / showing the errors.)

Have you made other changes? (What’s your current LeadEntryForm?)

Also, even if the formset were valid, I’d expect an error to be thrown:

Your formset is named formset, but you’re trying to save something called form, which isn’t defined in view. But if you’re getting the Failure response, you’re not getting to that point.

I have now changed it to formset.save(), but as you figured out, the formset is not valid:
1.- I’m getting redirected to the failure response, simply another webpage with the word failure, as expected when using redirect
2.- There are no errors in the terminal
3.- My form remains the same:

class LeadEntryForm(forms.ModelForm):
    class Meta:
        model = LeadEntry
        fields = ('lead_id','date','revenue','probability')
        widgets = {'date': DateInput()}

I had the feeling that the lead_id should not be in the update form, and created a new one without lead_id which is simply the foreign key for LeadEntry, that connects each entry with the Lead model

class LeadUpdateForm(forms.ModelForm):
    class Meta:
        model = LeadEntry
        fields = ('date','revenue','probability')
        widgets = {'date': DateInput()}

But the view still doesn’t validate the formset

Try this:

def update_forecast(request,lead_id):
    # Gets the lead queryset
    lead = get_object_or_404(Leads,pk=lead_id)
    #Create an inline formset using Leads the parent model and LeadEntry the child model
    FormSet = inlineformset_factory(Leads,LeadEntry,form=LeadEntryForm,extra=0)

    if request.method == "POST":
        formset = FormSet(request.POST, instance=lead)
        if formset.is_valid():
            form.save()
            return HttpResponse("Success") # Change this back to your redirect
    else:
        formset = FormSet(instance=lead)

    context = {
        'formset': formset
    }
    return render(request,"account/leadentry_update.html",context)

Then, see Working with forms | Django documentation | Django for how to include rendering the errors in your form template.

This will display the errors in your form when it re-renders.

Ken,

Thanks, I can see now that the problem is that there is a hidden field in the form that is not been updated:

I have to look in the documentation on how to add that field (or how it should be updated), presumably, the form is not validated unless that value is assigned somehow.

Ken,

I can see that you have to be extremely careful when you are manually unpacking the form in Django, the automatic form {{ formset.as_p }} works, and you can delete and update, it looks awful but I guess in a way we found the culprit.

Thanks a lot
Francisco

I don’t see that you posted your LeadEntry model - it would be helpful to see that.

class LeadEntry(models.Model):
    revenue = MoneyField(decimal_places=2,max_digits=14, default_currency='USD',blank=True)
    date = models.DateField()
    lead_id = models.ForeignKey(Leads,on_delete=models.CASCADE)
    id = models.BigAutoField(primary_key=True, serialize=False)
    probability = models.DecimalField(max_digits=2, decimal_places=2, default=0, blank=True)

You showed where you created a LeadUpdateForm without that lead_id field, but your update_forecast function (and my edits) is still referencing the LeadEntryForm - the wrong form for this purpose.

Ken,

Thanks, this is the final solution for views.py

@login_required
def update_forecast(request,lead_id):
    # Gets the lead queryset
    lead = get_object_or_404(Leads,pk=lead_id)
    #Create an inline formset using Leads the parent model and LeadEntry the child model
    FormSet = inlineformset_factory(Leads,LeadEntry,form=LeadUpdateForm,extra=0)
    if request.method == "POST":
        formset = FormSet(request.POST,instance=lead)
        if formset.is_valid():
            formset.save()
            return redirect('forecast_lead_update',lead_id=lead.project_id)

    else:
        formset = FormSet(instance=lead)

    context = {
                'formset':formset
               }
    return render(request,"account/leadentry_update.html",context)

HTML page

{% extends "base.html" %}
{% load crispy_forms_tags %}
{% crispy formset %}
{% block title %}Client Information {% endblock %}

{% block content %}
  <h1> Update Forecast  </h1>
  <h3> Use the form below to update your forecast </h3>
  <br>
    <form method="POST" enctype="multipart/form-data" >
        {% csrf_token %}
        {{ formset.management_form }}
        {% for form in formset %}
        <div class="col-lg">
        <div class="card" style="width: 18rem;" >
            <div class="card-body">
                {{ form.as_p }}
            </div>
        </div>
        </div>
        {% endfor %}
        <div class="row justify-content-center">
            <div class="col-sm">
                <button type="submit" class="buttons" > Submit </button>
            </div>
        </div>
    </form>
{% endblock %}

I couldn’t workout how to use crispy form for my formset, according to the documentation you can use something similar to this:

{{ formset.management_form|crispy }}
{% for form in formset %}
    {% crispy form %}
{% endfor %}

I was able to see the delete box in the HTMLview, but the form was still not validated. I was reading a few posts on how to use crispy form with formsets, and most of them required a lot of understanding on helpers and other parts of the Django framework that I’m not very familiar as of now.