When you make an inline formset, BaseInlineFormset
adds the foreign key name to the list of fields. However, it only creates a copy of the original form’s fields if the form defines its fields in a tuple; otherwise, it modifies the original reference to fields. If you pass in a form class that has defined Meta.fields
as a list, BaseInlineFormset
changes the fields on that form class. This means that if that form is used for other things besides the inline formset, it now has an extra field that has been erroneously added.
Here is a minimally reproducible example. We have a UserActionForm
that defines Meta.fields
as a list. Notice that after initializing an instance of the FormsetClass
, the fields on UserActionForm
have been modified to include user
(the foreign key of the inline formset). Expected behavior is that UserActionForm.Meta.fields
is not modified by instantiation of a formset, and instead that the formset modifies a copy of the fields.
## models.py ###
class User(AbstractUser):
email = models.EmailField(unique=True)
class UserAction(models.Model):
user = models.ForeignKey(User, on_delete=models.PROTECT)
url = models.URLField(max_length=2083)
## forms.py ###
from django import forms
class UserActionForm(forms.ModelForm):
class Meta:
model = UserAction
fields = ["url"]
### Shell ###
from common.models import User, UserAction
from django import forms
FormsetClass = forms.inlineformset_factory(User, UserAction, UserActionForm)
print(UserActionForm.Meta.fields)
# ['url']
FormsetClass()
print(UserActionForm.Meta.fields)
# ['url', 'user'] --> (should just be ['url'])
Here’s the line in BaseInlineFormset
's __init__
that modifies the form’s fields - in the event that the form’s fields are not a tuple
, the init appends directly to fields
without making a copy.
# django/forms/models.py:L1115
class BaseInlineFormSet(BaseModelFormSet):
def __init__(...):
...
if isinstance(self.form._meta.fields, tuple):
self.form._meta.fields = list(self.form._meta.fields)
self.form._meta.fields.append(self.fk.name)
The fix for this should be fairly straightforward: rather than only copying _meta.fields
if it’s a tuple, BaseInlineFormset
should always make a copy of _meta.fields
regardless of the type of the iterable. BaseInlineFormset
already works with a copy in the case that the original form’s fields are a tuple, so this change will maintain the current behavior while preventing modifications to the original form’s fields.
Proposed Patch:
Thank you for the report!
Replicated
tests/model_formsets/tests.py
("title",)