From e5880516f90773d653a67e95b9b942229e8e9021 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 17 Mar 2017 11:25:12 -0400 Subject: [PATCH] [1.11.x] Fixed #27915 -- Allowed Meta.indexes to be defined in abstract models. Thanks Markus Holtermann for review. Backport of 3d19d1428a05b514afb771b52870d1f7c25670d1 from master --- django/db/migrations/state.py | 5 +++++ django/db/models/base.py | 14 ++++++++------ django/db/models/indexes.py | 5 +++++ tests/migrations/test_state.py | 27 +++++++++++++++++++++++++++ tests/model_indexes/models.py | 16 ++++++++++++++++ tests/model_indexes/tests.py | 14 +++++++++++++- 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index e44f737106e..b6540f5f101 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -460,6 +460,11 @@ class ModelState(object): elif name == "index_together": it = model._meta.original_attrs["index_together"] options[name] = set(normalize_together(it)) + elif name == "indexes": + indexes = [idx.clone() for idx in model._meta.indexes] + for index in indexes: + index.set_name_with_model(model) + options['indexes'] = indexes else: options[name] = model._meta.original_attrs[name] # Force-convert all options to text_type (#23226) diff --git a/django/db/models/base.py b/django/db/models/base.py index 53b761f45ab..217bde1e2a9 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -303,12 +303,14 @@ class ModelBase(type): else: new_class.add_to_class(field.name, copy.deepcopy(field)) - # Set the name of _meta.indexes. This can't be done in - # Options.contribute_to_class() because fields haven't been added to - # the model at that point. - for index in new_class._meta.indexes: - if not index.name: - index.set_name_with_model(new_class) + if base_meta and base_meta.abstract and not abstract: + new_class._meta.indexes = [copy.deepcopy(idx) for idx in new_class._meta.indexes] + # Set the name of _meta.indexes. This can't be done in + # Options.contribute_to_class() because fields haven't been added + # to the model at that point. + for index in new_class._meta.indexes: + if not index.name: + index.set_name_with_model(new_class) if abstract: # Abstract base models can't be instantiated and don't appear in diff --git a/django/db/models/indexes.py b/django/db/models/indexes.py index 1ef5a4ab50d..0ee2bf3610c 100644 --- a/django/db/models/indexes.py +++ b/django/db/models/indexes.py @@ -77,6 +77,11 @@ class Index(object): path = path.replace('django.db.models.indexes', 'django.db.models') return (path, (), {'fields': self.fields, 'name': self.name}) + def clone(self): + """Create a copy of this Index.""" + path, args, kwargs = self.deconstruct() + return self.__class__(*args, **kwargs) + @staticmethod def _hash_generator(*args): """ diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index c8eb1726baf..68c3b983916 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -1044,6 +1044,33 @@ class ModelStateTests(SimpleTestCase): state = ModelState.from_model(PrivateFieldModel) self.assertNotIn('order_with_respect_to', state.options) + @isolate_apps('migrations') + def test_abstract_model_children_inherit_indexes(self): + class Abstract(models.Model): + name = models.CharField(max_length=50) + + class Meta: + app_label = 'migrations' + abstract = True + indexes = [models.indexes.Index(fields=['name'])] + + class Child1(Abstract): + pass + + class Child2(Abstract): + pass + + child1_state = ModelState.from_model(Child1) + child2_state = ModelState.from_model(Child2) + index_names = [index.name for index in child1_state.options['indexes']] + self.assertEqual(index_names, ['migrations__name_b0afd7_idx']) + index_names = [index.name for index in child2_state.options['indexes']] + self.assertEqual(index_names, ['migrations__name_016466_idx']) + + # Modifying the state doesn't modify the index on the model. + child1_state.options['indexes'][0].name = 'bar' + self.assertEqual(Child1._meta.indexes[0].name, 'migrations__name_b0afd7_idx') + class RelatedModelsTests(SimpleTestCase): diff --git a/tests/model_indexes/models.py b/tests/model_indexes/models.py index 598a4cb8089..34b3f3246cf 100644 --- a/tests/model_indexes/models.py +++ b/tests/model_indexes/models.py @@ -5,3 +5,19 @@ class Book(models.Model): title = models.CharField(max_length=50) author = models.CharField(max_length=50) pages = models.IntegerField(db_column='page_count') + + +class AbstractModel(models.Model): + name = models.CharField(max_length=50) + + class Meta: + abstract = True + indexes = [models.indexes.Index(fields=['name'])] + + +class ChildModel1(AbstractModel): + pass + + +class ChildModel2(AbstractModel): + pass diff --git a/tests/model_indexes/tests.py b/tests/model_indexes/tests.py index 33e4bfaa7c0..791233daf09 100644 --- a/tests/model_indexes/tests.py +++ b/tests/model_indexes/tests.py @@ -1,7 +1,7 @@ from django.db import models from django.test import SimpleTestCase -from .models import Book +from .models import Book, ChildModel1, ChildModel2 class IndexesTests(SimpleTestCase): @@ -76,3 +76,15 @@ class IndexesTests(SimpleTestCase): self.assertEqual(path, 'django.db.models.Index') self.assertEqual(args, ()) self.assertEqual(kwargs, {'fields': ['title'], 'name': 'model_index_title_196f42_idx'}) + + def test_clone(self): + index = models.Index(fields=['title']) + new_index = index.clone() + self.assertIsNot(index, new_index) + self.assertEqual(index.fields, new_index.fields) + + def test_abstract_children(self): + index_names = [index.name for index in ChildModel1._meta.indexes] + self.assertEqual(index_names, ['model_index_name_440998_idx']) + index_names = [index.name for index in ChildModel2._meta.indexes] + self.assertEqual(index_names, ['model_index_name_b6c374_idx'])