using add_error with inline formset

Django 3.2.7

I have an inline formset where I want to add_error messages to fields, in the form_valid method of UpdateView.

While I can see that these errors are indeed added to the relevant fields, they do not render in the resulting HTML. I have tested this to provoke a contraint error or required field error, and these do render, but my manually added errors do not.

The additional peculiarity is that I can use add_error on the “parent” form fields, and these also render.

…
def form_valid(self, form):
    context = self.get_context_data()
    group_formset = context['group_formset']

    if group_formset.is_valid():
        '''…'''
    else:
        '''there are two forms in the formset'''
        for theform in group_formset.forms:
            # add_error to formset form
            theform.add_error('group', 'added error')
            # add_error to parent form, renders to HTML, twice
            form.add_error('last_name', 'added error')
            # print theform in terminal shows errors added as <li> 
            # elements rendered in table – but this does not render
            # in HTML to browser
            print('(theform): ', (theform))

        # added errors are output to terminal as expected
        print(group_formset.errors)
        # added errors not present in HTML output
        return self.form_invalid(form)

Is there something else that I have to do to get the manual add_error to render?

This is going to cause your view to go to form_invalid directly - it’s never going to see your code here.

I’m thinking what you might need to do here would be to update the instance of self.form with the instance of the formset that has the error messages attached to it. My guess is that you’re working with a different instance of the forms within the formset than what’s attached to the view.

I’m also thinking that you’d be better off performing whatever validation in either the form or formset’s clean() method, throwing the errors there rather than trying to add them here.

After more than a little bit of head scratching… it turns out that the default form_invalid in UpdateView goes off and gets a fresh context (not sure what the point of that is if the form is invalid!?):

def form_invalid(self, form):
    """If the form is invalid, render the invalid form."""
    return self.render_to_response(self.get_context_data(form=form))

Calling the custom get_context_data

	def get_context_data(self, **kwargs):
		context = super(ControlMemberUpdateView, self).get_context_data(**kwargs)
		if self.request.POST:
			context['group_formset'] = ControlMemberGroupLineFormset(self.request.POST, instance=self.object)
		else:
			context['group_formset'] = ControlMemberGroupLineFormset(instance=self.object, queryset=Member_Group.objects.select_related('group').order_by('group__group_sort'))

		return context

Which effectively reloads/tests the formset and inserts the default errors, but wipes all the custom errors.

So, I overrode the form_invalid:

def form_invalid(self, context):
			return self.render_to_response(
				super(ControlMemberUpdateView, self).get_context_data(**context)
			)

Which preserves my custom errors. There seems to be some kind of error, in that one of the fields won’t display multiple errors, despite there being several mesages, and these can be displayed on the template…

That might preserve your custom errors, but it likely going to create other issues under different circumstances. I’d be working under the assumption that there are reasons for everything that is being done.

I wouldn’t be so quick to remove functionality from the generic CBVs.

I think you’re going to create fewer problems for yourself if you just move the validation into the form’s clean method so that the CBV can keep it’s standard flow of operations.

I’m not really sure I know how to do that.

See the docs at Form and field validation | Django documentation | Django for full details, but the short version is that you create a method named clean() in your form and perform validation there. If you want to validate individual fields, you create methods named clean_<fieldname>().

As far as I can tell, if you use form.add_error in the form.clean method, then that will prevent the form from saving/loading get_success_url.

The wrinkle in this (*which also seems to be a bug when using min_num in a formset) is that you do not appear to be able to block a delete action. If I have this in my clean:

if form.cleaned_data.get('DELETE') == True :
	print('form to delete')
	form.add_error('group', 'deleting this form')

Because I have added the error I’d expect the form not to delete, and not to save, but in fact it does both.

*If you use min_num in a formset, it will throw errors if you try to save without entering data for the formset, but if there are rows, and you delete them all, then there is no error!?

Correct. Because an error indicates that the form(s) were invalid. The standard mechanism at that point is to redisplay the forms with the error messages.

You “block” a delete action on a form in a formset by setting can_delete, or by handling the delete attribute on a form-by-form basis.

You validate the minimum number of forms using the validate_min attribute.

I think I am not explaining correctly.

I have a delete checkbox on forms in an inline formset, I want to be able to test this and stop the delete if certain conditions prevail.

I asumed that using add_error invalidates the form, and it should therefore invalidate the delete. But it does not – I add the error, the delete still happens.

There’s a similar question on StackOverflow here: validation - Validating delete on django-admin inline forms - Stack Overflow

At the moment I am just adding the error if there is a delete checked, without worrying about the actual conditions I want to impose on that delete. I tried using forms.ValidationError to, but that does not seemd to have an effect either.

Even though I have put in an error the form saves and the row is deleted.

Ok, my problem… the formset was not in the context when form_invalid was called.

Actually, turns out that isn’t the problem at all. In the class below, within get_context_data I needed to call context.get('group_formset').errors in order to display the errors in the group_formset.

I’ve looked at other examples of inline formsets, but haven’t seen anyone else doing that, but not clear to me either why this is required, or what it is I am doing wrong.

class ControlMemberUpdateView(UpdateView):
	model = Member
	form_class = ControlMemberUpdateForm
	template_name = 'b_manage/control/member_edit.html'
	
	
	def get_object(self, queryset=None):
		if self.kwargs.get('account_id'):
			# create a new object
			account = Account.objects.get(id=self.kwargs['account_id'])
			return Member(account=account)
		else:
			# either it is a valid get, or an error
			return super().get_object(queryset)

	
	def get_success_url(self):
		print('return parameter: ', self.request.GET.get('return'))
		if self.request.GET.get('return') == 'account' :
			return reverse_lazy('control-account-view', kwargs={'pk': self.object.account_id})
		else:
			return reverse_lazy('control-member-update', kwargs={'pk': self.object.id})
		
	
	def get_context_data(self, **kwargs):
		context = super(ControlMemberUpdateView, self).get_context_data(**kwargs)
		if self.request.POST:
			context['group_formset'] = ControlMemberGroupLineFormset(self.request.POST, instance=self.object)
			# this is required to display errors!?
			context.get('group_formset').errors
		else:
			context['group_formset'] = ControlMemberGroupLineFormset(instance=self.object, queryset=Member_Group.objects.select_related('group').order_by('group__group_sort'))

		return context
	
			
	def form_valid(self, form):
		context = self.get_context_data()
		group_formset = context['group_formset']
				
		if not group_formset.is_valid():		
			messages.error(self.request, 'There are errors with this form, please check notes below.')			
			return self.form_invalid(form)

		# in case this is a create, save the form first
		form.save()
		group_formset.save()
		
		messages.success(self.request, 'Your changes were saved.')
	
		return super(ControlMemberUpdateView, self).form_valid(form)