Curious formset behaviors

Some very curious formset behaviors:

Case 1: this works as expected, i.e. it validates and saves the changes to the form:

<form method="post">
	{% csrf_token %}

	{{ formset.management_form }}

	{{ formset.as_p }}
   
	<input type="submit" value="Submit">

</form>

Case 2: this fails with invalid formset: [{'member_name': ['This field is required.'], 'id': ['This field is required.']}, ... Note that member_name is displayed on the form prior to submission.

<form method="post">
	{% csrf_token %}

	{{ formset.management_form }}

	<table>
		{% for form in formset %}
			<tr>
				<td>{{form.member_name}}</td>
				<td style="padding-right: 16px;">{{form.status}}</td>
				<td>{{form.comment}}</td>
			</tr>
		{% endfor %}
	</table>
   
	<input type="submit" value="Submit">

</form>

Case 3: this generates two views of the formset; the second one works as expected, but the first one has no effect: no values are changed, though there’s no form errors:

<form method="post">
	{% csrf_token %}

	{{ formset.management_form }}

	{{ formset.as_p }} <hr>
	{{ formset.as_p }}

	<input type="submit" value="Submit">

</form>

Here’s the model:

class Participant(models.Model):
	member_name = models.CharField(max_length=50, null=False, blank=False, help_text="Member name")
	event = models.ForeignKey(Event, on_delete=models.CASCADE, null=False, related_name="participants", help_text="The event, e.g. a tennis session or match")
	status = models.CharField(max_length=50, choices=StatusChoices, default="Unknown")
	comment = models.CharField(max_length=400, blank=True, null=True, default="a comment", help_text="A comment made by a member e.g. about participant in the specific event")

	class Meta:
		ordering = ['member_name']

And the view:

def formset_test(request, event_id):

	event = get_object_or_404(Event, pk=event_id)

	ParticipantFormSet = modelformset_factory(Participant, form=ParticipantForm, fields=["id", "member_name", "status", "comment"], extra=0)
	formset = ParticipantFormSet()

	if request.method == 'POST':

		formset = ParticipantFormSet(request.POST)
		if formset.is_valid():
			formset.save()
		else:
			print("invalid formset: ", formset.errors) 
	else:
		formset = ParticipantFormSet()
	return render(request,'formset_test.html',{'formset': formset, 'event': event})

These are neither curious nor unexpected.

A model formset includes the id field as a hidden-input element within the HTML. You’re not rendering and submitting an id field, and so validation will fail.

Correct, because you’re creating two duplicate forms, which means the second entries overwrite the first.

This all becomes a lot easier to understand if you look at the rendered HTML in each of these cases and examine the data being submitted by the POST action.

A model formset includes the id field as a hidden-input element within the HTML. You’re not rendering and submitting an id field, and so validation will fail.

I don’t get it. I specified the id field in the modelformset_factory, if that’s what you’re referring to. As for rendering it, why would I? (and I just tried it, it’s of course not visible and doesn’t change anything). And I don’t understand what ’submitting an id field’ means. An example explaining those two things would help.

Correct, because you’re creating two duplicate forms, which means the second entries overwrite the first.

That’s non-obvious, to say the least.

Look at the generated HTML of your original (working) version. Then compare that to what you’re generating. Identify the differences.

Then, use your browser’s developer tools to examine what the forms are submitting in each case. Notice what’s different between the two.

That’s going to provide you a much better example than what I can do to explain here.

Consider any typical programming language, in this case, Python.
If you write:

x = 1
x = 2

what is the value of x going to be after these two lines of code?