Search code examples
djangodjango-modelsmany-to-manydjango-orm

Self Referencing Symmetrical Many to Many Django Model in Admin or View


How do you get the other side of a symmetrical self reference in a M2M django model?

Say we are making a parts catalog, and we have lots of Parts and we want to have an Interchange to show which parts can be used in place of another:

class Part(models.Model):
    name = models.CharField(max_length=300)
    number = models.CharField(max_length=200, default 'because numberphiles like more ids')
    …
    interchanges = models.ManyToManyField("self",
                                          through='Interchange',
                                          symmetrical=False,
                                          # + sign per http://charlesleifer.com/blog/self-referencing-many-many-through/
                                          # I have tried without the plus t omaintain the references as well, but can't seem to understand the difference(s)
                                          related_name="interchanges_to+",
                                          # through_fields per docs
                                          through_fields=('reference', 'interchange')
                                          )

    # per this http://charlesleifer.com/blog/self-referencing-many-many-through/
    def add_interchange(self, part, symm=True):
        interchange = models.Interchange.objects.get_or_create(
            reference_part=self,
            interchange_part=part)
        if symm:
            # avoid recursion by passing `symm=False`
            part.add_interchange(self, False)
        return interchange

    def remove_interchange(self, part, symm=True):
        models.Interchange.objects.filter(
            reference_part=self,
            interchange_part=part).delete()
        if symm:
            # avoid recursion by passing `symm=False`
            part.remove_interchange(self, False)

    def get_interchanges(self):
        return ", ".join([str(p) for p in self.interchanges.filter(interchange_part__reference=self)])

    def get_references(self):
        return …?
        # This is where I don't know how to get the Parts that are referring to it as being an interchange

class Interchange(models.Model):
    reference = models.ForeignKey(Part, related_name="reference_part")
    interchange = models.ForeignKey(Part, related_name="interchange_part")
    # just to confirm we have a M2M table, we will add a foregin key to a User if this interchange has been personally verified
    confirmed_by = models.ForeignKey(User, null=True, blank=True)
    # … others if needed

    class Meta:
        # because self-referencing, in this case, is silly, we make sure each part references another part
        unique_together = ('reference', 'interchange')

in the Django admin, I have:

@admin.register(app.Part)
class PartAdmin(admin.ModelAdmin):
    list_display = ['name', 
                    'number', 
                     # ▼ I can see the Parts itself Interchanges to
                    'get_interchanges', 
                     # ▼ I can not see the Parts that reference itself as being an Interchange
                    'get_references'] 

Parts Model in admin for reference...

Here is what I have: Current Models without symmetrical references

Here is what I am trying to get: What I am trying to get with symmetrical references

As a confirmation, the Interchange Model in admin: Interchange Model

How do you get the other side of symmetrical self-reference M2M django models?
How do I return Parts that are in the interchange table but in the reference (first) column

Notes:

  • using django 1.11
  • I know that the symmetry being False is required when using a M2M through Model per the docs
  • This nice write-up helped me get quite far, but in the admin, I can't seem to get the interchange_part to show its reference_parts in the admin page (thusly I haven't even tried to see if it is possible in a view with different/particular function)
  • In the docs it states the second part of the tuple for through_keys is called the target model, but I cant find out what to refer to the first one
  • I am sure that there is a way to go multiple levels deep, ie if part one interchanges to 2, and 2 => 3, then 1 should list 2 and 3 (but that isn't the focus at the moment)
  • Please correct my misunderstandings, Thanks :)

Solution

  • If the manytomany has a through table with a related_name parameter and does NOT include the +, then you can just access the set directly :

    def get_references(self):
        return ", ".join([str(p) for p in self.interchanges_to.all()])
    

    Note, I am unsure about performance/complexity