3

With a custom through model for an M2M relationship, .add(), .create() and .remove() are disabled.

At the moment, I attempt to use .add() (or whatever) and catch and deal with AttributeError for those custom M2M relationships.

Is there an 'official' way to identify a custom through model, using the Meta API or otherwise? At this stage in my processing, I would rather treat all custom through relationships as generically as possible (rather than lots of if m2m_field.related.through == FooBar statements)

(A solution was found for Django 1.8, but I'm starting a bounty for 2.2)

John Moutafis
  • 22,254
  • 11
  • 68
  • 112
Steven
  • 2,658
  • 14
  • 15

3 Answers3

2

Looks as though

m2m_field.related.through._meta.auto_created is False

does the job.

Steven
  • 2,658
  • 14
  • 15
  • 1
    Hi @Steven did you by any chance have to migrate this code to newer django versions? It looks like this doesn't exist in Django 2.2 and it's making me insane, there doesn't seem to be any way to get this unless I hardcode somewhere. – Sebastián Vansteenkiste Oct 02 '19 at 12:44
  • 1
    @SebastiánVansteenkiste — I'm only on 1.11 so far I'm afraid ;(. It looks like I am now using `m2m_fld.remote_field.through._meta.auto_created ` (returns `False` or a model class) but I haven't got a moment right now to see if that still works in 2.2. YMMV, obviously not officially supported etc etc! – Steven Oct 02 '19 at 15:13
2

From Django 2.2 the .add(), .create(), etc. methods are able to work with a custom through Model as long as you provide the corresponding values for the required fields of the intermediate model using through_defaults:

From the documentation:

You can also use add(), create(), or set() to create relationships, as long as you specify through_defaults for any required fields:

>>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})

You may prefer to create instances of the intermediate model directly.

The .remove() method's behavior needs a bit of attention:

If the custom through table defined by the intermediate model does not enforce uniqueness on the (model1, model2) pair, allowing multiple values, the remove() call will remove all intermediate model instances:

>>> Membership.objects.create(person=ringo, group=beatles,
...     date_joined=date(1968, 9, 4),
...     invite_reason="You've been gone for a month and we miss you.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
>>> # This deletes both of the intermediate model instances for Ringo Starr
>>> beatles.members.remove(ringo)
>>> beatles.members.all()
<QuerySet [<Person: Paul McCartney>]>

About the _meta field I haven't been able to access it from the m2m field, but the above part of the documentation seems to allow avoiding the "gymnastics" of accessing _meta.
If I find anything interesting I will update my answer accordingly.

Community
  • 1
  • 1
John Moutafis
  • 22,254
  • 11
  • 68
  • 112
  • 1
    Fantastic, here's a bounty for you, thanks! Now I've just gotta see how I can turn my code around to see if I can pass those `through_defaults` as `**kwargs` for creating the instances without having to jump through additional hoops. – Sebastián Vansteenkiste Oct 07 '19 at 18:59
  • @SebastiánVansteenkiste Happy to help, good luck with your refactoring! – John Moutafis Oct 07 '19 at 19:01
0

For django 2.2 you can directly check the whether the through model is autocreated or not

This can be checked through a direct check like the following

# check for the auto_created value
m2m_field.through._meta.auto_created == False

To test this I created some sample clases with multiple M2M fields one with a custom through field and one with default class.

from django.db import models
import uuid


class Leaf(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(null=True, max_length=25, blank=True)


# Create your models here.
class MainTree(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    new_leaves = models.ManyToManyField(Leaf, through='LeafTree')
    leaves = models.ManyToManyField(Leaf, related_name='main_branch')


class LeafTree(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    tree = models.ForeignKey(MainTree, on_delete=models.CASCADE)
    leaf = models.ForeignKey(Leaf, on_delete=models.CASCADE)

Testing our method

In [2]: from trees.models import MainTree                                                                                                                                                                

In [3]: m = MainTree()                                                                                                                                                                                   

In [4]: m.leaves.through._meta                                                                                                                                                                           
Out[4]: <Options for MainTree_leaves>

In [5]: m.leaves.through._meta.auto_created                                                                                                                                                              
Out[5]: trees.models.MainTree

In [6]: m.new_leaves.through._meta.auto_created                                                                                                                                                          
Out[6]: False

In [7]: m.new_leaves.through._meta.auto_created == False                                                                                                                                                 
Out[7]: True

In [8]: m.leaves.through._meta.auto_created == False                                                                                                                                                     
Out[8]: False
StarLord
  • 1,012
  • 1
  • 11
  • 22