Invalid forms in formset with initial values

Model:

class ParticipantForm(forms.ModelForm): 
	member_name = forms.CharField(disabled=True, max_length=40, widget=forms.TextInput(attrs={'class': 'event-table-data'}))
	status = forms.ChoiceField(choices=StatusChoices, widget=forms.Select(attrs={'class': 'popup-select-button'}))
	comment = forms.CharField(max_length=400, widget=forms.Textarea(attrs={'class': 'card-textarea'}))

	class Meta:		
		model = Participant
		fields = ["member_name", "status","comment"]

View:

def event_edit(request, roster_id, event_id): # todo: just get event from roster?

	event = get_object_or_404(Event, pk=event_id)
	roster = get_object_or_404(Roster, pk=roster_id)

	for p in event.participants.all():
		print("--- member_name: {}, status: {}, comment: {}".format(p.member_name, p.status, p.comment))

	ParticipantFormSet = formset_factory(ParticipantForm, extra=0)
	form_values = event.participants.all().values()
	print("--- form values: ", form_values)

	form_data = {'form-TOTAL_FORMS': len(form_values),'form-INITIAL_FORMS': '0'}
	formset = ParticipantFormSet(form_data, initial=form_values)

	for form in formset:
		print("--- form.is_valid: ", form.is_valid())
		print("--- form: ", form)

	context = {
		'roster': roster, 
		'event': event,
		'event_details_form': EventDetailsForm(instance=event),
		'participant_formset': ParticipantFormSet(form_data, initial=form_values),
		'status_choices': StatusChoices
		}
	return render(request,'tss/event_edit.html',context)

Result: each of the forms is invalid, due to “This field is required.” errors. The form_values var, used for the initial= parameter, looks correct. Not sure what’s going on here.

We would need more information about the errors. Which field(s) does it say is/are required?

You are not binding data to the form. The “initial” data represents what you’re sending out to the browser, not what gets returned. What comes back from the browser is bound to the form. See the docs at Working with forms | Django documentation | Django

Also, this is a ModelForm you are using, this means you should be using a Model formset and not a regular formset. (One of the features of the model formset is that it automatically includes the primary key in the form, so that bound data gets saved to the correct object.)

1 Like

I changed to modelformset_factory, and also added the ‘fields’ parameter. Now I get 'ModelFormOptions' object has no attribute ‘private_fields’. Formset is a minefield


def event_edit(request, roster_id, event_id): 

event = get_object_or_404(Event, pk=event_id)
roster = get_object_or_404(Roster, pk=roster_id)

ParticipantFormSet = modelformset_factory(ParticipantForm, fields=["member_name", "status", "comment"], extra=0)
form_values = event.participants.all().values()

form_data = {'form-TOTAL_FORMS': len(form_values),'form-INITIAL_FORMS': '0'}
formset = ParticipantFormSet(form_data, initial=form_values)

context = {
	'roster': roster, 
	'event': event,
	'event_details_form': EventDetailsForm(instance=event),
	'participant_formset': ParticipantFormSet(form_data, initial=form_values),
	'status_choices': StatusChoices
	}
return render(request,'tss/event_edit.html',context)

Please post the complete model and form.

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']

	def __str__(self):
		return self.member_name + " at " + self.event.location + ", " + str(self.event.date_time.strftime("%A, %b %d"))

class ParticipantForm(forms.ModelForm): 
	member_name = forms.CharField(disabled=True, max_length=40, widget=forms.TextInput(attrs={'class': 'event-table-data'}))
	status = forms.ChoiceField(choices=StatusChoices, widget=forms.Select(attrs={'class': 'popup-select-button'}))
	comment = forms.CharField(max_length=400, widget=forms.Textarea(attrs={'class': 'card-textarea'}))

	class Meta:		
		model = Participant
		fields = ["member_name", "status","comment"]

The calling signature for modelformset_factory is different than that for formset_factory. Also see the docs for Model formsets to see more of the differences.

The calling signature for modelformset_factory is different than that for formset_factory.

That doesn’t tell me anything useful. The signature for modelformset_factory doesn’t include private_fields. Do I need to specify a BaseModelFormset? Obscure.

modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True, renderer=None, edit_only=False)[source]¶

Note the difference between this:

formset_factory(form, ...

and this:

Thanks, Ken. My mistake
 after your previous post, I added the model parameter, but it had no effect.

ParticipantFormSet = modelformset_factory(Participant, ParticipantForm, fields=[“member_name”, “status”, “comment”], extra=0)

I’m still getting the ‘This field is required’ error. Strangely, it only happens with two of the three fields. Here’s the model and form again:

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']

class ParticipantForm(forms.ModelForm): 
	member_name = forms.CharField(disabled=True, max_length=40, widget=forms.TextInput(attrs={'class': 'event-table-data'}))
	status = forms.ChoiceField(choices=StatusChoices, widget=forms.Select(attrs={'class': 'popup-select-button'}))
	comment = forms.CharField(max_length=400, widget=forms.Textarea(attrs={'class': 'card-textarea'}))

	class Meta:		
		model = Participant
		fields = ["member_name", "status","comment"]

And the view:

def event_edit(request, roster_id, event_id):

	event = get_object_or_404(Event, pk=event_id)
	roster = get_object_or_404(Roster, pk=roster_id)

	ParticipantFormSet = modelformset_factory(Participant, form=ParticipantForm, fields=["member_name", "status", "comment"], extra=0)
	if request.method == "GET":
		form_values = event.participants.all().values()
		form_data = {'form-TOTAL_FORMS': len(form_values),'form-INITIAL_FORMS': '0'}
		formset = ParticipantFormSet(form_data, initial=form_values)
		
		print("form_values: ", form_values) # all fields (member_name, status, comment) have correct values
		print("formset: ", formset) # member_name is populated, but values for status and comment are missing, resulting in "This field is required"

		context = {
			'roster': roster, 
			'event': event,
			'event_details_form': EventDetailsForm(instance=event),
			'participant_formset': formset,
			'status_choices': StatusChoices
			}
		return render(request,'tss/event_edit.html',context)

As the comment says, this shows that the three model fields have correct values, but the second and third form fields don’t.

As written above:

You’re only binding two fields of the management form. And you can’t just bind the queryset for the formset, either. The data within the queryset needs to be bound to the form elements, which in a formset have a name attribute derrived from the prefix, form number and field name.

In the general case, you don’t bind data on a GET method anyway. You usually want to allow the formset to generate itself. Don’t try to overmanage it - let Django do its work and take advantage of what it does for you.

Finally, your print statement is misleading. Printing form_values provides no information regarding the status of the form. You’re printing a queryset and not a form.

Dang. I had originally searched around the web for how to provide a queryset to a formset, and found (bad) advice on passing the initial data, etc.

So now I have:

ParticipantFormSet = modelformset_factory(Participant, form=ParticipantForm, fields=["member_name", "status", "comment"], extra=0)
formset = ParticipantFormSet(queryset=event.participants.filter(status__in=["Invited", "Confirmed"]))

Which seems to work.

Printing form_values provides no information regarding the status of the form. You’re printing a queryset and not a form.

That was just to verify that the “initial” parameter was getting the right data (form_values is a dictionary created from the queryset).