Code

Opened 4 months ago

Closed 3 weeks ago

Last modified 3 weeks ago

#21596 closed New feature (wontfix)

Add method to formset to add a form

Reported by: nickname123 Owned by: nobody
Component: Forms Version: 1.6
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

It is very complicated to add a form to a formset with the formset api.

It should be simple to do without javascript.

It would be nice if there was an add_form method on the formset that added a empty form to formset.forms.

Attachments (0)

Change History (6)

comment:1 Changed 4 months ago by timo

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

I'm not sure I understand the problem or proposal. You mention JavaScript which makes me think you're adding a form on the client side, but then you propose an add_form method which I assume is on the server?

comment:2 Changed 4 months ago by nickname123

I will elaborate. Sorry for the confusing description.

Right now, there is no easy way to add a form to a formset that I could find documented. The formset api doesn't have anything to assist with this internally that I could find either.

My use case is similar to the django admin where a user can click "add another". The formset documentation suggests that this should be done client side by javascript: https://docs.djangoproject.com/en/dev/topics/forms/formsets/#empty-form

I think this should be possible to implement server side.

This is a simple mixin that demonstrates my intentions:

class AddableFormSet(BaseModelFormSet):
    can_add_form = True
    
    def add_form(self, **kwargs):
        if self.is_valid():
            # add the form
            tfc = self.total_form_count()
            form = self._construct_form(tfc, **kwargs)
            form.is_bound = False
            self.forms.append(form)
            
            # make data mutable
            self.data = self.data.copy()
    
            # increase hidden form counts
            total_count_name = '%s-%s' % (self.management_form.prefix, TOTAL_FORM_COUNT)
            self.data[total_count_name] = self.management_form.cleaned_data[TOTAL_FORM_COUNT] + 1
    
    @property
    def add_form_key(self):
        return self.add_prefix(ADD_FORM_KEY)

Now, it is easy to hook up in the view:

class CustomWizard(NamedUrlSessionWizardView):
	def get_form(self, step=None, data=None, files=None):
		# could be a form or formset
		formish = super(CustomWizard, self).get_form(step=step, data=data, files=files)
		
		# support adding forms to formsets if there is post data
		if data:
			if isinstance(formish, BaseModelFormSet) and formish.can_add_form:
				if formset.add_form_key in data:
					formset.add_form()

Now, in the template you just need something like this:

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

{% if formset.can_add_form %}
	<hr />
    <input name="{{ formset.add_form_key }}" type="submit" value="Add another" />
	<hr />
{% endif %}

This really makes the formset more useful to me. It is simple to implement and now doesn't require javascript to implement the "add another" button.

It took me a pretty long time to figure out the internals to implement this so I doubt someone new to the framework would be able to. This seems like a logical addition to the formset api. (aside: I chose to require is_valid() in the add_form() method because it would be frustrating for the user to create 15 forms to find out they are all invalid. The way I have implemented it supports multiple formsets on the same page. Which is important for my use case that mixes individual forms and formsets in a "form container")

It is fairly straightforward and I think that any formset used for data entry could make use of this. So I think it would be useful to almost anyone using a formset.

comment:3 Changed 3 weeks ago by timo

  • Resolution set to wontfix
  • Status changed from new to closed

This seems a bit over-engineered to me. Couldn't you accomplish something similar by passing extra=1 to the formset_factory and then simply reload the page on submit to get another form for your formset?

comment:4 Changed 3 weeks ago by nickname123

Maybe my solution isn't what django should go with, but you really don't see the utility in a method that adds another form to an existing formset?

I wasn't trying to propose my hack as the way to do it. I just posted it to demonstrate the concept. I pieced it together quickly from searching documentation and google without finding any clear way to add a form to an existing formset. And there currently isn't a straight forward way to achieve this

comment:5 follow-up: Changed 3 weeks ago by timo

Why not the solution I proposed?

comment:6 in reply to: ↑ 5 Changed 3 weeks ago by nickname123

Replying to timo:

Why not the solution I proposed?

It does not seem intuitive to me, but it seems like your proposed solution would work similarly in some situations but not all.

My major problem with it is that it requires all formsets to be created dynamically when needed and you can only specify to include an extra form initially when the factory function is called. This seems like a very complicated path to take just to avoid adding a small change to the formset class. Maybe this is a bad comparison, but it seems kind of like having a list [] that you must specify how many elements are required in advance vs having an append method.

    def add_form(self, **kwargs):
        self.forms.append(self._construct_form(self.total_form_count(), **kwargs))
        self.forms[-1].is_bound = False
        self.data = self.data.copy()
        self.data['%s-%s' % (self.management_form.prefix, TOTAL_FORM_COUNT)] = self.management_form.cleaned_data[TOTAL_FORM_COUNT] + 1

Note that TOTAL_FORM_COUNT is a key string imported from django.forms.formsets, not something I calculate elsewhere.

My change to the formset class only needs to add 5 lines if the comments/verboseness isn't important. And it makes adding forms to an existing formset possible and much more flexible. The is_valid() check probably shouldn't be added to the main formset class. That was just for a particular use case I wrote the mixin for.

I prefer to call the formset_factory method once and use it like a form class definition that I import elsewhere for readability. My preference probably isn't important to Django, but I figured I would mention it.

I.e.

...
OptionalPhoneNumberModelFormset = modelformset_factory(PhoneNumber, form=PhoneNumberModelForm, formset=EntryOnlyAddableFormSet)
FirstRequiredPhoneNumberModelFormset = modelformset_factory(PhoneNumber, form=PhoneNumberModelForm, formset=FirstRequiredEntryOnlyAddableFormSet)
...

For another case where the add_form method would be preferable see: https://code.djangoproject.com/ticket/18830

I am using a version of russelm's formcontainer that I have been working on and it is much easier to use the "add_form" method than recreating the formcontainer and its children when an additional form needs to be added to one of the child formsets.

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.