Create a form for a ManyToMany-through relationship and dynamically add more occurrences of the ManyToMany form

I’m writing an app that is an inventory of microfilm reels. I have models for Title and Reel. Through TitleReelRecord, there is a ManyToMany relationship between Titles and Reels. I have additional fields in the TitleReelRecord model for beginning and end dates and the title order. See the models below.

class Title(models.Model):
    star_id = models.CharField(max_length=7, null=True, blank=True, verbose_name="STAR ID")
    title = models.CharField(max_length=255)
    oclc = models.IntegerField(null=True, blank=True, verbose_name="OCLC #")
    microform_oclc = models.IntegerField(null=True, blank=True, verbose_name="Microform OCLC #")
    city = models.CharField(max_length=255, null=True, blank=True)
    student_name = models.CharField(max_length=100, null=True, blank=True, verbose_name="Entered by")
    notes = models.TextField(null=True, blank=True)
    status = models.ForeignKey(Status, null=True, default='Created', on_delete=models.SET_NULL)
    archive = models.ForeignKey(Archive, null=True, blank=True, on_delete=models.SET_NULL)
    filmed_with = models.CharField(max_length=255, null=True, blank=True)
    last_updated = models.CharField(max_length=135)
    srlf_call = models.TextField(null=True, blank=True)

    def __str__(self):
        return self.title
    
    def delete(self):
        self.status = 'Deleted'
        self.save()
    
    def get_absolute_url(self):
        """Returns the URL to access a particular title instance."""
        return reverse('title-detail', args=[str(self.id)])

class Reel(models.Model):
    acidity = models.SmallIntegerField()
    broken_film = models.CharField(max_length=3)
    digitized = models.BooleanField()
    location_id = models.ForeignKey(Location, on_delete=models.PROTECT, verbose_name='Location')
    notes = models.TextField(null=True, blank=True)
    status = models.ForeignKey(Status, null=True, default='Created', on_delete=models.SET_NULL)
    last_updated = models.CharField(max_length=135)
    title_id = models.ManyToManyField(Title, through='TitleReelRecord')

    def delete(self):
        self.status = 'Deleted'
        self.save()
    
    def get_absolute_url(self):
        """Returns the URL to access a particular title instance."""
        return(reverse('staff-reel-detail', args=[str(self.id)]))

class TitleReelRecord(models.Model):
    title_id = models.ForeignKey(Title, on_delete=models.CASCADE)
    reel_id = models.ForeignKey(Reel, on_delete=models.CASCADE)
    begin_date = models.DateField()
    end_date = models.DateField()
    title_order = models.CharField(max_length=6)
    last_updated = models.CharField(max_length=135)

I’m using generic Class-Based Views to create, update, and display details for Titles and Reels.

class TitleCreateView(CreateView):
    model = Title
    fields = ['title', 'star_id', 'oclc', 'microform_oclc', 'city', 
              'archive', 'filmed_with', 'status', 'student_name', 'notes',
    ]
    
class TitleDetailView(DetailView):
    model = Title

class TitleUpdateView(UpdateView):
    model = Title
    fields = ['title', 'star_id', 'oclc', 'microform_oclc', 'city', 
              'archive', 'filmed_with', 'status', 'student_name', 'notes',
    ]

class ReelCreateView(CreateView):
    form_class = AddReelForm
    model = Reel

class ReelDetailView(DetailView):
    model = Reel

class ReelUpdateView(UpdateView):
    model = Reel
    fields = ['acidity', 'broken_film', 'digitized', 'location_id', 'notes',
              'status',
    ]

For completeness, here is the form class used for creating Reels:

class AddReelForm(forms.ModelForm):
    acidity = forms.ChoiceField(widget=forms.Select(), 
                                choices=([(0,0), (1,1), (2,2), (3,3), (4,4)])
                                )
    broken_film = forms.ChoiceField(widget=forms.Select(),
                                    choices=([('no','No'), ('yes','Yes')]))
    digitized = forms.ChoiceField(widget=forms.Select(),
                                  choices=([('false','No'), ('true','Yes')]))
    class Meta:
        model = Reel
        fields = [
            'acidity', 'broken_film', 'digitized', 'location_id', 'notes',
            'status',
        ]
        labels = {
            'location_id': _('Location'),
        }

When creating the Reel, along with the form to create it, how do I display a form for the ManyToMany relationship between a (previously created) Title and the Reel being created? Also, how do I display the form elements for the beginning and end date of the reel and title order, which are the additional fields on the “through” model?

In addition, how do I dynamically add more occurrences of the ManyToMany form if a single reel contains more than one Title?

Here is an example of the form I wish to create:

In general, model forms cannot interfere with through models.
If you want to do that, you’ll need to add fields to your form to manipulate the through model, and optionally add actions for the through model.

The following is all that is done with the many-to-many field in the model form.

object.{manytomanyfield}.clear()
object.{manytomanyfield}.add()

Alternatively, you can use the method of entering and transmitting two or more form data in one form tag.

In this case, the field names used for each form will have to be different.

# views.py
context['form_a'] = {Reel Form}
context['form_b'] = {TitleReelRecord Form}
# template
<form>
{{ form_a }}
{{ form_b }}
</form>
# validate
{Reel Form}(data=request.POST).is_valid()
{TitleReelRecord Form}(data=request.POST).is_valid()

I never thought about sending the two forms in the context variable. I’ll give that a try. Thanks!

I’m using a generic Class-Based view to create a reel; I set form_class to AddReelForm. How would I send a second form to the template? Can form_class be a list? Will I have to change this view to a function-based view instead?

I appreciate your help.

override get_context_data method.

def get_contenxt_data(self):
  context: dict = super().get_context_data()
  return context