django-modelcluster icon indicating copy to clipboard operation
django-modelcluster copied to clipboard

AttributeError when ParentalManyToManyField is specified with a through model

Open m1kola opened this issue 7 years ago • 4 comments

Django throws AttributeError when ParentalManyToManyField is specified with a through model.

Demo with Wagtail:

from django.db import models
from wagtail.wagtailcore.models import Page
from modelcluster.fields import ParentalManyToManyField


class FooModel(models.Model):
    name = models.CharField(max_length=256)

    def __str__(self):
        return self.name


class BarToFoo(models.Model):
    foo = models.ForeignKey('appname.FooModel', blank=True, null=True)
    page = models.ForeignKey('BarPage', related_name='+')


class BarPage(Page):
    asset_programs = ParentalManyToManyField(
        to='appname.FooModel', through='appname.BarToFoo'
    )

Traceback:

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/django/core/handlers/exception.py" in inner
  42.             response = get_response(request)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/django/views/decorators/cache.py" in _cache_controlled
  43.             response = viewfunc(request, *args, **kw)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/wagtail/wagtailadmin/decorators.py" in decorated_view
  31.             return view_func(request, *args, **kwargs)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/wagtail/wagtailadmin/views/pages.py" in edit
  341.                 revision.publish()

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/wagtail/wagtailcore/models.py" in publish
  1539.         page.save()

File "/Users/mikalai/.pyenv/versions/3.4.6/lib/python3.4/contextlib.py" in inner
  30.                 return func(*args, **kwds)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/wagtail/wagtailcore/models.py" in save
  477.         result = super(Page, self).save(*args, **kwargs)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/modelcluster/models.py" in save
  209.             getattr(self, field).commit()

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/modelcluster/fields.py" in commit
  442.                 original_manager.add(*items_to_add)

File "/Users/mikalai/.virtualenvs/venv_name/lib/python3.4/site-packages/django/db/models/fields/related_descriptors.py" in add
  876.                     (opts.app_label, opts.object_name)

Exception Type: AttributeError at /admin/pages/133/edit/
Exception Value: Cannot use add() on a ManyToManyField which specifies an intermediary model. Use appname.BarToFoo's Manager instead.

Django==1.10.7 wagtail==1.11.1

m1kola avatar Aug 21 '17 15:08 m1kola

Is this feature really needed, or can the same thing be done with a ParentalKey on the through model (i.e. the way we 'faked' M2M relationships before ParentalManyToManyField was implemented)...?

gasman avatar Aug 21 '17 17:08 gasman

This feature is really useful for projects with models that were implemented before modelcluster/Wagtail introduced M2M support. So models like

class BarToFoo(models.Model):
    foo = models.ForeignKey('appname.FooModel', blank=True, null=True)
    page = models.ParentalKey('BarPage', related_name='something')

can be modified to become:

class BarToFoo(models.Model):
    foo = models.ForeignKey('appname.FooModel', blank=True, null=True)
    page = models.ForeignKey('BarPage', related_name='+')

Then you just need to add something = ParentalManyToManyField(..., through='appname.BarToFoo') and replace InlinePanel in the "target" model with FieldPanel. No need to write data migrations.

It also allows developers to add additional data into through models.

m1kola avatar Aug 22 '17 09:08 m1kola

A through model is a requirement when you want to record additional information about the relationship.

I need to record a rich text description per many-to-many relationship, the (source, target) tuples must be unique, and I need to put minimum and maximum limits on the relationship (each source needs to have at least 1, at most 8 such relationships recorded).

I guess I can achieve this with an inline panel and a full-blown model with a unique_together constraint, and some way to limit the available choices based on what's already present when editing? The latter would become rather hackish, I fear.

mjpieters avatar May 20 '19 17:05 mjpieters

The syntax for ParentalManyToManyField(to=FooModel, through=BarToFoo) seems more Django-like rather than only defining the intermediary model and setting a ParentalKey as seen in the Wagtail examples.

Also it looks like Django just merged better support for updating m2m relationships that have an intermediary model as well: https://github.com/django/django/pull/8981. So for the example in the ticket description you will be able to use bar_page.asset_programs.add(foo) even with a through model!

mihow avatar Jun 10 '19 23:06 mihow