Opened 11 years ago

Last modified 7 months ago

#897 new New feature

Bi-Directional ManyToMany in Admin

Reported by: anonymous Owned by:
Component: contrib.admin Version: master
Severity: Normal Keywords:
Cc: kmike84@…, carsten.fuchs@…, cmawebsite@…, mmitar@… Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Adrian Holovaty)

Allow amnytomany relationships to be defined both ways
E.G

class ItemType(meta.Model):
    name = meta.CharField(maxlength=100)
    descritpion = meta.CharField(maxlength=250)

class PropertyType(meta.Model):
    name = meta.CharField(maxlength=100)
    itemtypes = meta.ManyToManyField(ItemType)

Excellent. When I make a new property type in the admin screens I get a
mutiselect window for item types.

What I want to be able to do however is have this work back the other
way too so that whe I create a new item i can specify what property
types apply.

Thanks

Change History (43)

comment:1 Changed 10 years ago by Adrian Holovaty

Description: modified (diff)
priority: normallow

comment:2 Changed 10 years ago by Gary Wilson <gary.wilson@…>

Summary: Bi-Directional ManyToManyBi-Directional ManyToMany in Admin

comment:3 Changed 10 years ago by Gary Wilson <gary.wilson@…>

#2648 marked as duplicate of this.

comment:4 Changed 10 years ago by Chris Beaven

Triage Stage: UnreviewedAccepted

It's a valid enhancement, and supported by a couple of requests.

comment:5 Changed 9 years ago by gsf <gsf@…>

In the meantime, I was able to get what I think is the sought-after functionality by (continuing the above example):

class ItemType(meta.Model):
    name = meta.CharField(maxlength=100)
    description = meta.CharField(maxlength=250)
    properties = meta.ManyToManyField('PropertyType',
            db_table='app_propertytype_itemtypes')

class PropertyType(meta.Model):
    name = meta.CharField(maxlength=100)
    itemtypes = meta.ManyToManyField(ItemType)

Substitute "app" in "db_table" for the name of the app, of course.

comment:6 in reply to:  5 Changed 9 years ago by gsf <gsf@…>

This solution does break syncdb, however, because the same table wants to be created twice.

comment:7 Changed 8 years ago by mrts

milestone: post-1.0

Non-essential for 1.0.

comment:8 Changed 8 years ago by gsf@…

The need for this has come up for me again, so I've been thinking about it. I think the best solution would be to allow for something similar to edit_inline on a ManyToManyField. I'll probably wait to work on it until after new-forms-admin gets rolled into trunk.

comment:9 Changed 8 years ago by gsf@…

Also, regarding the issue of recursion, I think the solution would be to either

  1. modify the "pop-up" add screen so that it doesn't contain the recursive m2m reference or
  2. remove the add button on both sides.

comment:10 Changed 8 years ago by NicoeEchaniz

I believe that what gsf tried to do is what most of us tried to do when we needed this functionality, only to find out that it breaks syncdb.

Would it be a bad idea to modify syncdb to accomodate for this case?

comment:11 Changed 8 years ago by Russell Keith-Magee

Syncdb isn't the right place to do this - it isn't a model issue. This should be handled entirely at the forms level.

comment:12 Changed 8 years ago by anonymous

Some kind fellow posted a workaround on django snippets. http://www.djangosnippets.org/snippets/1295/

comment:13 Changed 8 years ago by (none)

milestone: post-1.0

Milestone post-1.0 deleted

comment:14 Changed 7 years ago by mrts

Keywords: gsoc09-admin-refactor added

comment:15 Changed 7 years ago by Jacob

Keywords: gsoc09-admin-refactor removed

comment:16 Changed 6 years ago by buchuki

Doesn't look like there's been much activity on this bug for a while, but I'd like to point out that the workaround posted by anonymous (Django snippet #1295) no longer works with Django 1.2. I have been unable to find any other workarounds.

comment:17 Changed 6 years ago by Eric Holscher

Just ran into this bug as well. I'll probably take a stab at fixing it in the next couple of days, as it is rather annoying. Though I'm curious if it might break expectations of people in existing setups.

comment:18 Changed 6 years ago by Eric Holscher

I talked to Alex Gaynor, and he mentioned ManytoMany inlines, which will solve this problem in a lot of cases.

http://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-models

comment:19 Changed 6 years ago by Alex Gaynor

Err, that was Carl Meyer who suggested it.

comment:20 Changed 6 years ago by Russell Keith-Magee

There's at least one simple approach for backwards compatibility: reverse m2ms are only included if they are explicitly listed in the fields list. Alternatively, we add an 'include_reverse_m2m' flag.

For 2.0, we can consider dropping the flag and making reverse m2m the default. It's a little inconvenient, but that's the price of backwards compatibility.

comment:21 in reply to:  17 ; Changed 6 years ago by anonymous

Replying to ericholscher:

Just ran into this bug as well. I'll probably take a stab at fixing it in the next couple of days, as it is rather annoying. Though I'm curious if it might break expectations of people in existing setups.

To fix it for 1.2, all needed is to change:

self.creates_table = False

to:

self.auto_created = False

comment:22 in reply to:  21 Changed 6 years ago by anonymous

Replying to anonymous:

Replying to ericholscher:

Just ran into this bug as well. I'll probably take a stab at fixing it in the next couple of days, as it is rather annoying. Though I'm curious if it might break expectations of people in existing setups.

To fix it for 1.2, all needed is to change:

self.creates_table = False

Forget this solution, it doesn't work. Sorry for the noise.

to:

self.auto_created = False

comment:23 Changed 6 years ago by Etienne Desautels

I found a solution that works with 1.2. Here it is:

class User(models.Model):
    groups = models.ManyToManyField('Group', through='UserGroups')

class Group(models.Model):
    users = models.ManyToManyField('User', through='UserGroups')

class UserGroups(models.Model):
    user = models.ForeignKey(User)
    group = models.ForeignKey(Group)

    class Meta:
        db_table = 'app_user_groups'
        auto_created = User

This way syncdb create the UserGroups table only one time (because users and groups have through as argument) but the admin think is auto_created so it show the ManyToManyField directly for both User and Group.

But that's definitely a lot of trouble to have the field in both model admin pages. I don't think the solution is to modified syncdb either. I think russellm's suggestions are good avenues.

comment:24 Changed 6 years ago by Mikhail Korobov

Cc: kmike84@… added

comment:25 Changed 6 years ago by Łukasz Rekucki

Severity: normalNormal
Type: enhancementNew feature

comment:26 Changed 5 years ago by Carsten Fuchs

Cc: carsten.fuchs@… added
Easy pickings: unset

comment:27 Changed 5 years ago by David Butler

UI/UX: unset

etienned's solution did not work for me, I am doing this instead:

class User(models.Model):
    groups = models.ManyToManyField('Group', db_table='testapp_user_groups')

class Group(models.Model):
    users = models.ManyToManyField('User', db_table=User.groups.field.db_table)
Group.users.through._meta.managed = False

This will also work for non symmetrical self M2M relationships:

class User(models.Model):
  supervisors = models.ManyToManyField('self', related_name='underlings_set', db_table='testapp_user_supervisors')
  underlings = models.ManyToManyField('self', related_name='supervisors_set', db_table=supervisors.db_table)
  underlings._get_m2m_attr = supervisors._get_m2m_reverse_attr
  underlings._get_m2m_reverse_attr = supervisors._get_m2m_attr
User.underlings.through._meta.managed = False

Except for the fact that, depending on the order of the fields that get evaluated by the admin, the widget for one may overwrite the value of the other

Last edited 5 years ago by David Butler (previous) (diff)

comment:28 Changed 5 years ago by anonymous

Check this out

class Test1(models.Model):
    tests2 = models.ManyToManyField('Test2', blank=True)

class Test2(models.Model):
    tests1 = models.ManyToManyField(Test1, through=Test1.tests2.through, blank=True)

I guess this is the most native way

comment:29 Changed 4 years ago by CB

To update comment 28:

This works great, but breaks south. So here's a fix for that:

class ReverseManyToManyField(models.ManyToManyField):
    pass
try:
    import south
except ImportError:
    pass
else:
    from south.modelsinspector import add_ignored_fields
    add_ignored_fields([".*\.ReverseManyToManyField$",])

class Test1(models.Model):
    tests2 = models.ManyToManyField('Test2', blank=True)

class Test2(models.Model):
    tests1 = models.ReverseManyToManyField(Test1, through=Test1.tests2.through, blank=True)

A possible Django enchancement would be adding this field (or rather, the results of it's contribute_to_class, I think) instead of the ReverseManyRelatedObjectsDescriptor

comment:30 Changed 3 years ago by rhunwicks

Using Django 1.5.1 I was getting:

app.test1: Reverse query name for m2m field 'tests2' clashes with m2m field 'Test2.tests1'. Add a related_name argument to the definition for 'tests2'

This is almost certainly because my field and model name is the same. Either way, I have solved it by adding related_name to Test1.tests2 and taking the opportunity to suppress it from Test2:

class Test1(models.Model):
    tests2 = models.ManyToManyField('Test2', related_name='test2_set+', blank=True)

class Test2(models.Model):
    tests1 = models.ReverseManyToManyField(Test1, through=Test1.tests2.through, blank=True)

comment:31 Changed 3 years ago by Ioan Alexandru Cucu

For people who still bump into this, it might be worth checking https://github.com/kux/django-admin-extend

It provides a mechanism for injecting bidirectional many-to-many fields in ModelAdmins that have already been defined by other apps.

comment:32 Changed 3 years ago by Koen Biermans <koen@…>

This seems to be more or less the same as #10964. I have a more generic solution for it on that ticket that allows you to use the reverse descriptor for M2M or nullable FK (like 'book_set') in the fields set for any modelform. It is also available in admin (also for filter_horizontal and filter_vertical).

Should one of these be considered as a duplicate?

comment:33 Changed 2 years ago by Collin Anderson

Cc: cmawebsite@… added

I started trying to get the patches from #10964 applying cleanly (and correctly) to master, but I think it's worth waiting for the new _meta options API. https://github.com/django/django/pull/2894

comment:34 Changed 21 months ago by Josh Schneier

The new _meta options api has landed. I'm very interested in seeing this make it across the finish line and would be happy to do the bulk of the work with a little bit of direction. (Not sure where the best place to post that kind of query).

comment:35 Changed 21 months ago by Collin Anderson

Awesome. As some have mentioned, I think the next basic step make auto created reverse related fields (especially ManyToManyRel) into actual fields.

After that, we'd need to allow ManyToManyRel to be used in any model form.

Here's what I think would be a start. (Doesn't work at all)
https://github.com/django/django/pull/3927/files

comment:36 Changed 20 months ago by Collin Anderson

#24317 should fix this.

comment:37 in reply to:  36 Changed 12 months ago by Asif Saifuddin Auvi

Replying to collinanderson:

#24317 should fix this.

then the issue should be closed?

comment:38 Changed 12 months ago by Claude Paroz

Note the "should". I'd rather see the result of #24317 once committed before closing this one.

comment:39 Changed 9 months ago by Asif Saifuddin Auvi

Owner: changed from nobody to Asif Saifuddin Auvi
Status: newassigned
Version: master

comment:40 Changed 9 months ago by Asif Saifuddin Auvi

Owner: Asif Saifuddin Auvi deleted
Status: assignednew

comment:41 Changed 7 months ago by Chronial

This snippet provides a workaround for this: https://snipt.net/chrisdpratt/symmetrical-manytomany-filter-horizontal-in-django-admin/
(and does it at the form level instead of with models)

comment:42 Changed 7 months ago by Mitar

Cc: mmitar@… added

comment:43 Changed 7 months ago by Mitar

The form approach does not work correctly because in the history view of the model in admin there are no entries for changed reverse M2M fields.

Note: See TracTickets for help on using tickets.
Back to Top