Skip to content
Permalink
Browse files

Added InheritanceManager, contributed by Jeff Elmore.

  • Loading branch information
carljm committed Nov 23, 2010
1 parent 820f479 commit ac36cbf56cd9a9a5d2e9268616aa986b408f8bca
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
@@ -1,3 +1,4 @@
Carl Meyer <carl@dirtcircle.com>
Jannis Leidel <jannis@leidel.info>
Gregor Müllegger <gregor@muellegger.de>
Jeff Elmore <jeffelmore.org>
@@ -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.

@@ -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
============
@@ -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``.
@@ -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

0 comments on commit ac36cbf

Please sign in to comment.
You can’t perform that action at this time.