Wednesday, May 21, 2014

Determining if a Manager is being used by a related object.

Recently, I wanted to figure out if a custom manager was being called by a related class or the original class. As an example, consider this abbreviated code:

class MembershipManager(models.Manager):
    def get_by_type(self, type, user=None):
        # figure out if we need user or not...
        # then call self.get_queryset().get()

class Membership(models.Model):
    user = models.ForeignKey('auth.User', related_name='memberships')
    m_type = models.CharField(max_length=200)

    objects = MembershipManager()

So... how does get_by_type know if it's being called as "user.memberships.get_by_type(foo)" or "Membership.objects.get_by_type(foo, user=bar)?" Well, I knew that related managers pre-filter on the object they're related to, so I just had to find the code that does that.

It's right here in the ForeignRelatedObjectsDescriptor class. The interesting bit is:

    def related_manager_cls(self):
        # Dynamically create a class that subclasses the related model's default
        # manager.
        superclass = self.related.model._default_manager.__class__
        rel_field = self.related.field
        rel_model = self.related.model

        class RelatedManager(superclass):
            def __init__(self, instance):
                super(RelatedManager, self).__init__()
                self.instance = instance
                self.core_filters= {'%s__exact' % instance}
                self.model = rel_model

This is a class factory that, in this case, subclasses MembershipManager when creating the attribute user.memberships (actually, that's not quite true. user.memberships is actually the ForeignRelatedObjectsDescriptor which hands out this dynamically created, and unnamed, subclass of MembershipManager. The descriptor is used as a proxy so that User.memberships doesn't return anything that actually is a related manager because it would be misleading at best.)

So, long story short, if self.instance exists when `get_by_type` is called, we're in a related object subclass and you can check if self.instance is a User object, in which case you can then allow the caller to skip the user keyword argument.

This is probably documented somewhere on the Django site, but I couldn't find it easily.