Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: How to add an extra form to a formset after it has been constructed?

This is roughly what I'm trying to do:

def post(request):
    VehicleFormSet = formset_factory(StaffVehicleForm)
    if request.method == 'POST':
        vehicle_formset = VehicleFormSet(request.POST)
        if 'add_vehicle' in request.POST:
            if vehicle_formset.is_valid():
                form_count = vehicle_formset.total_form_count()
                vehicle_formset.forms.append(vehicle_formset._construct_form(form_count))

Basically, if a user clicks the "Add" button and their entry is valid, I want to add another blank form to the formset, and hide the previous one.

The problem with the code above is that I can't figure out how to increase total_form_count(). The way I have it now, it will work once, and then if you press it again, nothing will happen, presumably because form_count is the same. I also don't like calling _construct_form and relying on the internals.

like image 373
mpen Avatar asked Feb 09 '11 21:02

mpen


People also ask

What is inline formset in django?

Django formset allows you to edit a collection of the same forms on the same page. It basically allows you to bulk edit a collection of objects at the same time.


4 Answers

class RequiredFormSet(BaseFormSet):
    def add_form(self, **kwargs):
        # add the form
        tfc = self.total_form_count()
        self.forms.append(self._construct_form(tfc, **kwargs))
        self.forms[tfc].is_bound = False

        # make data mutable
        self.data = self.data.copy()

        # increase hidden form counts
        total_count_name = '%s-%s' % (self.management_form.prefix, TOTAL_FORM_COUNT)
        initial_count_name = '%s-%s' % (self.management_form.prefix, INITIAL_FORM_COUNT)
        self.data[total_count_name] = self.management_form.cleaned_data[TOTAL_FORM_COUNT] + 1
        self.data[initial_count_name] = self.management_form.cleaned_data[INITIAL_FORM_COUNT] + 1

    def add_fields(self, form, index):
        super(RequiredFormSet, self).add_fields(form, index)
        form.empty_permitted = False

That will do it. Only took 7 hours to figure out. And I still don't know why I need .is_bound = False to make the initial values not screw up.

like image 72
mpen Avatar answered Oct 08 '22 16:10

mpen


I do this using javascript. Since the formset renders three management fields

<input type="hidden" id="id_TOTAL_FORMS" value="1" name="TOTAL_FORMS">
<input type="hidden" id="id_INITIAL_FORMS" value="1" name="INITIAL_FORMS">.
<input type="hidden" id="id_MAX_NUM_FORMS" name="MAX_NUM_FORMS">

you can use javascript to increment the id_TOTAL_FORMS value, and just add in the extra fields. So I'd create my fieldset like this:

VehicleFormSet = modelformset_factory(StaffVehicleForm, extra = 0, max_num = None)

The tricky thing is to create the extra form fields in javascript. I usually use AJAX to fetch a new row from a custom view.

like image 41
Humphrey Avatar answered Oct 08 '22 17:10

Humphrey


For posterity here is another way which works without JS (or alongside JS) and which does not require intimate knowledge of formset methods. Instead, you can just inspect the POST data and adjust it as if JS had done some work client-side. The following makes sure that there is always (at least) one empty form at the end of the formset:

def hsview( request):
    HS_formset = formset_factory( HSTestForm, extra=3 )
    prefix='XYZZY'
    testinpost, empty = 'key', ''  # field in the form and its default/empty value
    extra=3

# I prefer to do the short init of unbound forms first, so I invert the usual test ...   
    if request.method != 'POST':

        formset = HS_formset( prefix=prefix)
    else:
       # process POSTed forms data. 
       # pull all relevant things out of POST data, because POST itself is not mutable
        # (it doesn't matter if prefix allows in extraneous items)

        data = { k:v for k,v in request.POST.items() if k.startswith(prefix) } 

        #if there are no spare empty forms, tell it we want another form, in place of or extra to client-side JS
        #don't want to crash if unvalidated POST data is nbg so catch all ...
        try:
            n = int( data[ prefix + '-TOTAL_FORMS']) 
            test = '{}-{}-{}'.format(prefix, n-1, testinpost)
            #print(test)
            test = data.get( test, empty)
        except Exception:
            test = 'bleagh'
            # log the error if it matters enough ...
        if test != empty: 
            data[ prefix + '-TOTAL_FORMS'] = n + 1 

        # now the usual formset processing ...
        formset = HS_formset( data, prefix=prefix)
        # other_form = OtherForm( request.POST)
        if formset.is_valid(): 
            ...            
like image 1
nigel222 Avatar answered Oct 08 '22 15:10

nigel222


I use RegEx in my Vue.js method:

addForm: function () {
    this.count++
    let form_count = this.count
    form_count++

    let formID = 'id_form-' + this.count
    incremented_form = this.vue_form.replace(/form-\d/g, 'form-' + this.count)
    this.formList.push(incremented_form)
    this.$nextTick(() => {
        let total_forms = document.getElementsByName('form-TOTAL_FORMS').forEach
        (function (ele, idx) {
            ele.value = form_count
        })
    })
},

delForm: function () {
    if (this.count != 0) {
        this.count--
        let form_count = this.count
        form_count++

        let formID = 'id_form-' + this.count
        this.formList.pop()
        this.$nextTick(() => {
            let total_forms = document.getElementsByName('form-TOTAL_FORMS').forEach
            (function (ele, idx) {
                ele.value = form_count
            })
        })
    }
    else return
},
like image 1
Shane Cheek Avatar answered Oct 08 '22 17:10

Shane Cheek