Fixed #30397 -- Added app_label/class interpolation for names of indexes and constraints.

This commit is contained in:
can 2019-07-05 15:15:41 +03:00 committed by Mariusz Felisiak
parent 8233144ca0
commit febe136d4c
9 changed files with 166 additions and 11 deletions

View File

@ -180,6 +180,12 @@ class Options:
self.unique_together = normalize_together(self.unique_together) self.unique_together = normalize_together(self.unique_together)
self.index_together = normalize_together(self.index_together) self.index_together = normalize_together(self.index_together)
# App label/class name interpolation for names of constraints and
# indexes.
if not getattr(cls._meta, 'abstract', False):
for attr_name in {'constraints', 'indexes'}:
objs = getattr(self, attr_name, [])
setattr(self, attr_name, self._format_names_with_class(cls, objs))
# verbose_name_plural is a special case because it uses a 's' # verbose_name_plural is a special case because it uses a 's'
# by default. # by default.
@ -201,6 +207,18 @@ class Options:
self.db_table = "%s_%s" % (self.app_label, self.model_name) self.db_table = "%s_%s" % (self.app_label, self.model_name)
self.db_table = truncate_name(self.db_table, connection.ops.max_name_length()) self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
def _format_names_with_class(self, cls, objs):
"""App label/class name interpolation for object names."""
new_objs = []
for obj in objs:
obj = obj.clone()
obj.name = obj.name % {
'app_label': cls._meta.app_label.lower(),
'class': cls.__name__.lower(),
}
new_objs.append(obj)
return new_objs
def _prepare(self, model): def _prepare(self, model):
if self.order_with_respect_to: if self.order_with_respect_to:
# The app registry will not be ready at this point, so we cannot # The app registry will not be ready at this point, so we cannot

View File

@ -25,8 +25,11 @@ option.
cannot normally specify a constraint on an abstract base class, since the cannot normally specify a constraint on an abstract base class, since the
:attr:`Meta.constraints <django.db.models.Options.constraints>` option is :attr:`Meta.constraints <django.db.models.Options.constraints>` option is
inherited by subclasses, with exactly the same values for the attributes inherited by subclasses, with exactly the same values for the attributes
(including ``name``) each time. Instead, specify the ``constraints`` option (including ``name``) each time. To work around name collisions, part of the
on subclasses directly, providing a unique name for each constraint. name may contain ``'%(app_label)s'`` and ``'%(class)s'``, which are
replaced, respectively, by the lowercased app label and class name of the
concrete model. For example ``CheckConstraint(check=Q(age__gte=18),
name='%(app_label)s_%(class)s_is_adult')``.
.. admonition:: Validation of Constraints .. admonition:: Validation of Constraints
@ -63,6 +66,10 @@ ensures the age field is never less than 18.
The name of the constraint. The name of the constraint.
.. versionchanged:: 3.0
Interpolation of ``'%(app_label)s'`` and ``'%(class)s'`` was added.
``UniqueConstraint`` ``UniqueConstraint``
==================== ====================
@ -89,6 +96,10 @@ date.
The name of the constraint. The name of the constraint.
.. versionchanged:: 3.0
Interpolation of ``'%(app_label)s'`` and ``'%(class)s'`` was added.
``condition`` ``condition``
------------- -------------

View File

@ -55,9 +55,15 @@ than 30 characters and shouldn't start with a number (0-9) or underscore (_).
cannot normally specify a partial index on an abstract base class, since cannot normally specify a partial index on an abstract base class, since
the :attr:`Meta.indexes <django.db.models.Options.indexes>` option is the :attr:`Meta.indexes <django.db.models.Options.indexes>` option is
inherited by subclasses, with exactly the same values for the attributes inherited by subclasses, with exactly the same values for the attributes
(including ``name``) each time. Instead, specify the ``indexes`` option (including ``name``) each time. To work around name collisions, part of the
on subclasses directly, providing a unique name for each index. name may contain ``'%(app_label)s'`` and ``'%(class)s'``, which are
replaced, respectively, by the lowercased app label and class name of the
concrete model. For example ``Index(fields=['title'],
name='%(app_label)s_%(class)s_title_index')``.
.. versionchanged:: 3.0
Interpolation of ``'%(app_label)s'`` and ``'%(class)s'`` was added.
``db_tablespace`` ``db_tablespace``
----------------- -----------------

View File

@ -263,6 +263,11 @@ Models
* Allowed symmetrical intermediate table for self-referential * Allowed symmetrical intermediate table for self-referential
:class:`~django.db.models.ManyToManyField`. :class:`~django.db.models.ManyToManyField`.
* The ``name`` attributes of :class:`~django.db.models.CheckConstraint`,
:class:`~django.db.models.UniqueConstraint`, and
:class:`~django.db.models.Index` now support app label and class
interpolation using the ``'%(app_label)s'`` and ``'%(class)s'`` placeholders.
Requests and Responses Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

View File

@ -131,6 +131,22 @@ class IndexNameTests(SimpleTestCase):
), ),
]) ])
def test_no_collision_abstract_model_interpolation(self):
class AbstractModel(models.Model):
name = models.CharField(max_length=20)
class Meta:
indexes = [models.Index(fields=['name'], name='%(app_label)s_%(class)s_foo')]
abstract = True
class Model1(AbstractModel):
pass
class Model2(AbstractModel):
pass
self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
@modify_settings(INSTALLED_APPS={'append': 'basic'}) @modify_settings(INSTALLED_APPS={'append': 'basic'})
@isolate_apps('basic', 'check_framework', kwarg_name='apps') @isolate_apps('basic', 'check_framework', kwarg_name='apps')
def test_collision_across_apps(self, apps): def test_collision_across_apps(self, apps):
@ -154,6 +170,23 @@ class IndexNameTests(SimpleTestCase):
), ),
]) ])
@modify_settings(INSTALLED_APPS={'append': 'basic'})
@isolate_apps('basic', 'check_framework', kwarg_name='apps')
def test_no_collision_across_apps_interpolation(self, apps):
index = models.Index(fields=['id'], name='%(app_label)s_%(class)s_foo')
class Model1(models.Model):
class Meta:
app_label = 'basic'
constraints = [index]
class Model2(models.Model):
class Meta:
app_label = 'check_framework'
constraints = [index]
self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), [])
@isolate_apps('check_framework', attr_name='apps') @isolate_apps('check_framework', attr_name='apps')
@override_system_checks([checks.model_checks.check_all_models]) @override_system_checks([checks.model_checks.check_all_models])
@ -214,6 +247,22 @@ class ConstraintNameTests(TestCase):
), ),
]) ])
def test_no_collision_abstract_model_interpolation(self):
class AbstractModel(models.Model):
class Meta:
constraints = [
models.CheckConstraint(check=models.Q(id__gt=0), name='%(app_label)s_%(class)s_foo'),
]
abstract = True
class Model1(AbstractModel):
pass
class Model2(AbstractModel):
pass
self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
@modify_settings(INSTALLED_APPS={'append': 'basic'}) @modify_settings(INSTALLED_APPS={'append': 'basic'})
@isolate_apps('basic', 'check_framework', kwarg_name='apps') @isolate_apps('basic', 'check_framework', kwarg_name='apps')
def test_collision_across_apps(self, apps): def test_collision_across_apps(self, apps):
@ -236,3 +285,20 @@ class ConstraintNameTests(TestCase):
id='models.E032', id='models.E032',
), ),
]) ])
@modify_settings(INSTALLED_APPS={'append': 'basic'})
@isolate_apps('basic', 'check_framework', kwarg_name='apps')
def test_no_collision_across_apps_interpolation(self, apps):
constraint = models.CheckConstraint(check=models.Q(id__gt=0), name='%(app_label)s_%(class)s_foo')
class Model1(models.Model):
class Meta:
app_label = 'basic'
constraints = [constraint]
class Model2(models.Model):
class Meta:
app_label = 'check_framework'
constraints = [constraint]
self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), [])

View File

@ -13,6 +13,10 @@ class Product(models.Model):
check=models.Q(price__gt=models.F('discounted_price')), check=models.Q(price__gt=models.F('discounted_price')),
name='price_gt_discounted_price', name='price_gt_discounted_price',
), ),
models.CheckConstraint(
check=models.Q(price__gt=0),
name='%(app_label)s_%(class)s_price_gt_0',
),
models.UniqueConstraint(fields=['name', 'color'], name='name_color_uniq'), models.UniqueConstraint(fields=['name', 'color'], name='name_color_uniq'),
models.UniqueConstraint( models.UniqueConstraint(
fields=['name'], fields=['name'],
@ -20,3 +24,20 @@ class Product(models.Model):
condition=models.Q(color__isnull=True), condition=models.Q(color__isnull=True),
), ),
] ]
class AbstractModel(models.Model):
age = models.IntegerField()
class Meta:
abstract = True
constraints = [
models.CheckConstraint(
check=models.Q(age__gte=18),
name='%(app_label)s_%(class)s_adult',
),
]
class ChildModel(AbstractModel):
pass

View File

@ -3,7 +3,7 @@ from django.db import IntegrityError, connection, models
from django.db.models.constraints import BaseConstraint from django.db.models.constraints import BaseConstraint
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from .models import Product from .models import ChildModel, Product
def get_constraints(table): def get_constraints(table):
@ -76,8 +76,17 @@ class CheckConstraintTests(TestCase):
@skipUnlessDBFeature('supports_table_check_constraints') @skipUnlessDBFeature('supports_table_check_constraints')
def test_name(self): def test_name(self):
constraints = get_constraints(Product._meta.db_table) constraints = get_constraints(Product._meta.db_table)
expected_name = 'price_gt_discounted_price' for expected_name in (
self.assertIn(expected_name, constraints) 'price_gt_discounted_price',
'constraints_product_price_gt_0',
):
with self.subTest(expected_name):
self.assertIn(expected_name, constraints)
@skipUnlessDBFeature('supports_table_check_constraints')
def test_abstract_name(self):
constraints = get_constraints(ChildModel._meta.db_table)
self.assertIn('constraints_childmodel_adult', constraints)
class UniqueConstraintTests(TestCase): class UniqueConstraintTests(TestCase):

View File

@ -7,20 +7,26 @@ class Book(models.Model):
pages = models.IntegerField(db_column='page_count') pages = models.IntegerField(db_column='page_count')
shortcut = models.CharField(max_length=50, db_tablespace='idx_tbls') shortcut = models.CharField(max_length=50, db_tablespace='idx_tbls')
isbn = models.CharField(max_length=50, db_tablespace='idx_tbls') isbn = models.CharField(max_length=50, db_tablespace='idx_tbls')
barcode = models.CharField(max_length=31)
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['title']), models.Index(fields=['title']),
models.Index(fields=['isbn', 'id']), models.Index(fields=['isbn', 'id']),
models.Index(fields=['barcode'], name='%(app_label)s_%(class)s_barcode_idx'),
] ]
class AbstractModel(models.Model): class AbstractModel(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
shortcut = models.CharField(max_length=3)
class Meta: class Meta:
abstract = True abstract = True
indexes = [models.Index(fields=['name'])] indexes = [
models.Index(fields=['name']),
models.Index(fields=['shortcut'], name='%(app_label)s_%(class)s_idx'),
]
class ChildModel1(AbstractModel): class ChildModel1(AbstractModel):

View File

@ -134,13 +134,26 @@ class SimpleIndexesTests(SimpleTestCase):
def test_name_set(self): def test_name_set(self):
index_names = [index.name for index in Book._meta.indexes] index_names = [index.name for index in Book._meta.indexes]
self.assertCountEqual(index_names, ['model_index_title_196f42_idx', 'model_index_isbn_34f975_idx']) self.assertCountEqual(
index_names,
[
'model_index_title_196f42_idx',
'model_index_isbn_34f975_idx',
'model_indexes_book_barcode_idx',
],
)
def test_abstract_children(self): def test_abstract_children(self):
index_names = [index.name for index in ChildModel1._meta.indexes] index_names = [index.name for index in ChildModel1._meta.indexes]
self.assertEqual(index_names, ['model_index_name_440998_idx']) self.assertEqual(
index_names,
['model_index_name_440998_idx', 'model_indexes_childmodel1_idx'],
)
index_names = [index.name for index in ChildModel2._meta.indexes] index_names = [index.name for index in ChildModel2._meta.indexes]
self.assertEqual(index_names, ['model_index_name_b6c374_idx']) self.assertEqual(
index_names,
['model_index_name_b6c374_idx', 'model_indexes_childmodel2_idx'],
)
class IndexesTests(TestCase): class IndexesTests(TestCase):