Almabase Blog

Using django model managers for dynamic behaviour

Posted by Almabase on Mar 1, 2016 2:49:47 PM
Django models perform database query operations through managers. At least one Manager exists for every model in a Django application. By default it is accessed using “.objects” In order to explain the problem and the solution, I will use a simple example. We have Book and Author as two of the models among others. (Yes books and authors as examples to teach programming never gets old :))
class Book(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey(Author) class Author(models.Model): name = models.CharField(max_length=200)
Each user is given permission to an author and the user can perform actions(query, update, insert) on data for that author alone. The default manager can be used to achieve this but you will have to pass in author_id carefully in every queryset that you write. Examples:
Book.objects.get(author_id=2, name=’Harry Potter’) Book.objects.filter(name__contains=’Huckleberry’, author_id=1)
These querysets will be written at so many places in the code. This is not safe. You could forget passing the author id in some query and that gives you data across various authors. Solution 1 - Not scalable You could use different managers for each author. Something like this:
class TwainBookManager(models.Manager): def get_queryset(self): return super(TwainBookManager.self).get_queryset().filter(author_id=1) class RowlingBookManager(models.Manager): def get_queryset(self): return super(RowlingBookManager,self).get_queryset().filter(author_id=2) class Book(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey(Author) objects = models.Manager() # The default manager. twain_objects = TwainBookManager() rowling_objects = RowlingBookManager()
You’ll have to define this for every author if you want to achieve this behaviour. As you can see, this is not scalable. Solution 2 - Scalable & safe Using class constructors to achieve dynamic behavior.
class CustomBookManager(models.Manager): “”“Custom model manager for model Book that acts on specific author related objects””” author_id = None def __init__(self, model, author_id): super(CustomBookManager, self).__init__() self.model = model self.author_id = author_id def get_queryset(self): return super(CustomBookManager,self).get_queryset().filter(author_id=self.author_id)
In the book model, you can define
@classmethod def objects(cls, site_id): return CustomBookManager(cls, site_id)
Now, you could access author specific objects like this -
#Twain books Book.objects(1).all() Book.objects(1).filter(name__contains=’Huckleberry’) #Rowling books Book.objects(2).all() Book.objects(2).filter(name__contains=’Harry’)
This is now dynamic based on what author the user has permission for.
#Some function that gives the author the user is permissioned for author_id = request.user.get_permissioned_author_id() Book.objects(author_id).all()
This is safe because when other developers develop on top of this, and they write “Book.objects.all()” by mistake, it throws up an error. You do not want a query to access all objects across the database. In case you want to be able to access objects across all authors(say for superuser), you can provide another manager with “all_objects” which makes it clear to the developer that he is specifically asking for all objects
all_objects = models.Manager()
Beyond querying you can also easily extend this to creating new objects. By adding this method to the CustomBookManager,
def create(self, **kwargs): kwargs[‘site_id’] = self.site_id return super(BaseSiteModelManager,self).create(**kwargs)
You can now create books like this
Book.objects(1).create(name=’Huckleberry Finn’) #Twain book Book.objects(2).create(name=’Harry Potter 5’) #Rowling book
If you have multiple models like ‘Book’ where you need this behaviour, it is best to write an abstract base model where you define the above manager and derive that base model for all the models where this functionality is needed. I hope this is useful.