Permalink
Browse files

Added InheritanceManager, contributed by Jeff Elmore.

  • Loading branch information...
1 parent 820f479 commit ac36cbf56cd9a9a5d2e9268616aa986b408f8bca @carljm carljm committed Nov 23, 2010
Showing with 168 additions and 48 deletions.
  1. +1 −0 AUTHORS.rst
  2. +3 −0 CHANGES.rst
  3. +74 −22 README.rst
  4. +1 −3 TODO.rst
  5. +35 −0 model_utils/managers.py
  6. +10 −1 model_utils/tests/models.py
  7. +44 −22 model_utils/tests/tests.py
View
@@ -1,3 +1,4 @@
Carl Meyer <carl@dirtcircle.com>
Jannis Leidel <jannis@leidel.info>
Gregor Müllegger <gregor@muellegger.de>
+Jeff Elmore <jeffelmore.org>
View
@@ -4,6 +4,9 @@ CHANGES
tip (unreleased)
----------------
+- added InheritanceManager, a better approach to selecting subclass instances
+ for Django 1.2+. Thanks Jeff Elmore.
+
- added InheritanceCastManager and InheritanceCastQuerySet, to allow bulk
casting of a queryset to child types. Thanks Gregor Müllegger.
View
@@ -24,7 +24,8 @@ your ``INSTALLED_APPS`` setting.
Dependencies
------------
-``django-model-utils`` requires `Django`_ 1.0 or later.
+Most of ``django-model-utils`` works with `Django`_ 1.0 or later.
+`InheritanceManager`_ requires Django 1.2 or later.
.. _Django: http://www.djangoproject.com/
@@ -218,38 +219,89 @@ returns objects with that status only::
# this query will only return published articles:
Article.published.all()
-InheritanceCastModel
-====================
+InheritanceManager
+==================
-This abstract base class can be inherited by the root (parent) model
-in a model-inheritance tree. It allows each model in the tree to
-"know" what type it is (via an automatically-set foreign key to
-``ContentType``), allowing for automatic casting of a parent instance
-to its proper leaf (child) type.
+This manager (`contributed by Jeff Elmore`_) should be attached to a base model
+class in a model-inheritance tree. It allows queries on that base model to
+return heterogenous results of the actual proper subtypes, without any
+additional queries.
-For instance, if you have a ``Place`` model with subclasses
-``Restaurant`` and ``Bar``, you may want to query all Places::
+For instance, if you have a ``Place`` model with subclasses ``Restaurant`` and
+``Bar``, you may want to query all Places::
nearby_places = Place.objects.filter(location='here')
But when you iterate over ``nearby_places``, you'll get only ``Place``
-instances back, even for objects that are "really" ``Restaurant`` or
-``Bar``. If you have ``Place`` inherit from ``InheritanceCastModel``,
-you can just call the ``cast()`` method on each ``Place`` and it will
-return an instance of the proper subtype, ``Restaurant`` or ``Bar``::
+instances back, even for objects that are "really" ``Restaurant`` or ``Bar``.
+If you attach an ``InheritanceManager`` to ``Place``, you can just call the
+``select_subclasses()`` method on the ``InheritanceManager`` or any
+``QuerySet`` from it, and the resulting objects will be instances of
+``Restaurant`` or ``Bar``::
+
+ from model_utils.managers import InheritanceManager
+
+ class Place(models.Model):
+ # ...
+ objects = InheritanceManager()
+
+ class Restaurant(Place):
+ # ...
+
+ class Bar(Place):
+ # ...
+
+ nearby_places = Place.objects.filter(location='here').select_subclasses()
+ for place in nearby_places:
+ # "place" will automatically be an instance of Place, Restaurant, or Bar
+
+The database query performed will have an extra join for each subclass; if you
+want to reduce the number of joins and you only need particular subclasses to
+be returned as their actual type, you can pass subclass names to
+``select_subclasses()``, much like the built-in ``select_related()`` method::
+
+ nearby_places = Place.objects.select_subclasses("restaurant")
+ # restaurants will be Restaurant instances, bars will still be Place instances
+
+If you don't explicitly call ``select_subclasses()``, an ``InheritanceManager``
+behaves identically to a normal ``Manager``; so it's safe to use as your
+default manager for the model.
+
+.. note::
+ ``InheritanceManager`` currently only supports a single level of model
+ inheritance; it won't work for grandchild models.
+
+.. note::
+ ``InheritanceManager`` requires Django 1.2 or later.
+
+.. _contributed by Jeff Elmore: http://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/
+
+
+InheritanceCastModel
+====================
+
+This abstract base class can be inherited by the root (parent) model in a
+model-inheritance tree. It solves the same problem as `InheritanceManager`_ in
+a way that requires more database queries and is less convenient to use, but is
+compatible with Django versions prior to 1.2. Whenever possible,
+`InheritanceManager`_ should be used instead.
+
+Usage::
from model_utils.models import InheritanceCastModel
class Place(InheritanceCastModel):
# ...
-
+
class Restaurant(Place):
# ...
+ class Bar(Place):
+ # ...
+
nearby_places = Place.objects.filter(location='here')
for place in nearby_places:
- restaurant_or_bar = place.cast()
- # ...
+ restaurant_or_bar = place.cast() # ...
This is inefficient for large querysets, as it results in a new query for every
individual returned object. You can use the ``cast()`` method on a queryset to
@@ -259,16 +311,16 @@ reduce this to as many queries as subtypes are involved::
for place in nearby_places.cast():
# ...
-.. note:: The ``cast()`` queryset method does *not* return another
- queryset but an already evaluated result of the database query. This means
- that you cannot chain additional queryset methods after ``cast()``.
+.. note::
+ The ``cast()`` queryset method does *not* return another queryset but an
+ already evaluated result of the database query. This means that you cannot
+ chain additional queryset methods after ``cast()``.
TimeStampedModel
================
This abstract base class just provides self-updating ``created`` and
-``modified`` fields on any model that inherits from it.
-
+``modified`` fields on any model that inherits from it.
QueryManager
============
View
@@ -1,6 +1,4 @@
TODO
====
-* A version of InheritanceCastModel for 1.2+ (with reverse OneToOne
- select_related now available) that doesn't require the added real_type
- field.
+* Add support for multiple levels of inheritance to ``InheritanceManager``.
View
@@ -2,9 +2,44 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
+from django.db.models.fields.related import SingleRelatedObjectDescriptor
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
+class InheritanceQuerySet(QuerySet):
+ def select_subclasses(self, *subclasses):
+ if not subclasses:
+ subclasses = [o for o in dir(self.model)
+ if isinstance(getattr(self.model, o), SingleRelatedObjectDescriptor)
+ and issubclass(getattr(self.model,o).related.model, self.model)]
+ new_qs = self.select_related(*subclasses)
+ new_qs.subclasses = subclasses
+ return new_qs
+
+ def _clone(self, klass=None, setup=False, **kwargs):
+ try:
+ kwargs.update({'subclasses': self.subclasses})
+ except AttributeError:
+ pass
+ return super(InheritanceQuerySet, self)._clone(klass, setup, **kwargs)
+
+ def iterator(self):
+ iter = super(InheritanceQuerySet, self).iterator()
+ if getattr(self, 'subclasses', False):
+ for obj in iter:
+ obj = [getattr(obj, s) for s in self.subclasses if getattr(obj, s)] or [obj]
+ yield obj[0]
+ else:
+ for obj in iter:
+ yield obj
+
+class InheritanceManager(models.Manager):
+ def get_query_set(self):
+ return InheritanceQuerySet(self.model)
+
+ def select_subclasses(self, *subclasses):
+ return self.get_query_set().select_subclasses(*subclasses)
+
class InheritanceCastMixin(object):
def cast(self):
@@ -2,7 +2,7 @@
from django.utils.translation import ugettext_lazy as _
from model_utils.models import InheritanceCastModel, TimeStampedModel, StatusModel, TimeFramedModel
-from model_utils.managers import QueryManager, manager_from
+from model_utils.managers import QueryManager, manager_from, InheritanceManager
from model_utils.fields import SplitField, MonitorField
from model_utils import Choices
@@ -15,6 +15,15 @@ class InheritChild(InheritParent):
class InheritChild2(InheritParent):
pass
+class InheritanceManagerTestParent(models.Model):
+ objects = InheritanceManager()
+
+class InheritanceManagerTestChild1(InheritanceManagerTestParent):
+ pass
+
+class InheritanceManagerTestChild2(InheritanceManagerTestParent):
+ pass
+
class TimeStamp(TimeStampedModel):
pass
Oops, something went wrong.

0 comments on commit ac36cbf

Please sign in to comment.