question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Snippet `ManyToManyField` fields not handled properly by `RevisionMixin` and `DraftStateMixin`

See original GitHub issue

Issue Summary

Changes to a snippet’s ManyToManyField selections are ignored by RevisionMixin. If DraftStateMixin is also used, those changes are lost when saving.

Steps to Reproduce

With the following models, do the following in the Wagtail admin:

  1. Create some Category objects
  2. Create a TestSnippet object
  3. Edit the created TestSnippet, and change its Category selections
  4. Open the snippet again, and verify the changes have been saved
  5. View the revision history and compare the edited version to the original one
  6. Expected output: category changes. Actual output: “There are no differences between these two versions”.

Now, also add in DraftStateMixin, migrate, and then:

  1. Edit the snippet’s categories
  2. Press Publish or Save draft
  3. Open the snippet again
  4. Expected outcome: the changes were saved. Actual outcome: the changes have disappeared.
@register_snippet
class Category(models.Model):
    name = models.CharField(max_length=255)

    panels = [FieldPanel('name')]


@register_snippet
class TestSnippet(RevisionMixin, models.Model):
    title = models.CharField(max_length=255)
    categories = models.ManyToManyField('app.Category', blank=True)
    _revisions = GenericRelation('wagtailcore.Revision', related_query_name='test_snippet')

    panels = [
        FieldPanel('title'),
        FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
    ]

    @property
    def revisions(self):
        return self._revisions

Technical details

Django 4.1.3 Python 3.9.9 wagtail 4.1.1.final.1

Issue Analytics

  • State:closed
  • Created 10 months ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
laymonagecommented, Nov 17, 2022

Thank you @mikaraunio! Pretty sure that ParentalKey and ParentalManyToManyField only changes the application-level representation of the fields, (i.e. the database representation is the same as that of ForeignKey and ManyToManyField), so there should be no data loss (as you confirmed).

For slightly more technical details, the RevisionMixin relies on the serializable_data() and from_serializable_data() methods to serialize/deserialize the fields (including relations) of the model instance. By default, it tries to call those methods from the superclass, and fall back to get_serializable_data_for_fields() and model_from_serializable_data() functions from django-modelcluster if none of the superclasses have the method:

https://github.com/wagtail/wagtail/blob/119f288a3cf91775687aafffe19ac4c92bb45e0a/wagtail/models/__init__.py#L278-L291

In django-modelcluster’s ClusterableModel, the methods are defined to handle the relations, and this is “where the magic happens” to make relations stored in the revisions correctly:

https://github.com/wagtail/django-modelcluster/blob/8666f16eaf23ca98afc160b0a4729864411c0563/modelcluster/models.py#L209-L273

So, yes, I believe this is the officially supported way. PR for documentation up at #9691.

1reaction
mikarauniocommented, Nov 16, 2022

Confirm that switching from Model to ClusterableModel seems to make everything work here.

Also, have now switched over all my existing snippets to ParentalManyToManyField and migrated, and not seeing any data loss or oddities.

Would be good to have official confirmation that this is the way to go, and agree the docs should then be updated.

For others who might be facing this, here’s the “works for me” snippet:

@register_snippet
class Category(models.Model):
    name = models.CharField(max_length=255)

    panels = [FieldPanel('name')]


@register_snippet
class TestSnippet(DraftStateMixin, RevisionMixin, ClusterableModel):
    title = models.CharField(max_length=255)
    categories = ParentalManyToManyField('app.Category', blank=True)
    _revisions = GenericRelation('wagtailcore.Revision', related_query_name='test_snippet')

    panels = [
        FieldPanel('title'),
        FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
    ]

    @property
    def revisions(self):
        return self._revisions
Read more comments on GitHub >

github_iconTop Results From Across the Web

Mika Raunio mikaraunio - GitHub
Snippet ManyToManyField fields not handled properly by RevisionMixin and DraftStateMixin. Issue Summary Changes to a snippet's ManyToManyField selections ...
Read more >
Snippets — Wagtail Documentation 4.2a0 documentation
Snippets do not use multiple tabs of fields, nor do they provide the “save as draft” or ... template tags like pageurl need...
Read more >
django-reversion revert ManyToMany fields outside admin
If I understand it correctly, I think you should get the revision for the version; the version contains the data of the object,...
Read more >
Release 4.1.1 Torchbox - Wagtail Documentation
When blank=True, it means that this field is not required and can be empty. ... from wagtail.models import DraftStateMixin, RevisionMixin.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found