Skip to content

Commit 3d19d14

Browse files
committed
Fixed #27915 -- Allowed Meta.indexes to be defined in abstract models.
Thanks Markus Holtermann for review.
1 parent 4224ecb commit 3d19d14

File tree

6 files changed

+74
-7
lines changed

6 files changed

+74
-7
lines changed

django/db/migrations/state.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,11 @@ def from_model(cls, model, exclude_rels=False):
443443
elif name == "index_together":
444444
it = model._meta.original_attrs["index_together"]
445445
options[name] = set(normalize_together(it))
446+
elif name == "indexes":
447+
indexes = [idx.clone() for idx in model._meta.indexes]
448+
for index in indexes:
449+
index.set_name_with_model(model)
450+
options['indexes'] = indexes
446451
else:
447452
options[name] = model._meta.original_attrs[name]
448453
# If we're ignoring relationships, remove all field-listing model

django/db/models/base.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,14 @@ def __new__(cls, name, bases, attrs):
293293
else:
294294
new_class.add_to_class(field.name, copy.deepcopy(field))
295295

296-
# Set the name of _meta.indexes. This can't be done in
297-
# Options.contribute_to_class() because fields haven't been added to
298-
# the model at that point.
299-
for index in new_class._meta.indexes:
300-
if not index.name:
301-
index.set_name_with_model(new_class)
296+
if base_meta and base_meta.abstract and not abstract:
297+
new_class._meta.indexes = [copy.deepcopy(idx) for idx in new_class._meta.indexes]
298+
# Set the name of _meta.indexes. This can't be done in
299+
# Options.contribute_to_class() because fields haven't been added
300+
# to the model at that point.
301+
for index in new_class._meta.indexes:
302+
if not index.name:
303+
index.set_name_with_model(new_class)
302304

303305
if abstract:
304306
# Abstract base models can't be instantiated and don't appear in

django/db/models/indexes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ def deconstruct(self):
7575
path = path.replace('django.db.models.indexes', 'django.db.models')
7676
return (path, (), {'fields': self.fields, 'name': self.name})
7777

78+
def clone(self):
79+
"""Create a copy of this Index."""
80+
path, args, kwargs = self.deconstruct()
81+
return self.__class__(*args, **kwargs)
82+
7883
@staticmethod
7984
def _hash_generator(*args):
8085
"""

tests/migrations/test_state.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,33 @@ class Meta:
10431043
state = ModelState.from_model(PrivateFieldModel)
10441044
self.assertNotIn('order_with_respect_to', state.options)
10451045

1046+
@isolate_apps('migrations')
1047+
def test_abstract_model_children_inherit_indexes(self):
1048+
class Abstract(models.Model):
1049+
name = models.CharField(max_length=50)
1050+
1051+
class Meta:
1052+
app_label = 'migrations'
1053+
abstract = True
1054+
indexes = [models.indexes.Index(fields=['name'])]
1055+
1056+
class Child1(Abstract):
1057+
pass
1058+
1059+
class Child2(Abstract):
1060+
pass
1061+
1062+
child1_state = ModelState.from_model(Child1)
1063+
child2_state = ModelState.from_model(Child2)
1064+
index_names = [index.name for index in child1_state.options['indexes']]
1065+
self.assertEqual(index_names, ['migrations__name_b0afd7_idx'])
1066+
index_names = [index.name for index in child2_state.options['indexes']]
1067+
self.assertEqual(index_names, ['migrations__name_016466_idx'])
1068+
1069+
# Modifying the state doesn't modify the index on the model.
1070+
child1_state.options['indexes'][0].name = 'bar'
1071+
self.assertEqual(Child1._meta.indexes[0].name, 'migrations__name_b0afd7_idx')
1072+
10461073

10471074
class RelatedModelsTests(SimpleTestCase):
10481075

tests/model_indexes/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,19 @@ class Book(models.Model):
55
title = models.CharField(max_length=50)
66
author = models.CharField(max_length=50)
77
pages = models.IntegerField(db_column='page_count')
8+
9+
10+
class AbstractModel(models.Model):
11+
name = models.CharField(max_length=50)
12+
13+
class Meta:
14+
abstract = True
15+
indexes = [models.indexes.Index(fields=['name'])]
16+
17+
18+
class ChildModel1(AbstractModel):
19+
pass
20+
21+
22+
class ChildModel2(AbstractModel):
23+
pass

tests/model_indexes/tests.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.db import models
22
from django.test import SimpleTestCase
33

4-
from .models import Book
4+
from .models import Book, ChildModel1, ChildModel2
55

66

77
class IndexesTests(SimpleTestCase):
@@ -76,3 +76,15 @@ def test_deconstruction(self):
7676
self.assertEqual(path, 'django.db.models.Index')
7777
self.assertEqual(args, ())
7878
self.assertEqual(kwargs, {'fields': ['title'], 'name': 'model_index_title_196f42_idx'})
79+
80+
def test_clone(self):
81+
index = models.Index(fields=['title'])
82+
new_index = index.clone()
83+
self.assertIsNot(index, new_index)
84+
self.assertEqual(index.fields, new_index.fields)
85+
86+
def test_abstract_children(self):
87+
index_names = [index.name for index in ChildModel1._meta.indexes]
88+
self.assertEqual(index_names, ['model_index_name_440998_idx'])
89+
index_names = [index.name for index in ChildModel2._meta.indexes]
90+
self.assertEqual(index_names, ['model_index_name_b6c374_idx'])

0 commit comments

Comments
 (0)