Fixed #22172 -- Allowed index_together to be a single list (rather than list of lists)..

Thanks EmilStenstrom for the suggestion.
This commit is contained in:
Anubhav Joshi 2014-03-02 00:36:15 +05:30 committed by Tim Graham
parent 3273bd7b25
commit bb2ca9fe6c
10 changed files with 67 additions and 19 deletions

View File

@ -330,6 +330,7 @@ answer newbie questions, and generally made Django that much better:
Zak Johnson <zakj@nox.cx> Zak Johnson <zakj@nox.cx>
Nis Jørgensen <nis@superlativ.dk> Nis Jørgensen <nis@superlativ.dk>
Michael Josephson <http://www.sdjournal.com/> Michael Josephson <http://www.sdjournal.com/>
Anubhav Joshi <anubhav9042@gmail.com>
jpellerin@gmail.com jpellerin@gmail.com
junzhang.jn@gmail.com junzhang.jn@gmail.com
Krzysztof Jurewicz <krzysztof.jurewicz@gmail.com> Krzysztof Jurewicz <krzysztof.jurewicz@gmail.com>

View File

@ -1,5 +1,5 @@
from django.db import models, router from django.db import models, router
from django.db.models.options import normalize_unique_together from django.db.models.options import normalize_together
from django.db.migrations.state import ModelState from django.db.migrations.state import ModelState
from django.db.migrations.operations.base import Operation from django.db.migrations.operations.base import Operation
from django.utils import six from django.utils import six
@ -183,7 +183,7 @@ class AlterUniqueTogether(Operation):
def __init__(self, name, unique_together): def __init__(self, name, unique_together):
self.name = name self.name = name
unique_together = normalize_unique_together(unique_together) unique_together = normalize_together(unique_together)
self.unique_together = set(tuple(cons) for cons in unique_together) self.unique_together = set(tuple(cons) for cons in unique_together)
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
@ -220,6 +220,7 @@ class AlterIndexTogether(Operation):
def __init__(self, name, index_together): def __init__(self, name, index_together):
self.name = name self.name = name
index_together = normalize_together(index_together)
self.index_together = set(tuple(cons) for cons in index_together) self.index_together = set(tuple(cons) for cons in index_together)
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):

View File

@ -1,7 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.apps.registry import Apps from django.apps.registry import Apps
from django.db import models from django.db import models
from django.db.models.options import DEFAULT_NAMES, normalize_unique_together from django.db.models.options import DEFAULT_NAMES, normalize_together
from django.utils import six from django.utils import six
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
@ -145,7 +145,10 @@ class ModelState(object):
elif name in model._meta.original_attrs: elif name in model._meta.original_attrs:
if name == "unique_together": if name == "unique_together":
ut = model._meta.original_attrs["unique_together"] ut = model._meta.original_attrs["unique_together"]
options[name] = set(normalize_unique_together(ut)) options[name] = set(normalize_together(ut))
elif name == "index_together":
it = model._meta.original_attrs["index_together"]
options[name] = set(normalize_together(it))
else: else:
options[name] = model._meta.original_attrs[name] options[name] = model._meta.original_attrs[name]
# Make our record # Make our record

View File

@ -24,24 +24,26 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
'select_on_save') 'select_on_save')
def normalize_unique_together(unique_together): def normalize_together(option_together):
""" """
unique_together can be either a tuple of tuples, or a single option_together can be either a tuple of tuples, or a single
tuple of two strings. Normalize it to a tuple of tuples, so that tuple of two strings. Normalize it to a tuple of tuples, so that
calling code can uniformly expect that. calling code can uniformly expect that.
""" """
try: try:
if not unique_together: if not option_together:
return () return ()
first_element = next(iter(unique_together)) if not isinstance(option_together, (tuple, list)):
raise TypeError
first_element = next(iter(option_together))
if not isinstance(first_element, (tuple, list)): if not isinstance(first_element, (tuple, list)):
unique_together = (unique_together,) option_together = (option_together,)
# Normalize everything to tuples # Normalize everything to tuples
return tuple(tuple(ut) for ut in unique_together) return tuple(tuple(ot) for ot in option_together)
except TypeError: except TypeError:
# If the value of unique_together isn't valid, return it # If the value of option_together isn't valid, return it
# verbatim; this will be picked up by the check framework later. # verbatim; this will be picked up by the check framework later.
return unique_together return option_together
@python_2_unicode_compatible @python_2_unicode_compatible
@ -140,7 +142,10 @@ class Options(object):
self.original_attrs[attr_name] = getattr(self, attr_name) self.original_attrs[attr_name] = getattr(self, attr_name)
ut = meta_attrs.pop('unique_together', self.unique_together) ut = meta_attrs.pop('unique_together', self.unique_together)
self.unique_together = normalize_unique_together(ut) self.unique_together = normalize_together(ut)
it = meta_attrs.pop('index_together', self.index_together)
self.index_together = normalize_together(it)
# 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.

View File

@ -340,6 +340,13 @@ Django quotes column and table names behind the scenes.
This list of fields will be indexed together (i.e. the appropriate This list of fields will be indexed together (i.e. the appropriate
``CREATE INDEX`` statement will be issued.) ``CREATE INDEX`` statement will be issued.)
.. versionchanged:: 1.7
For convenience, ``index_together`` can be a single list when dealing with a single
set of fields::
index_together = ["pub_date", "deadline"]
``verbose_name`` ``verbose_name``
---------------- ----------------

View File

@ -642,6 +642,9 @@ Models
an error (before that, it would either result in a database error or an error (before that, it would either result in a database error or
incorrect data). incorrect data).
* You can use a single list for :attr:`~django.db.models.Options.index_together`
(rather than a list of lists) when specifying a single set of fields.
Signals Signals
^^^^^^^ ^^^^^^^

View File

@ -12,6 +12,14 @@ class Article(models.Model):
] ]
# Model for index_together being used only with single list
class IndexTogetherSingleList(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateTimeField()
class Meta:
index_together = ["headline", "pub_date"]
# Indexing a TextField on Oracle or MySQL results in index creation error. # Indexing a TextField on Oracle or MySQL results in index creation error.
if connection.vendor == 'postgresql': if connection.vendor == 'postgresql':
class IndexedArticle(models.Model): class IndexedArticle(models.Model):

View File

@ -4,7 +4,7 @@ from django.core.management.color import no_style
from django.db import connections, DEFAULT_DB_ALIAS from django.db import connections, DEFAULT_DB_ALIAS
from django.test import TestCase from django.test import TestCase
from .models import Article from .models import Article, IndexTogetherSingleList
class IndexesTests(TestCase): class IndexesTests(TestCase):
@ -13,6 +13,12 @@ class IndexesTests(TestCase):
index_sql = connection.creation.sql_indexes_for_model(Article, no_style()) index_sql = connection.creation.sql_indexes_for_model(Article, no_style())
self.assertEqual(len(index_sql), 1) self.assertEqual(len(index_sql), 1)
def test_index_together_single_list(self):
# Test for using index_together with a single list (#22172)
connection = connections[DEFAULT_DB_ALIAS]
index_sql = connection.creation.sql_indexes_for_model(IndexTogetherSingleList, no_style())
self.assertEqual(len(index_sql), 1)
@skipUnless(connections[DEFAULT_DB_ALIAS].vendor == 'postgresql', @skipUnless(connections[DEFAULT_DB_ALIAS].vendor == 'postgresql',
"This is a postgresql-specific issue") "This is a postgresql-specific issue")
def test_postgresql_text_indexes(self): def test_postgresql_text_indexes(self):

View File

@ -45,10 +45,7 @@ class IndexTogetherTests(IsolatedModelsTestCase):
def test_list_containing_non_iterable(self): def test_list_containing_non_iterable(self):
class Model(models.Model): class Model(models.Model):
class Meta: class Meta:
index_together = [ index_together = [('a', 'b'), 42]
'non-iterable',
'second-non-iterable',
]
errors = Model.check() errors = Model.check()
expected = [ expected = [
@ -139,6 +136,22 @@ class UniqueTogetherTests(IsolatedModelsTestCase):
] ]
self.assertEqual(errors, expected) self.assertEqual(errors, expected)
def test_non_list(self):
class Model(models.Model):
class Meta:
unique_together = 'not-a-list'
errors = Model.check()
expected = [
Error(
'"unique_together" must be a list or tuple.',
hint=None,
obj=Model,
id='E008',
),
]
self.assertEqual(errors, expected)
def test_valid_model(self): def test_valid_model(self):
class Model(models.Model): class Model(models.Model):
one = models.IntegerField() one = models.IntegerField()

View File

@ -25,6 +25,7 @@ class StateTests(TestCase):
app_label = "migrations" app_label = "migrations"
apps = new_apps apps = new_apps
unique_together = ["name", "bio"] unique_together = ["name", "bio"]
index_together = ["bio", "age"]
class AuthorProxy(Author): class AuthorProxy(Author):
class Meta: class Meta:
@ -63,7 +64,7 @@ class StateTests(TestCase):
self.assertEqual(author_state.fields[1][1].max_length, 255) self.assertEqual(author_state.fields[1][1].max_length, 255)
self.assertEqual(author_state.fields[2][1].null, False) self.assertEqual(author_state.fields[2][1].null, False)
self.assertEqual(author_state.fields[3][1].null, True) self.assertEqual(author_state.fields[3][1].null, True)
self.assertEqual(author_state.options, {"unique_together": set([("name", "bio")])}) self.assertEqual(author_state.options, {"unique_together": set([("name", "bio")]), "index_together": set([("bio", "age")])})
self.assertEqual(author_state.bases, (models.Model, )) self.assertEqual(author_state.bases, (models.Model, ))
self.assertEqual(book_state.app_label, "migrations") self.assertEqual(book_state.app_label, "migrations")