I’m working on a Django application at work that has several Many-to-Many relationships that have extra fields attached to them. Setting this up using the “through” parameter on the field definition is described in the Django documentation.
My trouble came in when it came time to render this information out to the user in a form so they could update the extra fields. From the user’s perspective the relationship can’t be changed, but the extra fields could be.
Extending Django’s example usage about band membership, if the user were viewing the information for “The Beatles” and including a formset for all band members the form would normally render something like this:
Group Name: The Beatles
Members:
Group: The Beatles
Person: Ringo Starr
Date Joined: 14 August 1962
-
Group: The Beatles
Person: John Lennon
Date Joined: 01 August 1960
-
[etc.]
But this is a little obtuse if the purpose of the form is to allow the user to edit the “Date Joined” value for each band member. We can whittle the form down by telling the formset to only render the “Date Joined” field [by passing fields=(‘date_joined’,) to modelformset_factory], but then we get this:
Group Name: The Beatles
Members:
Date Joined: 14 August 1962
-
Date Joined: 01 August 1960
-
[etc.]
Now we can’t tell to which person each date applies.
Adjusting the form so that the label “Date Joined” was instead the appropriate band members name seems like such a simple thing to change. But I didn’t want to hand-write the form as it was already being automatically built and rendered for me using using modelformset_factory and Crispy-Forms.
It took me most of a day to figure it out and in the end it was pretty simple. I only need to implement a custom Form that overrides the constructor and sets the label using the object instance before it gets thrown away:
class MembershipForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MembershipForm, self).__init__(*args, **kwargs)
if kwargs['instance'] and kwargs['instance'].person:
self.fields['date_joined'].label = kwargs['instance'].person.name
else:
self.fields['date_joined'].label = 'No Person'
class Meta:
model = Membership
fields = ['date_joined']
And now our output looks something like:
Group Name: The Beatles
Members:
Ringo Starr: 14 August 1962
-
John Lennon: 01 August 1960
-
[etc.]
Since it took me most of a day to figure this out, and I couldn’t find anything specifically addressing this on the Internet, I wrote this up so hopefully the next person (or me in the future once I’ve forgotten this) will have an easier time figuring it out.