Wednesday, June 19, 2013

Django Custom Chainable QuerySets

How to reuse custom defined 'models.Manager' QuerySets

Django recommends to define custom query sets on your sublcassed models.Manager, there is one problem however that these are not chainable, that is you can only call your custom queryset once - assuming I have defined an '.active()' queryset the following will not work:

>> PaymentTerm.objects.active().active()
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: 'PaymentTermManager' object has no attribute 'active'
>>>

Current solutions on the web involve creating a subclassed 'models.Manager' and a custom 'QuerySet' which do work but are to code verbose for my linking. I believe the below approach is simpler, cleaner and more succinct.

class OfferManager(models.Manager):
 
 ...
 
 STATUS_CHOICES = (
  (STATUS_DISABLED, "Disabled"),
  (STATUS_ENABLED, "Enabled"),
  (STATUS_NEGOTIATED, "Negotiated"),
  (STATUS_ARCHIVED, "Archived"),
 )
 QUERYSET_PUBLIC_KWARGS = {'status__gte': STATUS_ENABLED}
 QUERYSET_ACTIVE_KWARGS = {'status': STATUS_ENABLED}
  def get_query_set(self):
   """ Assing a modified QuerySet to include our chainable QuerySets """
   class OfferManagerQuerySet(QuerySet): # clone QuerySet
                pass
  # explictly copy our QuerySet methods
  OfferManagerQuerySet.public = self.public
  OfferManagerQuerySet.active = self.active

  ...
 
  return QuerySetDynamicallyCreated(self.model, using=self._db)
 def public(self):
  """ Returns all entries accessible through front end site"""
  return self.all().filter(**OfferManager.QUERYSET_PUBLIC_KWARGS)
 public.chainable = True
 def active(self):
  """ returns offers that are open to negotiation """
  return self.public().filter(**OfferManager.QUERYSET_ACTIVE_KWARGS)

 ...

Django patch for model.Manager chainable QuerySets

Please take a look at Django Ticket #20625 with a github Django branch for new proposed method for defining chainable filters based on this idea which works through a 'chainable == True' attribute as shown below:

class OfferManager(models.Manager):

    ...
    
    def public(self):
        """ Returns all entries accessible through front end site"""
        return self.all().filter(...)
    public.chainable = True     # instructs to dynamically tranplat this method onto
                                # returned QuerySet as .public(...) 
                                # effectively providing chainable custom QuerySets

    def active(self):
        """ Returns offers that are open to negotiation """
        return self.public().filter(**OfferManager.QUERYSET_ACTIVE_KWARGS)
                                    # an example of how to reffer to OfferManager
                                    # constants as 'self' context changes
    active.chainable = True
    ...

As always feel free to comment or follow me on twitter @danielsokolowski

No comments:

Post a Comment