Opened 19 years ago

Last modified 8 weeks ago

#897 new New feature

Bi-Directional ManyToMany in Admin

Reported by: anonymous Owned by: nobody
Component: contrib.admin Version:
Severity: Normal Keywords:
Cc: kmike84@…, carsten.fuchs@…, cmawebsite@…, mmitar@…, Hugo Osvaldo Barrera, Emmanuel Katchy 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 Khaled Emad)

Allow manytomany 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 screen I get a
multiselect window for item types.

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

Thanks

Change History (48)

comment:1 by Adrian Holovaty, 19 years ago

Description: modified (diff)
priority: normallow

comment:2 by Gary Wilson <gary.wilson@…>, 18 years ago

Summary: Bi-Directional ManyToManyBi-Directional ManyToMany in Admin

comment:3 by Gary Wilson <gary.wilson@…>, 18 years ago

#2648 marked as duplicate of this.

comment:4 by Chris Beaven, 18 years ago

Triage Stage: UnreviewedAccepted

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

comment:5 by gsf <gsf@…>, 18 years ago

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.

in reply to:  5 comment:6 by gsf <gsf@…>, 18 years ago

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

comment:7 by mrts, 17 years ago

milestone: post-1.0

Non-essential for 1.0.

comment:8 by gsf@…, 17 years ago

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 by gsf@…, 17 years ago

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 by NicoeEchaniz, 16 years ago

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 by Russell Keith-Magee, 16 years ago

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 by anonymous, 16 years ago

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

comment:13 by (none), 16 years ago

milestone: post-1.0

Milestone post-1.0 deleted

comment:14 by mrts, 16 years ago

Keywords: gsoc09-admin-refactor added

comment:15 by Jacob, 16 years ago

Keywords: gsoc09-admin-refactor removed

comment:16 by buchuki, 14 years ago

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 by Eric Holscher, 14 years ago

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 by Eric Holscher, 14 years ago

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 by Alex Gaynor, 14 years ago

Err, that was Carl Meyer who suggested it.

comment:20 by Russell Keith-Magee, 14 years ago

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.

in reply to:  17 ; comment:21 by anonymous, 14 years ago

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

in reply to:  21 comment:22 by anonymous, 14 years ago

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 by Etienne Desautels, 14 years ago

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 by Mikhail Korobov, 14 years ago

Cc: kmike84@… added

comment:25 by Łukasz Rekucki, 14 years ago

Severity: normalNormal
Type: enhancementNew feature

comment:26 by Carsten Fuchs, 14 years ago

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

comment:27 by David Butler, 14 years ago

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 14 years ago by David Butler (previous) (diff)

comment:28 by anonymous, 13 years ago

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 by CB, 12 years ago

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 by Roger Hunwicks, 12 years ago

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 by Ioan Alexandru Cucu, 11 years ago

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 by Koen Biermans <koen@…>, 11 years ago

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 by Collin Anderson, 10 years ago

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 by Josh Schneier, 10 years ago

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 by Collin Anderson, 10 years ago

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 by Collin Anderson, 10 years ago

#24317 should fix this.

in reply to:  36 comment:37 by Asif Saifuddin Auvi, 9 years ago

Replying to collinanderson:

#24317 should fix this.

then the issue should be closed?

comment:38 by Claude Paroz, 9 years ago

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

comment:41 by Chronial, 9 years ago

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 by Mitar, 9 years ago

Cc: mmitar@… added

comment:43 by Mitar, 9 years ago

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.

comment:44 by Ramiro Morales, 6 years ago

comment:45 by Collin Anderson, 6 years ago

It can be done with inlines, but not with easy left/right select widget.

comment:46 by Hugo Osvaldo Barrera, 4 years ago

Cc: Hugo Osvaldo Barrera added

comment:47 by Khaled Emad, 18 months ago

Description: modified (diff)

comment:48 by Emmanuel Katchy, 5 months ago

For clarity and to help whoever might take this up in the future:

This is still an issue in Django 5.2.dev20240621100134.

Given the following models

from django.db import models


class Student(models.Model):
    name = models.CharField(max_length=50)
    age = models.PositiveIntegerField()

    def __str__(self) -> str:
        return self.name


class Course(models.Model):
    name = models.CharField(max_length=20)
    students = models.ManyToManyField(
        to=Student,
        related_name="courses",
        related_query_name="course",
    )

    def __str__(self) -> str:
        return self.name

and the admin.py

from django.contrib import admin
from django.contrib.admin import ModelAdmin

from core.models import Course, Student


@admin.register(Course)
class CourseAdmin(ModelAdmin): ...


@admin.register(Student)
class StudentAdmin(ModelAdmin):
    filter_horizontal = [
        "course",
    ]

It raises an error

ERRORS:
<class 'core.admin.StudentAdmin'>: (admin.E020) The value of 'filter_horizontal[0]' must be a many-to-many field.

This error is raised by django.contrib.admin.checks.BaseModelAdminChecks._check_filter_item.

However, getting the widget to render would require modifying ModelAdmin.get_form as well.

comment:49 by Emmanuel Katchy, 5 months ago

Cc: Emmanuel Katchy added

comment:50 by David, 8 weeks ago

I was not aware of this very old issue and opened a duplicate where I posted a workaround to make this work: https://code.djangoproject.com/ticket/35878#comment:1

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