Symmetrical, Self-referencing ManyToManyField with Through Table

961 zobrazení
Přeskočit na první nepřečtenou zprávu

Jim Shepherd

nepřečteno,
24. 6. 2020 19:00:0624.06.20
komu: Django users
I am unable to get a symmetrical, self-referencing ManyToManyField with a through table to actually be symmetrical, at least through the Admin interface. I am using Django 3.0 so it looks like the capability is supported.

Models:

class Contact(models.Model):
last_name = models.TextField(default='', blank=True)
connections = models.ManyToManyField('self',
through='ContactConnection',
symmetrical=True,
blank=True)

def get_connections(self):
return ', '.join([str(p.last_name) for p in self.connections.all()])


class ContactConnection(models.Model):
to_contact = models.ForeignKey(Contact,
related_name='contacts',
on_delete=models.CASCADE,
null=False)
from_contact = models.ForeignKey(Contact,
on_delete=models.CASCADE,
null=False)
comments = models.TextField(default='', blank=True)



Admin:
class ContactConnectionInline(admin.TabularInline):
model = Contact.connections.through
fk_name = 'from_contact'
extra = 0

@admin.register(Contact)
class ContactAdmin(admin.ModelAdmin):
ordering = ('last_name',)
list_display = ['last_name', 'get_connections']
inlines = (ContactConnectionInline)

 
From the Admin page, when viewing a Contact, only one-direction of the ContactConnection relationship is shown which makes sense due to the fk_name='from_contact attribute fixing the direction.
But it seems that at least the get_connections() method would get both sides.

Is there a way either through a query or preferably an Inline to show the Contacts on both sides of the ContactConnection?

Thanks,
Jim

Mike Dewhirst

nepřečteno,
24. 6. 2020 21:20:5124.06.20
komu: django...@googlegroups.com
On 25/06/2020 8:29 am, Jim Shepherd wrote:
> I am unable to get a symmetrical, self-referencing ManyToManyField
> with a through table to actually be symmetrical, at least through the
> Admin interface. I am using Django 3.0 so it looks like the capability
> is supported.
>
> Models:
> class Contact(models.Model):
> last_name = models.TextField(default='', blank=True)
> connections = models.ManyToManyField('self', through='ContactConnection', symmetrical=True, blank=True)
>
> def get_connections(self):
> return ', '.join([str(p.last_name)for pin self.connections.all()])
>
>
> class ContactConnection(models.Model):
> to_contact = models.ForeignKey(Contact, related_name='contacts', on_delete=models.CASCADE, null=False)
> from_contact = models.ForeignKey(Contact, on_delete=models.CASCADE, null=False)
> comments = models.TextField(default='', blank=True)
>

Just a stab in the dark - have you tried giving from_contact a related_name?


>
> Admin:
> class ContactConnectionInline(admin.TabularInline):
> model = Contact.connections.through
> fk_name ='from_contact' extra =0
> @admin.register(Contact)
> class ContactAdmin(admin.ModelAdmin):
> ordering = ('last_name',)
> list_display = ['last_name', 'get_connections']
> inlines = (ContactConnectionInline)
> From the Admin page, when viewing a Contact, only one-direction of the
> ContactConnection relationship is shown which makes sense due to the
> fk_name='from_contact attribute fixing the direction.
> But it seems that at least the get_connections() method would get both
> sides.
>
> Is there a way either through a query or preferably an Inline to show
> the Contacts on both sides of the ContactConnection?

Another stab ... maybe you could just display the
ContactConnectionAdmin(admin.ModelAdmin) with model=ContactConnection?

I haven't advanced to 3.x so I don't know what it is capable of.


>
> Thanks,
> Jim
> --
> You received this message because you are subscribed to the Google
> Groups "Django users" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to django-users...@googlegroups.com
> <mailto:django-users...@googlegroups.com>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/django-users/91dad4bc-7eba-461d-89f1-aaf173e80831o%40googlegroups.com
> <https://groups.google.com/d/msgid/django-users/91dad4bc-7eba-461d-89f1-aaf173e80831o%40googlegroups.com?utm_medium=email&utm_source=footer>.

Jim Shepherd

nepřečteno,
25. 6. 2020 8:52:4725.06.20
komu: Django users
Mike, Thanks for your suggestions. 

Just a stab in the dark - have you tried giving from_contact a related_name?

Yes, I have tried a few different combinations of providing a single related_name and various naming conventions when providing related_name on both ForeignKey fields without success.
 
Another stab ... maybe you could just display the
ContactConnectionAdmin(admin.ModelAdmin) with model=ContactConnection?


Yes, adding a ModelAdmin for the through table will show all of the relationships, but it still requires searching both ForeignKey fields to capture all of the relationships, regardless of which Contact hey were created on.

Workarounds are to add two Inlines, one for each through table foreign key, or perform a compound query to combine the two results. I just figured that since the capability was added in Django 3.0, that the symmetric queries were included as well. My guess is that it is possible with the correct configuration / naming conventions. I'll dive into the code to see if anything pops up.

Thanks!

Dan Madere

nepřečteno,
25. 6. 2020 10:26:2525.06.20
komu: Django users
I tried out the example code, and can replicate this. What's interesting is that if I try removing the ContactConnection model, and the "through" attribute, this allows Django to create the intermediate table on its own, and then your get_connections() method works as expected! It seems like using a custom through table is causing this somehow. I haven't found a fix but will keep looking.

Jim Shepherd

nepřečteno,
25. 6. 2020 12:40:3225.06.20
komu: Django users
On Thursday, June 25, 2020 at 10:26:25 AM UTC-4, Dan Madere wrote:
I tried out the example code, and can replicate this. What's interesting is that if I try removing the ContactConnection model, and the "through" attribute, this allows Django to create the intermediate table on its own, and then your get_connections() method works as expected! It seems like using a custom through table is causing this somehow. I haven't found a fix but will keep looking.

 Dan, thanks for looking into this issue.

I found the unit tests for this capability in https://github.com/django/django/tree/master/tests/m2m_through which seems to indicate that it does work.

I updated my version of Django to 3.1.b1 and copied the models and tests into my code. Neither the admin nor queries on the ManyToManyField return the full "symmetric" results.

Any other ideas?

Thanks!

Jim Shepherd

nepřečteno,
25. 6. 2020 13:53:4525.06.20
komu: Django users
After reviewing the tests, I think I now understand what is going on.

Basically, for symmetric ManyToManyField, Django creates entries for both directions automatically if the correct interface is used. From the test models:

class PersonSelfRefM2M(models.Model):
name = models.CharField(max_length=5)
sym_friends = models.ManyToManyField('self', through='SymmetricalFriendship', symmetrical=True)


class SymmetricalFriendship(models.Model):
first = models.ForeignKey(PersonSelfRefM2M, models.CASCADE)
second = models.ForeignKey(PersonSelfRefM2M, models.CASCADE, related_name='+')
date_friended = models.DateField()

And the tests:
def test_self_referential_symmetrical(self):
tony = PersonSelfRefM2M.objects.create(name='Tony')
chris = PersonSelfRefM2M.objects.create(name='Chris')
SymmetricalFriendship.objects.create(
first=tony, second=chris, date_friended=date.today(),
)
self.assertSequenceEqual(tony.sym_friends.all(), [chris])
# Manually created symmetrical m2m relation doesn't add mirror entry
# automatically.
self.assertSequenceEqual(chris.sym_friends.all(), [])
SymmetricalFriendship.objects.create(
first=chris, second=tony, date_friended=date.today()
)
self.assertSequenceEqual(chris.sym_friends.all(), [tony])

def test_add_on_symmetrical_m2m_with_intermediate_model(self):
tony = PersonSelfRefM2M.objects.create(name='Tony')
chris = PersonSelfRefM2M.objects.create(name='Chris')
date_friended = date(2017, 1, 3)
tony.sym_friends.add(chris, through_defaults={'date_friended': date_friended})
self.assertSequenceEqual(tony.sym_friends.all(), [chris])
self.assertSequenceEqual(chris.sym_friends.all(), [tony])
friendship = tony.symmetricalfriendship_set.get()
self.assertEqual(friendship.date_friended, date_friended)

So the tests show that the add() method needs to be used to create both sides of the relationship. I assume that the Admin page does not use the add method, but creates the intermediate entries which would require both the be added for each entry.

Is it possible to configure the Admin models to use add() or to create both directions automatically?

Thanks!


Dan Madere

nepřečteno,
25. 6. 2020 17:49:2325.06.20
komu: Django users
I don't know of a way to configure the admin do that, but one solution would be to use signals to notice when one-way records are created, then automatically create the record for reverse relationship. We need to notice when records are deleted as well. This approach would allow showing one inline that shows the combined results. I was able to get it working with this: https://gist.github.com/dgmdan/e7888a73c14446dccd7ad9aaf5055b10
Hope this helps!

Jim Shepherd

nepřečteno,
25. 6. 2020 18:32:2125.06.20
komu: Django users
Nice solution. Thanks!
Odpovědět všem
Odpověď autorovi
Přeposlat
0 nových zpráv