Pre-populate an inline FormSet?

Question:

I’m working on an attendance entry form for a band. My idea is to have a section of the form to enter event information for a performance or rehearsal. Here’s the model for the event table:

class Event(models.Model):
    event_id = models.AutoField(primary_key=True)
    date = models.DateField()
    event_type = models.ForeignKey(EventType)
    description = models.TextField()

Then I’d like to have an inline FormSet that links the band members to the event and records whether they were present, absent, or excused:

class Attendance(models.Model):
    attendance_id = models.AutoField(primary_key=True)
    event_id = models.ForeignKey(Event)
    member_id = models.ForeignKey(Member)
    attendance_type = models.ForeignKey(AttendanceType)
    comment = models.TextField(blank=True)

Now, what I’d like to do is to pre-populate this inline FormSet with entries for all the current members and default them to being present (around 60 members). Unfortunately, Django doesn’t allow initial values in this case.

Any suggestions?

Asked By: Fred Larson

||

Answers:

So, you’re not going to like the answer, partly because I’m not yet done writing the code and partly because it’s a lot of work.

What you need to do, as I discovered when I ran into this myself, is:

  1. Spend a lot of time reading through the formset and model-formset code to get a feel for how it all works (not helped by the fact that some of the functionality lives on the formset classes, and some of it lives in factory functions which spit them out). You will need this knowledge in the later steps.
  2. Write your own formset class which subclasses from BaseInlineFormSet and accepts initial. The really tricky bit here is that you must override __init__(), and you must make sure that it calls up to BaseFormSet.__init__() rather than using the direct parent or grandparent __init__() (since those are BaseInlineFormSet and BaseModelFormSet, respectively, and neither of them can handle initial data).
  3. Write your own subclass of the appropriate admin inline class (in my case it was TabularInline) and override its get_formset method to return the result of inlineformset_factory() using your custom formset class.
  4. On the actual ModelAdmin subclass for the model with the inline, override add_view and change_view, and replicate most of the code, but with one big change: build the initial data your formset will need, and pass it to your custom formset (which will be returned by your ModelAdmin‘s get_formsets() method).

I’ve had a few productive chats with Brian and Joseph about improving this for future Django releases; at the moment, the way the model formsets work just make this more trouble than it’s usually worth, but with a bit of API cleanup I think it could be made extremely easy.

Answered By: James Bennett

I came accross the same problem.

You can do it through JavaScript, make a simple JS that makes an ajax call for all the band memebers, and populates the form.

This solution lacks DRY principle, because you need to write this for every inline form you have.

Answered By: Danni

Here is how I solved the problem.
There’s a bit of a trade-off in creating and deleting the records, but the code is clean…

def manage_event(request, event_id):
    """
    Add a boolean field 'record_saved' (default to False) to the Event model
    Edit an existing Event record or, if the record does not exist:
    - create and save a new Event record
    - create and save Attendance records for each Member
    Clean up any unsaved records each time you're using this view
    """
    # delete any "unsaved" Event records (cascading into Attendance records)
    Event.objects.filter(record_saved=False).delete()
    try:
        my_event = Event.objects.get(pk=int(event_id))
    except Event.DoesNotExist:
        # create a new Event record
        my_event = Event.objects.create()
        # create an Attendance object for each Member with the currect Event id
        for m in Members.objects.get.all():
            Attendance.objects.create(event_id=my_event.id, member_id=m.id)
    AttendanceFormSet = inlineformset_factory(Event, Attendance, 
                                        can_delete=False, 
                                        extra=0, 
                                        form=AttendanceForm)
    if request.method == "POST":
        form = EventForm(request.POST, request.FILES, instance=my_event)
        formset = AttendanceFormSet(request.POST, request.FILES, 
                                        instance=my_event)
        if formset.is_valid() and form.is_valid():
            # set record_saved to True before saving
            e = form.save(commit=False)
            e.record_saved=True
            e.save()
            formset.save()
            return HttpResponseRedirect('/')
    else:
        form = EventForm(instance=my_event)
        formset = OptieFormSet(instance=my_event)
    return render_to_response("edit_event.html", {
                            "form":form, 
                            "formset": formset,
                            }, 
                            context_instance=RequestContext(request))
Answered By: daan

I spent a fair amount of time trying to come up with a solution that I could re-use across sites. James’ post contained the key piece of wisdom of extending BaseInlineFormSet but strategically invoking calls against BaseFormSet.

The solution below is broken into two pieces: a AdminInline and a BaseInlineFormSet.

  1. The InlineAdmin dynamically generates an initial value based on the exposed request object.
  2. It uses currying to expose the initial values to a custom BaseInlineFormSet through keyword arguments passed to the constructor.
  3. The BaseInlineFormSet constructor pops the initial values off the list of keyword arguments and constructs normally.
  4. The last piece is overriding the form construction process by changing the maximum total number of forms and using the BaseFormSet._construct_form and BaseFormSet._construct_forms methods

Here are some concrete snippets using the OP’s classes. I’ve tested this against Django 1.2.3. I highly recommend keeping the formset and admin documentation handy while developing.

admin.py

from django.utils.functional import curry
from django.contrib import admin
from example_app.forms import *
from example_app.models import *

class AttendanceInline(admin.TabularInline):
    model           = Attendance
    formset         = AttendanceFormSet
    extra           = 5

    def get_formset(self, request, obj=None, **kwargs):
        """
        Pre-populating formset using GET params
        """
        initial = []
        if request.method == "GET":
            #
            # Populate initial based on request
            #
            initial.append({
                'foo': 'bar',
            })
        formset = super(AttendanceInline, self).get_formset(request, obj, **kwargs)
        formset.__init__ = curry(formset.__init__, initial=initial)
        return formset

forms.py

from django.forms import formsets
from django.forms.models import BaseInlineFormSet

class BaseAttendanceFormSet(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        """
        Grabs the curried initial values and stores them into a 'private'
        variable. Note: the use of self.__initial is important, using
        self.initial or self._initial will be erased by a parent class
        """
        self.__initial = kwargs.pop('initial', [])
        super(BaseAttendanceFormSet, self).__init__(*args, **kwargs)

    def total_form_count(self):
        return len(self.__initial) + self.extra

    def _construct_forms(self):
        return formsets.BaseFormSet._construct_forms(self)

    def _construct_form(self, i, **kwargs):
        if self.__initial:
            try:
                kwargs['initial'] = self.__initial[i]
            except IndexError:
                pass
        return formsets.BaseFormSet._construct_form(self, i, **kwargs)

AttendanceFormSet = formsets.formset_factory(AttendanceForm, formset=BaseAttendanceFormSet)
Answered By: Erik Karulf

Django 1.4 and higher supports providing initial values.

In terms of the original question, the following would work:

class AttendanceFormSet(models.BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        super(AttendanceFormSet, self).__init__(*args, **kwargs)
        # Check that the data doesn't already exist
        if not kwargs['instance'].member_id_set.filter(# some criteria):
            initial = []
            initial.append({}) # Fill in with some data
            self.initial = initial
            # Make enough extra formsets to hold initial forms
            self.extra += len(initial)

If you find that the forms are being populated but not being save then you may need to customize your model form. An easy way is to pass a tag in the initial data and look for it in the form init:

class AttendanceForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(AttendanceForm, self).__init__(*args, **kwargs)
        # If the form was prepopulated from default data (and has the
        # appropriate tag set), then manually set the changed data
        # so later model saving code is activated when calling
        # has_changed().
        initial = kwargs.get('initial')
        if initial:
            self._changed_data = initial.copy()

    class Meta:
        model = Attendance
Answered By: ruiseal

Just override “save_new” method, it worked for me in Django 1.5.5:

class ModelAAdminFormset(forms.models.BaseInlineFormSet):
    def save_new(self, form, commit=True):
        result = super(ModelAAdminFormset, self).save_new(form, commit=False)
        # modify "result" here
        if commit:
            result.save()
        return result
Answered By: Erwin

Using django 1.7 we ran into some issues creating an inline form with additional context baked into the model (not just an instance of the model to be passed in).

I came up with a different solution for injecting data into the ModelForm being passed in to the form set. Because in python you can dynamically create classes, instead of trying to pass in data directly through the form’s constructor, the class can be built by a method with whatever parameters you want passed in. Then when the class is instantiated it has access to the method’s parameters.

def build_my_model_form(extra_data):
    return class MyModelForm(forms.ModelForm):
        def __init__(self, *args, **kwargs):
            super(MyModelForm, self).__init__(args, kwargs)
            # perform any setup requiring extra_data here

        class Meta:
            model = MyModel
            # define widgets here

Then the call to the inline formset factory would look like this:

inlineformset_factory(ParentModel, 
                      MyModel, 
                      form=build_my_model_form(extra_data))
Answered By: Terhands

I ran into this question -6 years later- , and we are on Django 1.8 now.

Still no perfectly clean , short answer to the question.

The issue lies in the ModelAdmin._create_formsets() github ;
My Solution is to override it, and inject the initial data i want somewhere around the highlighted lines in the github link .

I also had to override the InlineModelAdmin.get_extra() in order “have room” for the initial data provided. Left default it will display only 3 of the initial data

I believe there should be a more cleaner answer in the upcoming versions

Answered By: Ramez Ashraf

You can override empty_form getter on a formset. Here is an example on how do I deal with this in conjunction with django admin:

class MyFormSet(forms.models.BaseInlineFormSet):
    model = MyModel

    @property
    def empty_form(self):
        initial = {}
        if self.parent_obj:
            initial['name'] = self.parent_obj.default_child_name
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True, initial=initial
        )
        self.add_fields(form, None)
        return form    

class MyModelInline(admin.StackedInline):
    model = MyModel
    formset = MyFormSet

    def get_formset(self, request, obj=None, **kwargs):    
        formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
        formset.parent_obj = obj
        return formset
Answered By: Simanas

I’m having the same problem. I’m using Django 1.9, and I’ve tried the solution proposed by Simanas, overriding the property “empty_form”, adding some default values in de dict initial. That worked but in my case I had 4 extra inline forms, 5 in total, and only one of the five forms was populated with the initial data.

I’ve modified the code like this (see initial dict):

class MyFormSet(forms.models.BaseInlineFormSet):
    model = MyModel

    @property
    def empty_form(self):
        initial = {'model_attr_name':'population_value'}
        if self.parent_obj:
            initial['name'] = self.parent_obj.default_child_name
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True, initial=initial
        )
        self.add_fields(form, None)
        return form    

class MyModelInline(admin.StackedInline):
    model = MyModel
    formset = MyFormSet

    def get_formset(self, request, obj=None, **kwargs):    
        formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
        formset.parent_obj = obj
        return formset

If we find a way to make it work when having extra forms, this solution would be a good workaround.

Answered By: Martinez Mariano
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.