开发者

'private' models, default query sets and chaining methods

I have a private boolean flag on my model, and a custom manager that overwrites the get_query_set method, with a filter, removing private=True:

class myManager(models.Manager):
    def get_query_set(self):
        qs = super(myManager, self).get_query_set()
        qs = qs.filter(private=False)
        return qs

class myModel(models.Model):
    private = models.BooleanField(default=False)
    owner = models.ForeignKey('Profile', related_name="owned")
    #...etc...

    objects = myManager()

I want the default queryset to exclude the private models be default as a security measure, preventing accidental usage of the model showing private models.

Sometimes, however, I will want to show the private models, so I have the following on the manager:

def for_user(self, user):
    if user and not user.is_authenticated():
        return self.get_query_set()
    qs = super(myManager, self).get_query_set()
    qs = qs.filter(Q(owner=user, private=True) | Q(private=False))
    return qs

This works excellently, with the limitation that I can't chain the filter. This becomes a problem when I have a fk pointing the myModel and use otherModel.mymodel_set. otherModel.mymodel_set.for_user(user) wont work because mymodel_set returns a QuerySet object, rather than the manager.

Now the real problem starts, as I can't see a way to make the for_user() method work on a QuerySet subclass, because I can't access the full, unfiltered queryset (basically overwriting the get_query_set) form the QuerySet subclass, like I can in the manager (using super() to get the base queryset.)

What is the best way to work around this?

I'm not tied to any particular interface, but I would like it to be as djangoy/DRY as it can be. Obviously I could drop the security and just call a method to filter out private tasks on each call开发者_StackOverflow, but I really don't want to have to do that.

Update

manji's answer below is very close, however it fails when the queryset I want isn't a subset of the default queryset. I guess the real question here is how can I remove a particular filter from a chained query?


Define a custom QuerySet (containing your custom filter methods):

class MyQuerySet(models.query.QuerySet):

    def public(self):
        return self.filter(private=False)

    def for_user(self, user):
        if user and not user.is_authenticated():
            return self.public()
        return self.filter(Q(owner=user, private=True) | Q(private=False))

Define a custom manager that will use MyQuerySet (MyQuerySet custom filters will be accessible as if they were defined in the manager[by overriding __getattr__]):

# A Custom Manager accepting custom QuerySet
class MyManager(models.Manager):

    use_for_related_fields = True

    def __init__(self, qs_class=models.query.QuerySet):
        self.queryset_class = qs_class
        super(QuerySetManager, self).__init__()

    def get_query_set(self):
        return self.queryset_class(self.model).public()

    def __getattr__(self, attr, *args):
        try:
            return getattr(self.__class__, attr, *args)
        except AttributeError:
            return getattr(self.get_query_set(), attr, *args) 

Then in the model:

class MyModel(models.Model):
    private = models.BooleanField(default=False)
    owner = models.ForeignKey('Profile', related_name="owned")
    #...etc...

    objects = myManager(MyQuerySet)

Now you can:

   ¤ access by default only public models:

    MyModel.objects.filter(..

   ¤ access for_user models:

    MyModel.objects.for_user(user1).filter(..

Because of (use_for_related_fields = True), this same manager wil be used for related managers. So you can also:

   ¤ access by default only public models from related managers:

    otherModel.mymodel_set.filter(..

   ¤ access for_user from related managers:

    otherModel.mymodel_set.for_user(user).filter(..


More informations: Subclassing Django QuerySets & Custom managers with chainable filters (django snippet)


To use the chain you should override the get_query_set in your manager and place the for_user in your custom QuerySet.

I don't like this solution, but it works.

class CustomQuerySet(models.query.QuerySet):
    def for_user(self):
        return super(CustomQuerySet, self).filter(*args, **kwargs).filter(private=False)

class CustomManager(models.Manager):
    def get_query_set(self):
        return CustomQuerySet(self.model, using=self._db)


If you need to "reset" the QuerySet you can access the model of the queryset and call the original manager again (to fully reset). However that's probably not very useful for you, unless you were keeping track of the previous filter/exclude etc statements and can replay them again on the reset queryset. With a bit of planning that actually wouldn't be too hard to do, but may be a bit brute force.

Overall manji's answer is definitely the right way to go.

So amending manji's answer you need to replace the existing "model"."private" = False with ("model"."owner_id" = 2 AND "model"."private" = True ) OR "model"."private" = False ). To do that you will need to walk through the where object on the query object of the queryset to find the relevant bit to remove. The query object has a WhereNode object that represents the tree of the where clause, with each node having multiple children. You'd have to call the as_sql on the node to figure out if it's the one you are after:


from django.db import connection
qn = connection.ops.quote_name
q = myModel.objects.all()
print q.query.where.children[0].as_sql(qn, connection)

Which should give you something like:


('"model"."private" = ?', [False])

However trying to do that is probably way more effort than it's worth and it's delving into bits of Django that are probably not API-stable.

My recommendation would be to use two managers. One that can access everything (an escape hatch of sort), the other with the default filtering applied. The default manager is the first one, so you need to play around with the ordering depending on what you need to do. Then restructure your code to know which one to use - so you don't have the problem of having the extra private=False clause in there already.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜