Hello!

Wouldn't it be great to be able to inherit django models like any other 
python class? I.e not like the default multi-table inheritance.

"How would this differ from an abstract model" you may ask. Well, it is 
sort of like an abstract model inheritance, except for the abstract part 
where the base model isn't registered
and need to have abstract = True

*For example*

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

class CopiedBaseModel(NormalModel):
    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 original use case for this was with django-cms, which didn't play well 
with the multi-table inheritance when I needed to extend a built in plugin 
of theirs. So I ended up copying the whole model.
which annoyed me that it was the only way to do it.

This also isolates the models from each other in constrast to multi-table 
inheritance, as the base class is just copied over to the new class, it 
shares this functionality with abstract models. Think of it as a kind of 
reverse abstract model. I wrote some code for this and actually reused much 
of the code used for abstract models. (see attatched 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/62b0b093-c7e5-4abb-a797-63277b31d27c%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