'How do I reuse a ModelForm for a through model with an additional field?

I have a Model Example and corresponding ModelForm ExampleModelForm that I use in ModelFormset

ExampleModelFormset = modelformset_factory(Example, form=ExampleModelForm)

I have a many-to-many through Model with an additional property.

class ExampleThrough(models.Model):
    batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
    example = models.ForeignKey(Example, on_delete=models.CASCADE)
    property = models.FloatField()

Is there a way to reuse the ExampleModelForm to create a ExampleThroughModelForm that I can also use in a ExampleThroughModelFormset? I want to have all of the same fields as the ExampleModelForm plus the one new property.

I don't think I can inherit from the ExampleModelForm:

class ExampleThroughModelForm(ExampleModelForm):
    class Meta:
        model = ExampleThrough
        fields = '__all__'

because the documentation has this note:

Normal Python name resolution rules apply. If you have multiple base classes that declare a Meta inner class, only the first one will be used. This means the child’s Meta, if it exists, otherwise the Meta of the first parent, etc.

I can't apply the ModelForm and Meta inheritance used in Django ModelForm inheritance and Meta inheritance because my ExampleThrough model does not inherit from Example.



Solution 1:[1]

What I usually do in situations like this is to create the ExampleTroughModelForm and make the ExampleModelForm its property. Then in HTML I render both of them without form tags and save them together. Remember to override save(), clean(), and is_valid() methods so the ExampleModelForm gets saved and validated properly.

Suggestion code for the ExampleThroughModelForm (not tested):

class ExampleThroughModelForm(ModelForm):

  class Meta:
    model = ExampleThrough
    fields = '__all__'

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

    # Add custom prefix to avoid name clashes in case of same
    # attribute name in ExampleModelForm and ExampleThroughModelForm
    self.example_form = ExampleModelForm(data=kwargs.get('data'), instance=getattr(self.instance, 'example', None), prefix='example')

    self.helper = FormHelper()
    self.helper.form_tags = False

  def save(self, **kwargs):
     # The ExampleModelForm needs to be saved first in case you are just
     # creating it. We can't have the through Model without this
     saved_example_instance = self.example_form.save()

     # Set the Example model on the ExampleThrough instance before saving
     self.instance.example = saved_example_instance

     # Save the ExampleThrough model
     saved_example_through_instance = super().save(**kwargs)

     return saved_example_through_instance

 def clean(self):
    # Here we also hook the calls to the clean method of the example form
    # This is not necessary but if you have a custom clean method on the
    # ExampleModelForm, you will want to call it when this form is cleaned
    self.example_form.clean()
    super().clean()

    return self.cleaned_data

 def is_valid(self)
    # This is necessary because is_valid method sets the cleaned_data attribute
    return self.example_form.is_valid() and super().is_valid()

Remember that in the ExampleForm you also need to remove the form tags.

In HTML with crispy forms used for rendering, it would look something like this:

<form id="example-through-form" method="post" actions="{% url 'save-example-through' %}">
  {% crispy form %}

  {% crispy form.example_form %}

  <!-- Submit input buttons omitted for brevity -->

</form>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1