diff --git a/django/contrib/postgres/apps.py b/django/contrib/postgres/apps.py index 0eeee6332b1..9de733c261b 100644 --- a/django/contrib/postgres/apps.py +++ b/django/contrib/postgres/apps.py @@ -5,7 +5,7 @@ from django.db.models import CharField, TextField from django.utils.translation import ugettext_lazy as _ from .lookups import SearchLookup, TrigramSimilar, Unaccent -from .signals import register_hstore_handler +from .signals import register_type_handlers class PostgresConfig(AppConfig): @@ -16,8 +16,8 @@ class PostgresConfig(AppConfig): # Connections may already exist before we are called. for conn in connections.all(): if conn.connection is not None: - register_hstore_handler(conn) - connection_created.connect(register_hstore_handler) + register_type_handlers(conn) + connection_created.connect(register_type_handlers) CharField.register_lookup(Unaccent) TextField.register_lookup(Unaccent) CharField.register_lookup(SearchLookup) diff --git a/django/contrib/postgres/operations.py b/django/contrib/postgres/operations.py index cb45d4f705e..cf790209b1f 100644 --- a/django/contrib/postgres/operations.py +++ b/django/contrib/postgres/operations.py @@ -1,4 +1,4 @@ -from django.contrib.postgres.signals import register_hstore_handler +from django.contrib.postgres.signals import register_type_handlers from django.db.migrations.operations.base import Operation @@ -15,6 +15,10 @@ class CreateExtension(Operation): if schema_editor.connection.vendor != 'postgresql': return schema_editor.execute("CREATE EXTENSION IF NOT EXISTS %s" % schema_editor.quote_name(self.name)) + # Registering new type handlers cannot be done before the extension is + # installed, otherwise a subsequent data migration would use the same + # connection. + register_type_handlers(schema_editor.connection) def database_backwards(self, app_label, schema_editor, from_state, to_state): schema_editor.execute("DROP EXTENSION %s" % schema_editor.quote_name(self.name)) @@ -40,13 +44,6 @@ class HStoreExtension(CreateExtension): def __init__(self): self.name = 'hstore' - def database_forwards(self, app_label, schema_editor, from_state, to_state): - super(HStoreExtension, self).database_forwards(app_label, schema_editor, from_state, to_state) - # Register hstore straight away as it cannot be done before the - # extension is installed, a subsequent data migration would use the - # same connection - register_hstore_handler(schema_editor.connection) - class TrigramExtension(CreateExtension): diff --git a/django/contrib/postgres/signals.py b/django/contrib/postgres/signals.py index 183ba1d9830..af7f0b2d309 100644 --- a/django/contrib/postgres/signals.py +++ b/django/contrib/postgres/signals.py @@ -1,10 +1,11 @@ +import psycopg2 from psycopg2 import ProgrammingError from psycopg2.extras import register_hstore from django.utils import six -def register_hstore_handler(connection, **kwargs): +def register_type_handlers(connection, **kwargs): if connection.vendor != 'postgresql': return @@ -23,3 +24,17 @@ def register_hstore_handler(connection, **kwargs): # This is also needed in order to create the connection in order to # install the hstore extension. pass + + try: + with connection.cursor() as cursor: + # Retrieve oids of citext arrays. + cursor.execute("SELECT typarray FROM pg_type WHERE typname = 'citext'") + oids = tuple(row[0] for row in cursor) + array_type = psycopg2.extensions.new_array_type(oids, 'citext[]', psycopg2.STRING) + psycopg2.extensions.register_type(array_type, None) + except ProgrammingError: + # citext is not available on the database. + # + # The same comments in the except block of the above call to + # register_hstore() also apply here. + pass diff --git a/docs/releases/1.11.1.txt b/docs/releases/1.11.1.txt index acce5259afe..0b7d6d9efbd 100644 --- a/docs/releases/1.11.1.txt +++ b/docs/releases/1.11.1.txt @@ -88,3 +88,6 @@ Bugfixes * Fixed a regression where ``Model._state.db`` wasn't set correctly on multi-table inheritance parent models after saving a child model (:ticket:`28166`). + +* Corrected the return type of ``ArrayField(CITextField())`` values retrieved + from the database (:ticket:`28161`). diff --git a/tests/postgres_tests/__init__.py b/tests/postgres_tests/__init__.py index 75286781ddf..932057532c3 100644 --- a/tests/postgres_tests/__init__.py +++ b/tests/postgres_tests/__init__.py @@ -12,14 +12,14 @@ class PostgreSQLTestCase(TestCase): @classmethod def tearDownClass(cls): # No need to keep that signal overhead for non PostgreSQL-related tests. - from django.contrib.postgres.signals import register_hstore_handler + from django.contrib.postgres.signals import register_type_handlers - connection_created.disconnect(register_hstore_handler) + connection_created.disconnect(register_type_handlers) super(PostgreSQLTestCase, cls).tearDownClass() @unittest.skipUnless(connection.vendor == 'postgresql', "PostgreSQL specific tests") # To locate the widget's template. @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'}) -class PostgreSQLWidgetTestCase(WidgetTest): +class PostgreSQLWidgetTestCase(WidgetTest, PostgreSQLTestCase): pass diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py index 5a6a3783585..4f2aec0b24c 100644 --- a/tests/postgres_tests/migrations/0002_create_test_models.py +++ b/tests/postgres_tests/migrations/0002_create_test_models.py @@ -144,6 +144,7 @@ class Migration(migrations.Migration): ('name', CICharField(primary_key=True, max_length=255)), ('email', CIEmailField()), ('description', CITextField()), + ('array_field', ArrayField(CITextField(), null=True)), ], options={ 'required_db_vendor': 'postgresql', diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index 255db915130..a24d45cdbfe 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -105,6 +105,7 @@ class CITestModel(PostgreSQLModel): name = CICharField(primary_key=True, max_length=255) email = CIEmailField() description = CITextField() + array_field = ArrayField(CITextField(), null=True) def __str__(self): return self.name diff --git a/tests/postgres_tests/test_citext.py b/tests/postgres_tests/test_citext.py index f99f3bfa479..0a7012b0720 100644 --- a/tests/postgres_tests/test_citext.py +++ b/tests/postgres_tests/test_citext.py @@ -4,11 +4,13 @@ strings and thus eliminates the need for operations such as iexact and other modifiers to enforce use of an index. """ from django.db import IntegrityError +from django.test.utils import modify_settings from . import PostgreSQLTestCase from .models import CITestModel +@modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'}) class CITextTestCase(PostgreSQLTestCase): @classmethod @@ -17,6 +19,7 @@ class CITextTestCase(PostgreSQLTestCase): name='JoHn', email='joHn@johN.com', description='Average Joe named JoHn', + array_field=['JoE', 'jOhn'], ) def test_equal_lowercase(self): @@ -34,3 +37,8 @@ class CITextTestCase(PostgreSQLTestCase): """ with self.assertRaises(IntegrityError): CITestModel.objects.create(name='John') + + def test_array_field(self): + instance = CITestModel.objects.get() + self.assertEqual(instance.array_field, self.john.array_field) + self.assertTrue(CITestModel.objects.filter(array_field__contains=['joe']).exists())