35
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:
- 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.
- Write your own formset class which subclasses from
BaseInlineFormSet
and acceptsinitial
. The really tricky bit here is that you must override__init__()
, and you must make sure that it calls up toBaseFormSet.__init__()
rather than using the direct parent or grandparent__init__()
(since those areBaseInlineFormSet
andBaseModelFormSet
, respectively, and neither of them can handle initial data). - Write your own subclass of the appropriate admin inline class (in my case it was
TabularInline
) and override itsget_formset
method to return the result ofinlineformset_factory()
using your custom formset class. - On the actual
ModelAdmin
subclass for the model with the inline, overrideadd_view
andchange_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 yourModelAdmin
‘sget_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.
20
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
.
- The
InlineAdmin
dynamically generates an initial value based on the exposed request object. - It uses currying to expose the initial values to a custom
BaseInlineFormSet
through keyword arguments passed to the constructor. - The
BaseInlineFormSet
constructor pops the initial values off the list of keyword arguments and constructs normally. - The last piece is overriding the form construction process by changing the maximum total number of forms and using the
BaseFormSet._construct_form
andBaseFormSet._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)
- [Django]-Django returns 403 error when sending a POST request
- [Django]-Getting model ContentType in migration – Django 1.7
- [Django]-Django – Reverse for '' not found. '' is not a valid view function or pattern name
19
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
- [Django]-Django default_from_email name
- [Django]-Django: How to format a DateField's date representation?
- [Django]-Reference list item by index within Django template?
3
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.
- [Django]-Raw SQL queries in Django views
- [Django]-Select between two dates with Django
- [Django]-Django: Generic detail view must be called with either an object pk or a slug
3
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))
- [Django]-Django template display item value or empty string
- [Django]-Gunicorn + nginx: Server via socket or proxy?
- [Django]-Django upgrading to 1.9 error "AppRegistryNotReady: Apps aren't loaded yet."
3
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
- [Django]-Multiple django sites with apache & mod_wsgi
- [Django]-Serving dynamically generated ZIP archives in Django
- [Django]-Django middleware difference between process_request and process_view
2
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
- [Django]-How to receive json data using HTTP POST request in Django 1.6?
- [Django]-How to add Indian Standard Time (IST) in Django?
- [Django]-Multiple ModelAdmins/views for same model in Django admin
0
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))
- [Django]-How to store a dictionary on a Django Model?
- [Django]-How to loop over form field choices and display associated model instance fields
- [Django]-Django Query That Get Most Recent Objects From Different Categories
0
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
- [Django]-Making a Regex Django URL Token Optional
- [Django]-Django admin make a field read-only when modifying obj but required when adding new obj
- [Django]-Using Django auth UserAdmin for a custom user model
0
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.
- [Django]-Django change default runserver port
- [Django]-Manage.py runserver
- [Django]-Django fix Admin plural