django-modelcluster
django-modelcluster copied to clipboard
AttributeError when ParentalManyToManyField is specified with a through model
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
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)...?
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.
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.
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!