Why is my PK required when posting form

Hi everyone, hope you’re having a good day !

My name is Alexis and im having trouble with a modelformset_factory.
i will try to explain as much as i can.

Context: I have two models involved in this case, linked by a ManyToMany relationship declared in the GfcRecette model via num_dossier = models.ManyToManyField(Dossier):

class Dossier(models.Model):
    num_dossier = models.AutoField(primary_key=True)
    statut = models.ForeignKey(Statut, on_delete=models.CASCADE, default=1)
    actif = models.BooleanField(max_length=100, default=1)
    ...

class GfcRecette(models.Model):
    ANNEES = [(str(a), str(a)) for a in range(2010, date.today().year + 1)]

    id_gfc_recette = models.AutoField(primary_key=True)
    num_dossier = models.ManyToManyField(Dossier)
    libelle_facture = models.CharField(max_length=300)
    ...

Here’s how i declare my formset in my view :

GfcFormset = modelformset_factory(
        GfcRecette,
        form=GfcRecetteForm,
        extra=1,
        exclude=['num_dossier']
    )

Im using JS to add more forms in my formset (it duplicate my first form and adjust fields id’s)
My page that allows creating or modifying one of these forms is the same.
When I POST my Dossier and GfcRecette forms for the first time, everything works.
When I’m on the page with an existing dossier ID, the Dossier and GfcFormset forms instantiate correctly and appear as expected.
If I modify an element of the Dossier form and POST the modification, it works.
If I add an empty form to the GfcFormset, it adds successfully.
If I add a form to the GfcFormset with data, it adds, but no data is present, and the error is displayed:

  • id_gfc_recette
    • This field is required.
If I modify an element of a GfcRecette form (from the formset), no modification is applied, and the same error appears:
  • id_gfc_recette
    • This field is required.

As a reminder, the field “id_gfc_recette” is the primary key of the GfcRecette model. It seems like it should be managed automatically by Django, so I don’t understand why it is lost and requested by the form."
I tried to print primary keys of my GfcForms (forms in my formset) when i access my view :

  • When i access my view with instancied forms, primary keys are existing and are printed
  • When i access my view with instancied forms, i modify something in any of my formset and POST it, i can see in logs, all my primary keys are none. Then page load again and these Pk’s appears.
    It looks like POSTing is making them disapear. I am lost …

Thx for reading me,
Alexis

When you are creating the new instance, are you also copying the id_gfc_recette field? If so, are you removing any value that may be included in it? (A new entry would need to have that as a hidden input field, but with no value.)

You might want to look at the html that was created from this to visually verify that the new form instance is correct.

1 Like

Hi, this is solving a big part of my problem, i only had to add ‘id_gfc_recette’: forms.HiddenInput() in my form declaration, and {% bootstrap_field form.id_gfc_recette %} in my html.

It took my some time to reply you because i have one last problem linked to this one and i tried many things before asking for more.

Now i can modify anything in my formset and it’s saved when i POST it, but, if i add a form on my page (with my JS, i have a button to do that), i can see in my debugger, in request.POST all my form / data is here, but it only save an empty form, with a well generated id_gfc_recette.
Then when my view load again i can see this new empty form and modify it if i want.

This is how i do it in my view :

          for form in formset:
              if form.is_valid():
                  gfc_recette = form.save()
                  gfc_recette.num_dossier.add(dossier)

So i don’t understand what i am missing here, everything looks ok, but it’s not haha

We would need to see the template and the code you’re using to add rows. I’m guessing that there’s still something wrong with the JavaScript. Either an element name isn’t right or the management form hasn’t been properly updated. (That’s usually the case about 80% of the time.)

It might also be helpful to see the rendered formset HTML from the browser as a “before” and “after” comparison.

1 Like

Hi,
(im sorry, since im a new user i can only add one image, i screened 6 in total but can’t put them all)
So there is my JS to add more forms to my formset :

function cloneMore(selector, prefix) {
    $(selector).find('select').selectpicker('destroy');
    var total = $('#id_' + prefix + '-TOTAL_FORMS').val();
    console.log("total:"+total)
    console.log("prefix:"+prefix)
    var newElement = $(selector).clone(true);
    newElement.find(':input:not([type=button]):not([type=submit]):not([type=reset])').each(function() {
        if(typeof($(this)) === 'object') {
            var name = $(this).attr('name')
            if (name !== undefined) {
                name = name.replace(new RegExp('-\\d+-'), '-' + total + '-');
                var id = 'id_' + name;
                $(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
            }
        } else return false
    });
    newElement.find('label').each(function() {
        var forValue = $(this).attr('for');
        if (forValue) {
            forValue = forValue.replace('-' + (total-1) + '-', '-' + total + '-');
            $(this).attr({'for': forValue});
        }
    });
    total++;
    $('#id_' + prefix + '-TOTAL_FORMS').val(total);
    // Ici, vous devez actualiser les éléments select avec SelectPicker
    newElement.find('select').each(function() {
        $(this).selectpicker({liveSearch: true, liveSearchPlaceholder: 'Rechercher', noneResultsText: 'Pas de résultat pour la recherche {0}'});
    });

    $(selector).after(newElement);
    var conditionRow = $('.form-row:not(:last)');
    conditionRow.find('.btn.add-form-row')
        .removeClass('btn-success').addClass('btn-danger')
        .removeClass('add-form-row').addClass('remove-form-row')
        .html('<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>');
    conditionRow.find('.btn.remove-form-row').off('click');
    conditionRow.find('.btn.remove-form-row').on('click', function(e){
        e.preventDefault();
        deleteForm('form', $(this));
        return false;
    });
    return false;
}

My HTML code for this formset :

<div class="bg-info">
                                {{ formset.management_form }}
                                {% for form in formset %}
                                    <div class="row form-row spacer">
                                        <div class="champ-container">
                                            {% bootstrap_field form.id_gfc_recette %}
                                            {% bootstrap_field form.exercice %}
                                            {% bootstrap_field form.num_recette %}
                                            {% bootstrap_field form.num_facture %}
                                            {% bootstrap_field form.libelle_facture %}
                                            {% bootstrap_field form.num_titre %}
                                            {% bootstrap_field form.recette_reduite %}
                                            {% bootstrap_field form.ht_facture %}
                                            {% bootstrap_field form.ht_recette %}
                                            {% bootstrap_field form.ligne_budget %}
                                            {% bootstrap_field form.etat_gfc %}
                                            {% bootstrap_field form.isRapprocher %}
                                            {% bootstrap_field form.isEmarger %}
                                            <button name="button-0-exercice" class="btn btn-success add-form-row cloneable-row">+</button>
                                        </div>
                                    </div>
                                {% endfor %}
                                <div class="row spacer">
                                </div>
                            </div>

(my JS function is trigerred by this button)

So for example, here i have one form in my formset (already saved, instancied on my page), and another generated by my button and filled with some data :
In debug mode, i can see in my request.POST, both my form fields are here and filled with data :
When i check in debugger my first form, i got it, but then i step over to my second form, and it’s empty :
Then my view is reloading and my first form is still here (and if i modified it it would have worked), but my new added form is here but empty :

If you need anymore information let me know, and again, thx for your help,

Alexis

It might be helpful if you posted the html for the complete formset as it appears in the browser before and after a new form was added. And, if you’re seeing this data in your request.POST, it may also be helpful to post the raw POST data being issued by the browser as shown in the network tab of the developer tools.

1 Like

Hi Ken,

When i was replying to you i found the solution.
My first form id is “id_form-0” and the new one jumps to “id_form-2” (1 is missing and we need it).
So i adjusted my JS and it works like charm now !

Thx a lot for your help, i was lost and i didn’t know how to find the solution.