Monday, November 3, 2008

Lazy User Foreign Keys(this is a double entendre)

A *very* common pattern in Django is for models to have a foreign key to django.contrib.auth.User for the owner(or submitter, or whatever other relation with User) and then to have views that filter this down to the related objects for a specific user(often the currently logged in user). If we think ahead, we can make a manager with a method to filter down to a specific user. But since we are really lazy we are going to make a field that automatically generates the foreign key to User, and gives us a manager, automatically, to filter for a specific User, and we can reuse this for all types of models.

So what does the code look like:

from django.db.models import ForeignKey, Manager

from django.contrib.auth.models import User

class LazyUserForeignKey(ForeignKey):
def __init__(self, **kwargs):
kwargs['to'] = User
self.manager_name = kwargs.pop('manager_name', 'for_user')
super(ForeignKey, self).__init__(**kwargs)

def contribute_to_class(self, cls, name):
super(ForeignKey, self).contribute_to_class(cls, name)

class MyManager(Manager):
def __call__(self2, user):
return cls._default_manager.filter(**{self.name: user})

cls.add_to_class(self.manager_name, MyManager())


So now, what does this do?

We are subclassing ForeignKey. In __init__ we make sure to is set to User and we also set self.manager_name equal to either the manager_name kwarg, if provided or 'for_user'. contribute_to_class get called by the ModelMetaclass to add each item to the Model itself. So here we call the parent method, to get the ForeignKey itself set on the model, and then we create a new subclass of Manager. And we define an __call__ method on it, this lets us call an instance as if it were a function. And we make __call__ return the QuerySet that would be returned by filtering the default manager for the class where the user field is equal to the given user. And then we add it to the class with the name provided earlier.

And that's all. Now we can do things like:

MyModel.for_user(request.user)

Next post we'll probably look at making this more generic.

4 comments:

  1. Nice first post and glad to see you posting Alex!

    ReplyDelete
  2. Very cool idea. I haven't seen this approach before. I really like this sort of abstraction. I look forward to more posts from you.

    ReplyDelete
  3. Why so many magic for what can be accomplished with simple reusable Manager class?

    class MyManager(models.Manager):
    def get_for_user(self, user):
    return self.get_query_set().filter(user=user)

    class MyModel(models.Model)
    user = models.ForeignKey(User)
    objects = MyManager()

    MyModel.objects.get_for_user(user)

    ReplyDelete
  4. Andy, the main advantage of this is that it doesn't require the user foreign key to be named "user", and also this same technique can be abstracted, as I discuss in the next post.

    ReplyDelete

Note: Only a member of this blog may post a comment.