views:

1212

answers:

3

I was wondering if it was possible (and, if so, how) to chain together multiple managers to produce a query set that is affected by both of the individual managers. I'll explain the specific example that I'm working on:

I have multiple abstract model classes that I use to provide small, specific functionality to other models. Two of these models are a DeleteMixin and a GlobalMixin.

The DeleteMixin is defined as such:

class DeleteMixin(models.Model):
    deleted = models.BooleanField(default=False)
    objects = DeleteManager()

    class Meta:
        abstract = True

    def delete(self):
        self.deleted = True
        self.save()

Basically it provides a pseudo-delete (the deleted flag) instead of actually deleting the object.

The GlobalMixin is defined as such:

class GlobalMixin(models.Model):
    is_global = models.BooleanField(default=True)

    objects = GlobalManager()

    class Meta:
        abstract = True

It allows any object to be defined as either a global object or a private object (such as a public/private blog post).

Both of these have their own managers that affect the queryset that is returned. My DeleteManager filters the queryset to only return results that have the deleted flag set to False, while the GlobalManager filters the queryset to only return results that are marked as global. Here is the declaration for both:

class DeleteManager(models.Manager):
    def get_query_set(self):
        return super(DeleteManager, self).get_query_set().filter(deleted=False)

class GlobalManager(models.Manager):
    def globals(self):
        return self.get_query_set().filter(is_global=1)

The desired functionality would be to have a model extend both of these abstract models and grant the ability to only return the results that are both non-deleted and global. I ran a test case on a model with 4 instances: one was global and non-deleted, one was global and deleted, one was non-global and non-deleted, and one was non-global and deleted. If I try to get result sets as such: SomeModel.objects.all(), I get instance 1 and 3 (the two non-deleted ones - great!). If I try SomeModel.objects.globals(), I get an error that DeleteManager doesn't have a globals (this is assuming my model declaration is as such: SomeModel(DeleteMixin, GlobalMixin). If I reverse the order, I don't get the error, but it doesn't filter out the deleted ones). If I change GlobalMixin to attach GlobalManager to globals instead of objects (so the new command would be SomeModel.globals.globals()), I get instances 1 and 2 (the two globals), while my intended result would be to only get instance 1 (the global, non-deleted one).

I wasn't sure if anyone had run into any situation similar to this and had come to a result. Either a way to make it work in my current thinking or a re-work that provides the functionality I'm after would be very much appreciated. I know this post has been a little long-winded. If any more explanation is needed, I would be glad to provide it.

Edit:

I have posted the eventual solution I used to this specific problem below. It is based on the link to Simon's custom QuerySetManager.

+4  A: 

See this snippet on Djangosnippets: http://www.djangosnippets.org/snippets/734/

Instead of putting your custom methods in a manager, you subclass the queryset itself. It's very easy and works perfectly. The only issue I've had is with model inheritance, you always have to define the manager in model subclasses (just: "objects = QuerySetManager()" in the subclass), even though they will inherit the queryset. This will make more sense once you are using QuerySetManager.

Scott
That seems to have done the trick. I should've known to focus more on the query set and less on the manager. I posted my eventual solution as an answer, although I chose yours as the best.
Adam
+1  A: 

I spent a while trying to come up with a way to build a nice factory to do this, but I'm running into a lot of problems with that.

The best I can suggest to you is to chain your inheritance. It's not very generic, so I'm not sure how useful it is, but all you would have to do is:

class GlobalMixin(DeleteMixin):
    is_global = models.BooleanField(default=True)

    objects = GlobalManager()

    class Meta:
        abstract = True

class GlobalManager(DeleteManager):
    def globals(self):
        return self.get_query_set().filter(is_global=1)

If you want something more generic, the best I can come up with is to define a base Mixin and Manager that redefines get_query_set() (I'm assuming you only want to do this once; things get pretty complicated otherwise) and then pass a list of fields you'd want added via Mixins.

It would look something like this (not tested at all):

class DeleteMixin(models.Model):
    deleted = models.BooleanField(default=False)

    class Meta:
        abstract = True

def create_mixin(base_mixin, **kwargs):
    class wrapper(base_mixin):
        class Meta:
            abstract = True
    for k in kwargs.keys():
        setattr(wrapper, k, kwargs[k])
    return wrapper

class DeleteManager(models.Manager):
    def get_query_set(self):
        return super(DeleteManager, self).get_query_set().filter(deleted=False)

def create_manager(base_manager, **kwargs):
    class wrapper(base_manager):
        pass
    for k in kwargs.keys():
        setattr(wrapper, k, kwargs[k])
    return wrapper

Ok, so this is ugly, but what does it get you? Essentially, it's the same solution, but much more dynamic, and a little more DRY, though more complex to read.

First you create your manager dynamically:

def globals(inst):
    return inst.get_query_set().filter(is_global=1)

GlobalDeleteManager = create_manager(DeleteManager, globals=globals)

This creates a new manager which is a subclass of DeleteManager and has a method called globals.

Next, you create your mixin model:

GlobalDeleteMixin = create_mixin(DeleteMixin,
                                 is_global=models.BooleanField(default=False),
                                 objects = GlobalDeleteManager())

Like I said, it's ugly. But it means you don't have to redefine globals(). If you want a different type of manager to have globals(), you just call create_manager again with a different base. And you can add as many new methods as you like. Same for the manager, you just keep adding new functions that will return different querysets.

So, is this really practical? Maybe not. This answer is more an exercise in (ab)using Python's flexibility. I haven't tried using this, though I do use some of the underlying principals of dynamically extending classes to make things easier to access.

Let me know if anything is unclear and I'll update the answer.

tghw
Chaining the inheritance would definitely work, but sadly it would defeat the purpose of my mixins. There are instances (I'm not sure when), where I would want something to just have GlobalMixin or just have DeleteMixin (or have both). I'll definitely have to look into your other suggestion. I agree that it's a little ugly, but maybe it can be cleaned to provide the similar functionality in a cleaner package.
Adam
You can make them with this method by just passing in models.Model as the base mixin and models.Manager as the base manager, but it's only worth while if you're going to have a lot of permutations of mixins and managers.
tghw
+3  A: 

Here is the specific solution to my problem using the custom QuerySetManager by Simon that Scott linked to.

from django.db import models
from django.contrib import admin
from django.db.models.query import QuerySet
from django.core.exceptions import FieldError

class MixinManager(models.Manager):    
    def get_query_set(self):
        try:
            return self.model.MixinQuerySet(self.model).filter(deleted=False)
        except FieldError:
            return self.model.MixinQuerySet(self.model)

class BaseMixin(models.Model):
    admin = models.Manager()
    objects = MixinManager()

    class MixinQuerySet(QuerySet):

        def globals(self):
            try:
                return self.filter(is_global=True)
            except FieldError:
                return self.all()

    class Meta:
        abstract = True

class DeleteMixin(BaseMixin):
    deleted = models.BooleanField(default=False)

    class Meta:
        abstract = True

    def delete(self):
        self.deleted = True
        self.save()

class GlobalMixin(BaseMixin):
    is_global = models.BooleanField(default=True)

    class Meta:
        abstract = True

Any mixin in the future that wants to add extra functionality to the query set simply needs to extend BaseMixin (or have it somewhere in its heirarchy). Any time I try to filter the query set down, I wrapped it in a try-catch in case that field doesn't actually exist (ie, it doesn't extend that mixin). The global filter is invoked using globals(), while the delete filter is automatically invoked (if something is deleted, I never want it to show). Using this system allows for the following types of commands:

TemporaryModel.objects.all() # If extending DeleteMixin, no deleted instances are returned
TemporaryModel.objects.all().globals() # Filter out the private instances (non-global)
TemporaryModel.objects.filter(...) # Ditto about excluding deleteds

One thing to note is that the delete filter won't affect admin interfaces, because the default Manager is declared first (making it the default). I don't remember when they changed the admin to use Model._default_manager instead of Model.objects, but any deleted instances will still appear in the admin (in case you need to un-delete them).

Adam