djangowagtailmanytomanyfieldwagtail-snippet

What is the simplest way to handle M2M through fields in wagtail FieldPanel?


I recently added a "through" model to allow sorting connected objects. In the example below, a Stage has an ordered list of Blocks linked through StageBlock (with the StageBlock.order field)

@register_snippet
class Block(index.Indexed, models.Model):
    title = models.CharField(max_length=100, verbose_name=_("Block Name"))

@register_snippet
class Stage(index.Indexed, models.Model):
    title = models.CharField(max_length=100, verbose_name=_("Stage Name"))
    blocks = models.ManyToManyField(
        to="app.Block",
        blank=True,
        help_text=_("Blocks associated to this stage"),
        related_name="stages",
        verbose_name=_("Blocks"),
        through="StageBlock",
    )

    panels = [
        FieldPanel("title", classname="title full"),
        FieldPanel(
           "blocks",
           widget=autocomplete.ModelSelect2Multiple(
               url="block-autocomplete",
               attrs={"data-maximum-selection-length": 3},
           ),
        ),



class StageBlock(models.Model):
    block = models.ForeignKey("app.Block", on_delete=models.CASCADE)
    stage = models.ForeignKey("app.Stage", on_delete=models.CASCADE)
    order = models.PositiveSmallIntegerField()

The problem is that the related Wagtail admin form breaks, since it tries to associate Block objects to Stage, without providing the "through" model "order" field value.

I'm wondering what is the cleanest/least effort solution to allow an ordered selection of elements in the admin panel, then to properly save the Stage instance with its blocks and related stageblocks.

For the moment, I will add a custom form to the snippet, and auto-assign the order from the position of blocks in the form data (hoping that it always matches the order of blocks as selected in the fieldpanel).

It feels like this use-case could be auto-handled, either by the wagtail-autocomplete plugin, or by wagtail fieldpanel. But as far as I understand, fieldpanel will simply re-use the Django ModelMultipleChoiceField field, which returns a html element.


Solution

  • A many-to-many relation with a 'through' model is structurally the same as a one-to-many child relationship on that 'through' model, so one possibility is to implement this with an InlinePanel (as described here):

    from django_modelcluster.fields import ParentalKey
    from django_modelcluster.models import ClusterableModel
    
    from wagtail.core.models import Orderable
    
    
    @register_snippet
    class Stage(index.Indexed, ClusterableModel):
        title = models.CharField(max_length=100, verbose_name=_("Stage Name"))
    
        panels = [
            FieldPanel("title", classname="title full"),
            InlinePanel("blocks", label="Blocks"),
        ]
    
    
    class StageBlock(Orderable):
        stage = ParentalKey("app.Stage", on_delete=models.CASCADE, related_name='blocks')
        block = models.ForeignKey("app.Block", on_delete=models.CASCADE)
    
        panels = [
            FieldPanel('block'),
        ]