Form with ManyToMany relation and additional fields

Hello all,

I need your advice how to create Form that contains ManyToMany relation and additional fields. I would like to use CBV and ModelForms if possible. This is what I currently have:

models.py

class Person(TimeStampedModel):
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    first = models.CharField(_('first'), max_length=64)
    last = models.CharField(_('last'), max_length=64)
    relation = models.ManyToManyField('self', through='PersonRelation', through_fields=('person','relation'), blank=True)

class PersonRelation(TimeStampedModel):
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    person = models.ForeignKey(Person, on_delete=models.PROTECT, related_name='relation_set')
    relation = models.ForeignKey(Person, on_delete=models.PROTECT, related_name='+')
    type = models.CharField(_('type'), max_length=4, choices=TypeRelation.choices)

views.py

class PersonCreateView(LoginRequiredMixin, CreateView):
    model = Person
    form_class = PersonModelForm
    template_name = 'form.html'

    def form_valid(self, form):
        person = form.save(commit=False)
        person.save()
        person.relation.set(form.cleaned_data['relation'],
                            through_defaults={'type': 'PARENT'})
        return super().form_valid(form)

forms.py

class PersonModelForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = ['first',
                  'last',
                  'relation']

As you can see my form class PersonModelForm does not have field type because it is a part of model PersonRelation.

I do not know how to create Form that will have all fields from both models Person and PersonRelation. I would like to use Model Forms to avoid repeating fields definitions.

Please note that because I currently don’t know how to have the field type on my form, I put through_defaults={'type': 'PARENT'} in view PersonCreateView
I would like to have an option to choose the value of field type on my form instead of that default entry.

I appreciate your advice how to make the Form in a nice and neat way. FormMixin ??? I am not familiar with that, so will be grateful for any help.

I will also appreciate any comment about my current code. Is it something I do wrong?

You could create a form that handles both the Person and PersonRelation models:

from django import forms
from .models import Person, PersonRelation

class PersonRelationForm(forms.ModelForm):
    class Meta:
        model = PersonRelation
        fields = ['relation', 'type']

class PersonModelForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = ['first', 'last']

class PersonCreateForm(PersonModelForm):
    # Fields for the relation
    relation = forms.ModelChoiceField(
        queryset=Person.objects.all(),
        required=False
    )
    type = forms.ChoiceField(
        choices=TypeRelation.choices,
        required=False
    )

    def save(self, commit=True):
        instance = super().save(commit=commit)
        
        if self.cleaned_data.get('relation') and self.cleaned_data.get('type'):
            PersonRelation.objects.create(
                person=instance,
                relation=self.cleaned_data['relation'],
                type=self.cleaned_data['type']
            )
        
        return instance

Then in your views.py:

class PersonCreateView(LoginRequiredMixin, CreateView):
    model = Person
    form_class = PersonCreateForm
    template_name = 'form.html'
    
    def form_valid(self, form):
        response = super().form_valid(form)
        return response

And in your template form.html:

<form method="post">
    {% csrf_token %}
    <div>
        {{ form.first.label_tag }}
        {{ form.first }}
    </div>
    <div>
        {{ form.last.label_tag }}
        {{ form.last }}
    </div>
    <div>
        {{ form.relation.label_tag }}
        {{ form.relation }}
    </div>
    <div>
        {{ form.type.label_tag }}
        {{ form.type }}
    </div>
    <button type="submit">Save</button>
</form>