3

I have a data model where I am using a manual intermediate table for a m2m relationship. Building on the classical example from the django doc:

from django.db import models

INSTRUMENT_CHOICES = (
    ('guitar', 'Guitar'),
    ('bass', 'Bass Guitar'),
    ('drum', 'Drum'),
    ('keyboard', 'Keyboard'),
)

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Leadership')

    def __str__(self):
        return self.name

    def get_leadership():
        return self.leadership_set.first()


class Leadership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    instrument = models.CharField('Playing Instrument', choices=INSTRUMENT_CHOICES, 
                                   max_length=15, 
                                   null=True, 
                                   blank=False)
    class Meta:
        unique_together = ('person', 'group')

When I create a new group I also want to specify who is going to be the leader, and for this relationship also specify which instrument he will play in that group.

What really confuses me, given also the lack of documentation on this topic is how to handle this kind of relationship in forms.

This is the form I came with:

class InstrumentField(forms.ChoiceField):
    def __init__(self, *args, **kwargs):
        super().__init__(INSTRUMENT_CHOICES, *args, **kwargs)


class GroupForm(forms.ModelForm):
    instrument = InstrumentField(required=False)

    class Meta:
        model = Group
        fields = ['name', 
                  'members'
                  'instrument']  # This works but it's not correctly initalized in case of edit form
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.pk is not None:  # editing 
            # PROBLEM: this doesn't work
            self.fields["instrument"].initial = self.instance.get_leadership().instrument

    def save(self, commit=True):
        group = super().save(commit=False)
        if commit:
            group.save()
            if 'instrument' in self.changed_data:
                leader = self.cleaned_data.get('members').first()  
                instrument = self.cleaned_data['instrument']

                Leadership.objects.update_or_create(person=leader, group=group, defaults={'instrument': instrument})

        return group

As suggested in the django doc I am manually instantiating Leadership objects (see the form save method).

What I couldn't solve is how to populate the instrument field in case of form editing. I try to do this in the __init__: first I check that we are in "edit" mode (the instance has a pk) then I get the relevant Leadership object (see Group.get_leadership) and from that I extract the instrument and I assign it to the fields["instrument"].initial.

This doesn't work.

I could inspect that the initial value was set but then when I render the form the default choice value is shown (the first value of the INSTRUMENT_CHOICES).

What am I missing here? Is there a better way or a better docs on how to handle m2m with through model in forms?

Leonardo
  • 4,046
  • 5
  • 44
  • 85

0 Answers0