Commit 61a2708c authored by Malcolm Tredinnick's avatar Malcolm Tredinnick
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
parent c0b6e23e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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>
+109 −61
Original line number Diff line number Diff line
@@ -67,9 +67,19 @@ class ModelBase(type):
                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):
            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 @@ class ModelBase(type):
        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 @@ class ModelBase(type):
                                        (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 @@ class ModelBase(type):
                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 @@ class ModelBase(type):
        # 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,12 +402,16 @@ class Model(object):
                # 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)
                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.
+3 −0
Original line number Diff line number Diff line
@@ -60,6 +60,9 @@ class Manager(object):
        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):
        """
+16 −3
Original line number Diff line number Diff line
@@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
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 @@ class Options(object):
        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 @@ class Options(object):
            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

+32 −19
Original line number Diff line number Diff line
@@ -641,6 +641,7 @@ class BaseQuery(object):
        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,6 +649,9 @@ class BaseQuery(object):
                try:
                    alias = seen[model]
                except KeyError:
                    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))
@@ -1158,8 +1162,12 @@ class BaseQuery(object):
        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:
                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))
@@ -1559,7 +1567,11 @@ class BaseQuery(object):
                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):
                    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:
@@ -1572,7 +1584,8 @@ class BaseQuery(object):
                        joins.append(alias)
                        exclusions.add(alias)
                        for (dupe_opts, dupe_col) in dupe_set:
                        self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
                            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
Loading