Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #10356 -- Added pure-Python inheritance for models (a.k.a proxy…

… models).

Large portions of this are needed for #5420, so I implemented it fully.
Thanks to Ryan Kelly for an initial patch to get this started.

Refs #5420.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10083 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 61a2708c4108939795c70cf124d5696275d6c255 1 parent c0b6e23
@malcolmt malcolmt authored
View
1  AUTHORS
@@ -220,6 +220,7 @@ answer newbie questions, and generally made Django that much better:
Erik Karulf <erik@karulf.com>
Ben Dean Kawamura <ben.dean.kawamura@gmail.com>
Ian G. Kelly <ian.g.kelly@gmail.com>
+ Ryan Kelly <ryan@rfk.id.au>
Thomas Kerpe <thomas@kerpe.net>
Ossama M. Khayat <okhayat@yahoo.com>
Ben Khoo <khoobks@westnet.com.au>
View
170 django/db/models/base.py
@@ -67,9 +67,19 @@ def __new__(cls, name, bases, attrs):
if not hasattr(meta, 'get_latest_by'):
new_class._meta.get_latest_by = base_meta.get_latest_by
+ is_proxy = new_class._meta.proxy
+
if getattr(new_class, '_default_manager', None):
- new_class._default_manager = None
- new_class._base_manager = None
+ if not is_proxy:
+ # Multi-table inheritance doesn't inherit default manager from
+ # parents.
+ new_class._default_manager = None
+ new_class._base_manager = None
+ else:
+ # Proxy classes do inherit parent's default manager, if none is
+ # set explicitly.
+ new_class._default_manager = new_class._default_manager._copy_to_model(new_class)
+ new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
# Bail out early if we have already created this class.
m = get_model(new_class._meta.app_label, name, False)
@@ -80,21 +90,43 @@ def __new__(cls, name, bases, attrs):
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
+ # All the fields of any type declared on this model
+ new_fields = new_class._meta.local_fields + \
+ new_class._meta.local_many_to_many + \
+ new_class._meta.virtual_fields
+ field_names = set([f.name for f in new_fields])
+
+ # Basic setup for proxy models.
+ if is_proxy:
+ base = None
+ for parent in [cls for cls in parents if hasattr(cls, '_meta')]:
+ if parent._meta.abstract:
+ if parent._meta.fields:
+ raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name)
+ else:
+ continue
+ if base is not None:
+ raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name)
+ else:
+ base = parent
+ if base is None:
+ raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
+ if (new_class._meta.local_fields or
+ new_class._meta.local_many_to_many):
+ raise FieldError("Proxy model '%s' contains model fields."
+ % name)
+ new_class._meta.setup_proxy(base)
+
# Do the appropriate setup for any model parents.
o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
if isinstance(f, OneToOneField)])
+
for base in parents:
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
- # All the fields of any type declared on this model
- new_fields = new_class._meta.local_fields + \
- new_class._meta.local_many_to_many + \
- new_class._meta.virtual_fields
- field_names = set([f.name for f in new_fields])
-
parent_fields = base._meta.local_fields + base._meta.local_many_to_many
# Check for clashes between locally declared fields and those
# on the base classes (we cannot handle shadowed fields at the
@@ -107,15 +139,19 @@ def __new__(cls, name, bases, attrs):
(field.name, name, base.__name__))
if not base._meta.abstract:
# Concrete classes...
+ while base._meta.proxy:
+ # Skip over a proxy class to the "real" base it proxies.
+ base = base._meta.proxy_for_model
if base in o2o_map:
field = o2o_map[base]
- else:
+ elif not is_proxy:
attr_name = '%s_ptr' % base._meta.module_name
field = OneToOneField(base, name=attr_name,
auto_created=True, parent_link=True)
new_class.add_to_class(attr_name, field)
+ else:
+ field = None
new_class._meta.parents[base] = field
-
else:
# .. and abstract ones.
for field in parent_fields:
@@ -125,13 +161,12 @@ def __new__(cls, name, bases, attrs):
new_class._meta.parents.update(base._meta.parents)
# Inherit managers from the abstract base classes.
- base_managers = base._meta.abstract_managers
- base_managers.sort()
- for _, mgr_name, manager in base_managers:
- val = getattr(new_class, mgr_name, None)
- if not val or val is manager:
- new_manager = manager._copy_to_model(new_class)
- new_class.add_to_class(mgr_name, new_manager)
+ new_class.copy_managers(base._meta.abstract_managers)
+
+ # Proxy models inherit the non-abstract managers from their base,
+ # unless they have redefined any of them.
+ if is_proxy:
+ new_class.copy_managers(base._meta.concrete_managers)
# Inherit virtual fields (like GenericForeignKey) from the parent
# class
@@ -160,6 +195,15 @@ def __new__(cls, name, bases, attrs):
# registered version.
return get_model(new_class._meta.app_label, name, False)
+ def copy_managers(cls, base_managers):
+ # This is in-place sorting of an Options attribute, but that's fine.
+ base_managers.sort()
+ for _, mgr_name, manager in base_managers:
+ val = getattr(cls, mgr_name, None)
+ if not val or val is manager:
+ new_manager = manager._copy_to_model(cls)
+ cls.add_to_class(mgr_name, new_manager)
+
def add_to_class(cls, name, value):
if hasattr(value, 'contribute_to_class'):
value.contribute_to_class(cls, name)
@@ -358,55 +402,59 @@ def save_base(self, raw=False, cls=None, force_insert=False,
# At this point, parent's primary key field may be unknown
# (for example, from administration form which doesn't fill
# this field). If so, fill it.
- if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
+ if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
- self.save_base(raw, parent)
- setattr(self, field.attname, self._get_pk_val(parent._meta))
-
- non_pks = [f for f in meta.local_fields if not f.primary_key]
-
- # First, try an UPDATE. If that doesn't update anything, do an INSERT.
- pk_val = self._get_pk_val(meta)
- pk_set = pk_val is not None
- record_exists = True
- manager = cls._base_manager
- if pk_set:
- # Determine whether a record with the primary key already exists.
- if (force_update or (not force_insert and
- manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())):
- # It does already exist, so do an UPDATE.
- if force_update or non_pks:
- values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
- rows = manager.filter(pk=pk_val)._update(values)
- if force_update and not rows:
- raise DatabaseError("Forced update did not affect any rows.")
- else:
- record_exists = False
- if not pk_set or not record_exists:
- if not pk_set:
- if force_update:
- raise ValueError("Cannot force an update in save() with no primary key.")
- values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)]
- else:
- values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields]
+ self.save_base(cls=parent)
+ if field:
+ setattr(self, field.attname, self._get_pk_val(parent._meta))
+ if meta.proxy:
+ return
+
+ if not meta.proxy:
+ non_pks = [f for f in meta.local_fields if not f.primary_key]
+
+ # First, try an UPDATE. If that doesn't update anything, do an INSERT.
+ pk_val = self._get_pk_val(meta)
+ pk_set = pk_val is not None
+ record_exists = True
+ manager = cls._base_manager
+ if pk_set:
+ # Determine whether a record with the primary key already exists.
+ if (force_update or (not force_insert and
+ manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())):
+ # It does already exist, so do an UPDATE.
+ if force_update or non_pks:
+ values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
+ rows = manager.filter(pk=pk_val)._update(values)
+ if force_update and not rows:
+ raise DatabaseError("Forced update did not affect any rows.")
+ else:
+ record_exists = False
+ if not pk_set or not record_exists:
+ if not pk_set:
+ if force_update:
+ raise ValueError("Cannot force an update in save() with no primary key.")
+ values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)]
+ else:
+ values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields]
- if meta.order_with_respect_to:
- field = meta.order_with_respect_to
- values.append((meta.get_field_by_name('_order')[0], manager.filter(**{field.name: getattr(self, field.attname)}).count()))
- record_exists = False
+ if meta.order_with_respect_to:
+ field = meta.order_with_respect_to
+ values.append((meta.get_field_by_name('_order')[0], manager.filter(**{field.name: getattr(self, field.attname)}).count()))
+ record_exists = False
- update_pk = bool(meta.has_auto_field and not pk_set)
- if values:
- # Create a new record.
- result = manager._insert(values, return_id=update_pk)
- else:
- # Create a new record with defaults for everything.
- result = manager._insert([(meta.pk, connection.ops.pk_default_value())], return_id=update_pk, raw_values=True)
+ update_pk = bool(meta.has_auto_field and not pk_set)
+ if values:
+ # Create a new record.
+ result = manager._insert(values, return_id=update_pk)
+ else:
+ # Create a new record with defaults for everything.
+ result = manager._insert([(meta.pk, connection.ops.pk_default_value())], return_id=update_pk, raw_values=True)
- if update_pk:
- setattr(self, meta.pk.attname, result)
- transaction.commit_unless_managed()
+ if update_pk:
+ setattr(self, meta.pk.attname, result)
+ transaction.commit_unless_managed()
if signal:
signals.post_save.send(sender=self.__class__, instance=self,
View
3  django/db/models/manager.py
@@ -60,6 +60,9 @@ def contribute_to_class(self, model, name):
if model._meta.abstract or self._inherited:
model._meta.abstract_managers.append((self.creation_counter, name,
self))
+ else:
+ model._meta.concrete_managers.append((self.creation_counter, name,
+ self))
def _set_creation_counter(self):
"""
View
19 django/db/models/options.py
@@ -21,7 +21,7 @@
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace',
- 'abstract', 'managed')
+ 'abstract', 'managed', 'proxy')
class Options(object):
def __init__(self, meta, app_label=None):
@@ -43,11 +43,15 @@ def __init__(self, meta, app_label=None):
self.has_auto_field, self.auto_field = False, None
self.abstract = False
self.managed = True
+ self.proxy = False
+ self.proxy_for_model = None
self.parents = SortedDict()
self.duplicate_targets = {}
- # Managers that have been inherited from abstract base classes. These
- # are passed onto any children.
+
+ # To handle various inheritance situations, we need to track where
+ # managers came from (concrete or abstract base classes).
self.abstract_managers = []
+ self.concrete_managers = []
def contribute_to_class(self, cls, name):
from django.db import connection
@@ -164,6 +168,15 @@ def setup_pk(self, field):
self.pk = field
field.serialize = False
+ def setup_proxy(self, target):
+ """
+ Does the internal setup so that the current model is a proxy for
+ "target".
+ """
+ self.pk = target._meta.pk
+ self.proxy_for_model = target
+ self.db_table = target._meta.db_table
+
def __repr__(self):
return '<Options for %s>' % self.object_name
View
51 django/db/models/sql/query.py
@@ -641,6 +641,7 @@ def get_default_columns(self, with_aliases=False, col_aliases=None,
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
aliases = set()
+ proxied_model = opts.proxy and opts.proxy_for_model or 0
if start_alias:
seen = {None: start_alias}
for field, model in opts.get_fields_with_model():
@@ -648,9 +649,12 @@ def get_default_columns(self, with_aliases=False, col_aliases=None,
try:
alias = seen[model]
except KeyError:
- link_field = opts.get_ancestor_link(model)
- alias = self.join((start_alias, model._meta.db_table,
- link_field.column, model._meta.pk.column))
+ if model is proxied_model:
+ alias = start_alias
+ else:
+ link_field = opts.get_ancestor_link(model)
+ alias = self.join((start_alias, model._meta.db_table,
+ link_field.column, model._meta.pk.column))
seen[model] = alias
else:
# If we're starting from the base model of the queryset, the
@@ -1158,11 +1162,15 @@ def setup_inherited_models(self):
opts = self.model._meta
root_alias = self.tables[0]
seen = {None: root_alias}
+ proxied_model = opts.proxy and opts.proxy_for_model or 0
for field, model in opts.get_fields_with_model():
if model not in seen:
- link_field = opts.get_ancestor_link(model)
- seen[model] = self.join((root_alias, model._meta.db_table,
- link_field.column, model._meta.pk.column))
+ if model is proxied_model:
+ seen[model] = root_alias
+ else:
+ link_field = opts.get_ancestor_link(model)
+ seen[model] = self.join((root_alias, model._meta.db_table,
+ link_field.column, model._meta.pk.column))
self.included_inherited_models = seen
def remove_inherited_models(self):
@@ -1559,20 +1567,25 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
raise MultiJoin(pos + 1)
if model:
# The field lives on a base class of the current model.
+ proxied_model = opts.proxy and opts.proxy_for_model or 0
for int_model in opts.get_base_chain(model):
- lhs_col = opts.parents[int_model].column
- dedupe = lhs_col in opts.duplicate_targets
- if dedupe:
- exclusions.update(self.dupe_avoidance.get(
- (id(opts), lhs_col), ()))
- dupe_set.add((opts, lhs_col))
- opts = int_model._meta
- alias = self.join((alias, opts.db_table, lhs_col,
- opts.pk.column), exclusions=exclusions)
- joins.append(alias)
- exclusions.add(alias)
- for (dupe_opts, dupe_col) in dupe_set:
- self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
+ if int_model is proxied_model:
+ opts = int_model._meta
+ else:
+ lhs_col = opts.parents[int_model].column
+ dedupe = lhs_col in opts.duplicate_targets
+ if dedupe:
+ exclusions.update(self.dupe_avoidance.get(
+ (id(opts), lhs_col), ()))
+ dupe_set.add((opts, lhs_col))
+ opts = int_model._meta
+ alias = self.join((alias, opts.db_table, lhs_col,
+ opts.pk.column), exclusions=exclusions)
+ joins.append(alias)
+ exclusions.add(alias)
+ for (dupe_opts, dupe_col) in dupe_set:
+ self.update_dupe_avoidance(dupe_opts, dupe_col,
+ alias)
cached_data = opts._join_cache.get(name)
orig_opts = opts
dupe_col = direct and field.column or field.field.column
View
10 docs/ref/models/options.txt
@@ -162,6 +162,16 @@ that has ``admin`` set. This example specifies an extra permission,
This is a list or tuple of 2-tuples in the format ``(permission_code,
human_readable_permission_name)``.
+``proxy``
+---------
+
+.. attribute:: Options.proxy
+
+.. versionadded: 1.1
+
+If set to ``True``, a model which subclasses another model will be treated as
+a :ref:`proxy model <proxy-models>`.
+
``unique_together``
-------------------
View
2  docs/topics/db/managers.txt
@@ -195,6 +195,8 @@ attribute on the manager class. This is documented fully below_.
.. _below: manager-types_
+.. _custom-managers-and-inheritance:
+
Custom managers and model inheritance
-------------------------------------
View
151 docs/topics/db/models.txt
@@ -773,13 +773,18 @@ is whether you want the parent models to be models in their own right
of common information that will only be visible through the child
models.
-Often, you will just want to use the parent class to hold information
-that you don't want to have to type out for each child model. This
-class isn't going to ever be used in isolation, so
-:ref:`abstract-base-classes` are what you're after. However, if you're
-subclassing an existing model (perhaps something from another
-application entirely), or want each model to have its own database
-table, :ref:`multi-table-inheritance` is the way to go.
+There are three styles of inheritance that are possible in Django.
+
+ 1. Often, you will just want to use the parent class to hold information that
+ you don't want to have to type out for each child model. This class isn't
+ going to ever be used in isolation, so :ref:`abstract-base-classes` are
+ what you're after.
+ 2. If you're subclassing an existing model (perhaps something from another
+ application entirely) and want each model to have its own database table,
+ :ref:`multi-table-inheritance` is the way to go.
+ 3. Finally, if you only want to modify the Python-level behaviour of a model,
+ without changing the models fields in any way, you can use
+ :ref:`proxy-models`.
.. _abstract-base-classes:
@@ -937,14 +942,16 @@ referring to ``p.restaurant`` would raise a Restaurant.DoesNotExist exception.
In the multi-table inheritance situation, it doesn't make sense for a child
class to inherit from its parent's :ref:`Meta <meta-options>` class. All the :ref:`Meta <meta-options>` options
have already been applied to the parent class and applying them again would
-normally only lead to contradictory behaviour (this is in contrast with the
+normally only lead to contradictory behavior (this is in contrast with the
abstract base class case, where the base class doesn't exist in its own
right).
-So a child model does not have access to its parent's :ref:`Meta <meta-options>` class. However,
-there are a few limited cases where the child inherits behaviour from the
-parent: if the child does not specify an :attr:`django.db.models.Options.ordering` attribute or a
-:attr:`django.db.models.Options.get_latest_by` attribute, it will inherit these from its parent.
+So a child model does not have access to its parent's :ref:`Meta
+<meta-options>` class. However, there are a few limited cases where the child
+inherits behavior from the parent: if the child does not specify an
+:attr:`django.db.models.Options.ordering` attribute or a
+:attr:`django.db.models.Options.get_latest_by` attribute, it will inherit
+these from its parent.
If the parent has an ordering and you don't want the child to have any natural
ordering, you can explicitly disable it::
@@ -990,6 +997,126 @@ own :class:`~django.db.models.fields.OneToOneField` and set
:attr:`parent_link=True <django.db.models.fields.OneToOneField.parent_link>`
to indicate that your field is the link back to the parent class.
+.. _proxy-models:
+
+Proxy models
+------------
+
+.. versionadded:: 1.1
+
+When using :ref:`multi-table inheritance <multi-table-inheritance>`, a new
+database table is created for each subclass of a model. This is usually the
+desired behavior, since the subclass needs a place to store any additional
+data fields that are not present on the base class. Sometimes, however, you
+only want to change the Python behavior of a model -- perhaps to change the
+default manager, or add a new method.
+
+This is what proxy model inheritance is for: creating a *proxy* for the
+original model. You can create, delete and update instances of the proxy model
+and all the data will be saved as if you were using the original (non-proxied)
+model. The difference is that you can change things like the default model
+ordering or the default manager in the proxy, without having to alter the
+original.
+
+Proxy models are declared like normal models. You tell Django that it's a
+proxy model by setting the :attr:`~django.db.models.Options.proxy` attribute to of the ``Meta`` class to ``True``.
+
+For example, suppose you want to add a method to the standard ``User`` model
+that will make be used in your templates. You can do it like this::
+
+ from django.contrib.auth.models import User
+
+ class MyUser(User):
+ class Meta:
+ proxy = True
+
+ def do_something(self):
+ ...
+
+The ``MyUser`` class operates on the same database table as its parent
+``User`` class. In particular, any new instances of ``User`` will also be
+accessible through ``MyUser``, and vice-versa::
+
+ >>> u = User.objects.create(username="foobar")
+ >>> MyUser.objects.get(username="foobar")
+ <MyUser: foobar>
+
+You could also use a proxy model to define a different default ordering on a
+model. The standard ``User`` model has no ordering defined on it
+(intentionally; sorting is expensive and we don't want to do it all the time
+when we fetch users). You might want to regularly order by the ``username``
+attribute when you use the proxy. This is easy::
+
+ class OrderedUser(User):
+ class Meta:
+ ordering = ["username"]
+ proxy = True
+
+Now normal ``User`` queries will be unorderd and ``OrderedUser`` queries will
+be ordered by ``username``.
+
+Querysets still return the model that was requested
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There is no way to have Django return, say, a ``MyUser`` object whenever you
+query for ``User`` objects. A queryset for ``User`` objects will return those
+types of objects. The whole point of proxy objects is that code relying on the
+original ``User`` will use those and your own code can use the extensions you
+included (that no other code is relying on anyway). It is not a way to replace
+the ``User`` (or any other) model everywhere with something of your own
+creation.
+
+Base class restrictions
+~~~~~~~~~~~~~~~~~~~~~~~
+
+A proxy model must inherit from exactly one non-abstract model class. You
+can't inherit from multiple non-abstract models as the proxy model doesn't
+provide any connection between the rows in the different database tables. A
+proxy model can inherit from any number of abstract model classes, providing
+they do *not* define any model fields.
+
+Proxy models inherit any ``Meta`` options that they don't define from their
+non-abstract model parent (the model they are proxying for).
+
+Proxy model managers
+~~~~~~~~~~~~~~~~~~~~
+
+If you don't specify any model managers on a proxy model, it inherits the
+managers from its model parents. If you define a manager on the proxy model,
+it will become the default, although any managers defined on the parent
+classes will still be available.
+
+Continuing our example from above, you could change the default manager used
+when you query the ``User`` model like this::
+
+ class NewManager(models.Manager):
+ ...
+
+ class MyUser(User):
+ objects = NewManager()
+
+ class Meta:
+ proxy = True
+
+If you wanted to add a new manager to the Proxy, without replacing the
+existing default, you can use the techniques described in the :ref:`custom
+manager <custom-managers-and-inheritance>` documentation: create a base class
+containing the new managers and inherit that after the primary base class::
+
+ # Create an abstract class for the new manager.
+ class ExtraManagers:
+ secondary = NewManager()
+
+ class Meta:
+ abstract = True
+
+ class MyUser(User, ExtraManagers):
+ class Meta:
+ proxy = True
+
+You probably won't need to do this very often, but, when you do, it's
+possible.
+
Multiple inheritance
--------------------
View
0  tests/modeltests/proxy_models/__init__.py
No changes.
View
176 tests/modeltests/proxy_models/models.py
@@ -0,0 +1,176 @@
+"""
+By specifying the 'proxy' Meta attribute, model subclasses can specify that
+they will take data directly from the table of their base class table rather
+than using a new table of their own. This allows them to act as simple proxies,
+providing a modified interface to the data from the base class.
+"""
+
+from django.db import models
+
+
+# A couple of managers for testing managing overriding in proxy model cases.
+
+class PersonManager(models.Manager):
+ def get_query_set(self):
+ return super(PersonManager, self).get_query_set().exclude(name="fred")
+
+class SubManager(models.Manager):
+ def get_query_set(self):
+ return super(SubManager, self).get_query_set().exclude(name="wilma")
+
+class Person(models.Model):
+ """
+ A simple concrete base class.
+ """
+ name = models.CharField(max_length=50)
+
+ objects = PersonManager()
+
+ def __unicode__(self):
+ return self.name
+
+class Abstract(models.Model):
+ """
+ A simple abstract base class, to be used for error checking.
+ """
+ data = models.CharField(max_length=10)
+
+ class Meta:
+ abstract = True
+
+class MyPerson(Person):
+ """
+ A proxy subclass, this should not get a new table. Overrides the default
+ manager.
+ """
+ class Meta:
+ proxy = True
+ ordering = ["name"]
+
+ objects = SubManager()
+ other = PersonManager()
+
+ def has_special_name(self):
+ return self.name.lower() == "special"
+
+class ManagerMixin(models.Model):
+ excluder = SubManager()
+
+ class Meta:
+ abstract = True
+
+class OtherPerson(Person, ManagerMixin):
+ """
+ A class with the default manager from Person, plus an secondary manager.
+ """
+ class Meta:
+ proxy = True
+ ordering = ["name"]
+
+class StatusPerson(MyPerson):
+ """
+ A non-proxy subclass of a proxy, it should get a new table.
+ """
+ status = models.CharField(max_length=80)
+
+# We can even have proxies of proxies (and subclass of those).
+class MyPersonProxy(MyPerson):
+ class Meta:
+ proxy = True
+
+class LowerStatusPerson(MyPersonProxy):
+ status = models.CharField(max_length=80)
+
+__test__ = {'API_TESTS' : """
+# The MyPerson model should be generating the same database queries as the
+# Person model (when the same manager is used in each case).
+>>> MyPerson.other.all().query.as_sql() == Person.objects.order_by("name").query.as_sql()
+True
+
+# The StatusPerson models should have its own table (it's using ORM-level
+# inheritance).
+>>> StatusPerson.objects.all().query.as_sql() == Person.objects.all().query.as_sql()
+False
+
+# Creating a Person makes them accessible through the MyPerson proxy.
+>>> _ = Person.objects.create(name="Foo McBar")
+>>> len(Person.objects.all())
+1
+>>> len(MyPerson.objects.all())
+1
+>>> MyPerson.objects.get(name="Foo McBar").id
+1
+>>> MyPerson.objects.get(id=1).has_special_name()
+False
+
+# Person is not proxied by StatusPerson subclass, however.
+>>> StatusPerson.objects.all()
+[]
+
+# A new MyPerson also shows up as a standard Person
+>>> _ = MyPerson.objects.create(name="Bazza del Frob")
+>>> len(MyPerson.objects.all())
+2
+>>> len(Person.objects.all())
+2
+
+>>> _ = LowerStatusPerson.objects.create(status="low", name="homer")
+>>> LowerStatusPerson.objects.all()
+[<LowerStatusPerson: homer>]
+
+# And now for some things that shouldn't work...
+#
+# All base classes must be non-abstract
+>>> class NoAbstract(Abstract):
+... class Meta:
+... proxy = True
+Traceback (most recent call last):
+ ....
+TypeError: Abstract base class containing model fields not permitted for proxy model 'NoAbstract'.
+
+# The proxy must actually have one concrete base class
+>>> class TooManyBases(Person, Abstract):
+... class Meta:
+... proxy = True
+Traceback (most recent call last):
+ ....
+TypeError: Abstract base class containing model fields not permitted for proxy model 'TooManyBases'.
+
+>>> class NoBaseClasses(models.Model):
+... class Meta:
+... proxy = True
+Traceback (most recent call last):
+ ....
+TypeError: Proxy model 'NoBaseClasses' has no non-abstract model base class.
+
+
+# A proxy cannot introduce any new fields
+>>> class NoNewFields(Person):
+... newfield = models.BooleanField()
+... class Meta:
+... proxy = True
+Traceback (most recent call last):
+ ....
+FieldError: Proxy model 'NoNewFields' contains model fields.
+
+# Manager tests.
+
+>>> Person.objects.all().delete()
+>>> _ = Person.objects.create(name="fred")
+>>> _ = Person.objects.create(name="wilma")
+>>> _ = Person.objects.create(name="barney")
+
+>>> MyPerson.objects.all()
+[<MyPerson: barney>, <MyPerson: fred>]
+>>> MyPerson._default_manager.all()
+[<MyPerson: barney>, <MyPerson: fred>]
+
+>>> OtherPerson.objects.all()
+[<OtherPerson: barney>, <OtherPerson: wilma>]
+>>> OtherPerson.excluder.all()
+[<OtherPerson: barney>, <OtherPerson: fred>]
+>>> OtherPerson._default_manager.all()
+[<OtherPerson: barney>, <OtherPerson: wilma>]
+"""}
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.