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?
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:
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).TabularInline
) and override its get_formset
method to return the result of inlineformset_factory()
using your custom formset class.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.
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
.
InlineAdmin
dynamically generates an initial value based on the exposed request object.BaseInlineFormSet
through keyword arguments passed to the constructor.BaseInlineFormSet
constructor pops the initial values off the list of keyword arguments and constructs normally.BaseFormSet._construct_form
and BaseFormSet._construct_forms
methodsHere 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)
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With