Hello!

I'd like to propose another inheritance strategy for django's models.

Think of it sort of like reversed abstract models

For example:

class NormalModel(models.Model):
    foo = models.CharField(max_length=10)
    bar = models.CharField(max_length=10)

class CopiedBaseModel(NormalModel):
    bar = models.CharField(max_length=2)
    buzz = models.CharField(max_length=10)

    class Meta:
        copy_from_base = True

Would be equivalent to:

class NormalModel(models.Model):
    foo = models.CharField(max_length=10)
    bar = models.CharField(max_length=10)

class CopiedBaseModel(NormalModel):
    foo = models.CharField(max_length=10)
    bar = models.CharField(max_length=10)
    buzz = models.CharField(max_length=10)

My initial use case for this was with django-cms which didn't play well with
multi-table inheritance when i needed to extend a built in plugin of 
theirs. So
I ended copying the whole model instead, which didn't make me too happy.
So I started writing some code for the behaviour I was after. Which was,
ironicly a standard python inheritance. Django only offers part of this 
behaviour
through the proxy and abstract models. But neither of them worked for me.

This is quite easy to implement with some of the current abstract model
logic (see the patch).

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers  (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/django-developers.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-developers/d4872520-eeb1-49a7-a8f3-aefc23a98346%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 8cc9599..7c0f34a 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -188,20 +188,24 @@ class ModelBase(type):
         else:
             new_class._meta.concrete_model = new_class
 
-        # Collect the parent links for multi-table inheritance.
-        parent_links = {}
-        for base in reversed([new_class] + parents):
-            # Conceptually equivalent to `if base is Model`.
-            if not hasattr(base, '_meta'):
-                continue
-            # Skip concrete parent classes.
-            if base != new_class and not base._meta.abstract:
-                continue
-            # Locate OneToOneField instances.
-            for field in base._meta.local_fields:
-                if isinstance(field, OneToOneField):
-                    related = resolve_relation(new_class, field.remote_field.model)
-                    parent_links[make_model_tuple(related)] = field
+        should_copy_from_base = getattr(new_class._meta, 'copy_from_base', False)
+
+        if not should_copy_from_base:
+            # Collect the parent links for multi-table inheritance.
+            parent_links = {}
+            for base in reversed([new_class] + parents):
+                # Conceptually equivalent to `if base is Model`.
+                if not hasattr(base, '_meta'):
+                    continue
+                # Skip concrete parent classes.
+                if base != new_class and not base._meta.abstract:
+                    continue
+                # Locate OneToOneField instances.
+                for field in base._meta.local_fields:
+                    if isinstance(field, OneToOneField):
+                        related = resolve_relation(new_class, field.remote_field.model)
+                        parent_links[make_model_tuple(related)] = field
+
         # Do the appropriate setup for any model parents.
         for base in parents:
             original_base = base
@@ -211,17 +215,19 @@ class ModelBase(type):
                 continue
 
             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
-            # moment).
-            for field in parent_fields:
-                if field.name in field_names:
-                    raise FieldError(
-                        'Local field %r in class %r clashes '
-                        'with field of similar name from '
-                        'base class %r' % (field.name, name, base.__name__)
-                    )
-            if not base._meta.abstract:
+            if not should_copy_from_base:
+                # Check for clashes between locally declared fields and those
+                # on the base classes (we cannot handle shadowed fields at the
+                # moment).
+                for field in parent_fields:
+                    if field.name in field_names:
+                        raise FieldError(
+                            'Local field %r in class %r clashes '
+                            'with field of similar name from '
+                            'base class %r' % (field.name, name, base.__name__)
+                        )
+
+            if not base._meta.abstract and not should_copy_from_base:
                 # Concrete classes...
                 base = base._meta.concrete_model
                 base_key = make_model_tuple(base)
@@ -246,11 +252,18 @@ class ModelBase(type):
             else:
                 # .. and abstract ones.
                 for field in parent_fields:
+                    if should_copy_from_base:
+                        if field is base._meta.auto_field:
+                            continue
+                        if field.name in field_names:
+                            continue
+
                     new_field = copy.deepcopy(field)
                     new_class.add_to_class(field.name, new_field)
 
-                # Pass any non-abstract parent classes onto child.
-                new_class._meta.parents.update(base._meta.parents)
+                if not should_copy_from_base:
+                    # Pass any non-abstract parent classes onto child.
+                    new_class._meta.parents.update(base._meta.parents)
 
             # Inherit managers from the abstract base classes.
             new_class.copy_managers(base._meta.abstract_managers)
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 4fdcc02..8729c2d 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -34,7 +34,8 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                  'abstract', 'managed', 'proxy', 'swappable', 'auto_created',
                  'index_together', 'apps', 'default_permissions',
                  'select_on_save', 'default_related_name',
-                 'required_db_features', 'required_db_vendor')
+                 'required_db_features', 'required_db_vendor',
+                 'copy_from_base')
 
 
 def normalize_together(option_together):
diff --git a/tests/copy_from_base_inheritance/__init__.py b/tests/copy_from_base_inheritance/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/copy_from_base_inheritance/models.py b/tests/copy_from_base_inheritance/models.py
new file mode 100644
index 0000000..e65c6ab
--- /dev/null
+++ b/tests/copy_from_base_inheritance/models.py
@@ -0,0 +1,18 @@
+from django.db import models
+
+
+class NormalModel(models.Model):
+    foo = models.CharField(max_length=10)
+    bar = models.CharField(max_length=10)
+
+
+class MultiTableInheritanceModel(NormalModel):
+    buzz = models.CharField(max_length=10)
+
+
+class CopiedBaseModel(NormalModel):
+    bar = models.CharField(max_length=2)
+    buzz = models.CharField(max_length=10)
+
+    class Meta:
+        copy_from_base = True
diff --git a/tests/copy_from_base_inheritance/tests.py b/tests/copy_from_base_inheritance/tests.py
new file mode 100644
index 0000000..7f4e6d5
--- /dev/null
+++ b/tests/copy_from_base_inheritance/tests.py
@@ -0,0 +1,42 @@
+from django.test import TestCase
+from .models import CopiedBaseModel, MultiTableInheritanceModel
+
+
+class CopyFromBaseInheritance(TestCase):
+    def setUp(self):
+        self.copied_base_model = CopiedBaseModel.objects.create(foo='foo',
+                                                                bar='ba',
+                                                                buzz='buzz')
+
+    def test_copy_from_base_no_one_to_one_field(self):
+        self.assertFalse(hasattr(self.copied_base_model, 'normalmodel_ptr'))
+
+
+    def test_copy_from_base_can_access_inherited(self):
+        copied_base_model = CopiedBaseModel.objects.get()
+        self.assertEqual(copied_base_model.foo, 'foo')
+        self.assertEqual(copied_base_model.bar, 'ba')
+        self.assertEqual(copied_base_model.buzz, 'buzz')
+
+    def test_shadow(self):
+        for field in CopiedBaseModel._meta.fields:
+            if field.name == 'bar':
+                self.assertEqual(field.max_length, 2)
+
+
+class MultiTableInheritance(TestCase):
+    def setUp(self):
+        self.multi_table = MultiTableInheritanceModel.objects.create(
+            foo='foo2',
+            bar='bar2',
+            buzz='buzz2'
+        )
+
+    def test_copy_from_base_has_one_to_one_field(self):
+        self.assertTrue(hasattr(self.multi_table, 'normalmodel_ptr'))
+
+    def test_multi_table_can_access_inherited(self):
+        multi_table = MultiTableInheritanceModel.objects.get()
+        self.assertEqual(multi_table.foo, 'foo2')
+        self.assertEqual(multi_table.bar, 'bar2')
+        self.assertEqual(multi_table.buzz, 'buzz2')

Reply via email to