From 35f86b641a4eb26fa2b7c3842b6d374834a4ea6b Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 14 Nov 2025 13:36:15 +0100 Subject: [PATCH 1/2] Refs #24928 -- Added introspection support for PostgreSQL HStoreField. --- django/contrib/postgres/apps.py | 5 +++++ django/db/backends/postgresql/base.py | 2 ++ django/db/backends/postgresql/introspection.py | 12 +++++++++++- docs/releases/6.1.txt | 4 +++- tests/backends/postgresql/tests.py | 3 +++ tests/postgres_tests/test_introspection.py | 13 +++++++++++++ 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/django/contrib/postgres/apps.py b/django/contrib/postgres/apps.py index a8ee3fbf4b7e..63fd9b274e2f 100644 --- a/django/contrib/postgres/apps.py +++ b/django/contrib/postgres/apps.py @@ -65,6 +65,11 @@ def ready(self): 3910: "django.contrib.postgres.fields.DateTimeRangeField", 3912: "django.contrib.postgres.fields.DateRangeField", 3926: "django.contrib.postgres.fields.BigIntegerRangeField", + # PostgreSQL OIDs may vary depending on the + # installation, especially for datatypes from + # extensions, e.g. "hstore". In such cases, the + # type_display attribute (psycopg 3.2+) should be used. + "hstore": "django.contrib.postgres.fields.HStoreField", } ) if conn.connection is not None: diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 17ac417fff54..e2728afea5f4 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -8,6 +8,7 @@ import threading import warnings from contextlib import contextmanager +from functools import lru_cache from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -29,6 +30,7 @@ raise ImproperlyConfigured("Error loading psycopg2 or psycopg module") +@lru_cache def psycopg_version(): version = Database.__version__.split(" ", 1)[0] return get_version_tuple(version) diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py index 30edaf10dafd..791d729ea480 100644 --- a/django/db/backends/postgresql/introspection.py +++ b/django/db/backends/postgresql/introspection.py @@ -3,6 +3,7 @@ from django.db.backends.base.introspection import BaseDatabaseIntrospection from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo from django.db.backends.base.introspection import TableInfo as BaseTableInfo +from django.db.backends.postgresql.base import psycopg_version from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING, Index FieldInfo = namedtuple("FieldInfo", [*BaseFieldInfo._fields, "is_autofield", "comment"]) @@ -120,10 +121,19 @@ def get_table_description(self, cursor, table_name): cursor.execute( "SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name) ) + + # PostgreSQL OIDs may vary depending on the installation, especially + # for datatypes from extensions, e.g. "hstore". In such cases, the + # type_display attribute (psycopg 3.2+) should be used. + type_display_available = psycopg_version() >= (3, 2) return [ FieldInfo( line.name, - line.type_code, + ( + line.type_display + if type_display_available and line.type_display == "hstore" + else line.type_code + ), # display_size is always None on psycopg2. line.internal_size if line.display_size is None else line.display_size, line.internal_size, diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index e0edf6876af0..8c5594d5b69d 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -126,7 +126,9 @@ Minor features :mod:`django.contrib.postgres` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* :djadmin:`inspectdb` now introspects + :class:`~django.contrib.postgres.fields.HStoreField` when ``psycopg`` 3.2+ is + installed and ``django.contrib.postgres`` is in :setting:`INSTALLED_APPS`. :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index f7e7d1e68ccf..e03c37cb574f 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -517,8 +517,11 @@ def test_lookup_cast_isnull_noop(self): def test_correct_extraction_psycopg_version(self): from django.db.backends.postgresql.base import Database, psycopg_version + psycopg_version.cache_clear() with mock.patch.object(Database, "__version__", "4.2.1 (dt dec pq3 ext lo64)"): + self.addCleanup(psycopg_version.cache_clear) self.assertEqual(psycopg_version(), (4, 2, 1)) + psycopg_version.cache_clear() with mock.patch.object( Database, "__version__", "4.2b0.dev1 (dt dec pq3 ext lo64)" ): diff --git a/tests/postgres_tests/test_introspection.py b/tests/postgres_tests/test_introspection.py index 73c426d1bafc..1ae940fdb91a 100644 --- a/tests/postgres_tests/test_introspection.py +++ b/tests/postgres_tests/test_introspection.py @@ -33,3 +33,16 @@ def test_range_fields(self): "null=True)", ], ) + + def test_hstore_field(self): + from django.db.backends.postgresql.base import psycopg_version + + if psycopg_version() < (3, 2): + self.skipTest("psycopg 3.2+ is required.") + self.assertFieldsInModel( + "postgres_tests_hstoremodel", + [ + "field = django.contrib.postgres.fields.HStoreField(blank=True, " + "null=True)", + ], + ) From 5c60763561c67924eff1069e1516b60a59d068d5 Mon Sep 17 00:00:00 2001 From: Kasyap Pentamaraju Date: Tue, 11 Nov 2025 11:10:29 +0530 Subject: [PATCH 2/2] Fixed #36723 -- Removed logic for repositioning FilteredSelectMultiple help text. --- .../contrib/admin/static/admin/js/SelectFilter2.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js index fde59d30293f..99efa1e0cabf 100644 --- a/django/contrib/admin/static/admin/js/SelectFilter2.js +++ b/django/contrib/admin/static/admin/js/SelectFilter2.js @@ -18,18 +18,6 @@ Requires core.js and SelectBox.js. from_box.setAttribute('aria-labelledby', field_id + '_from_label'); from_box.setAttribute('aria-describedby', `${field_id}_helptext ${field_id}_choose_helptext`); - for (const p of from_box.parentNode.getElementsByTagName('p')) { - if (p.classList.contains("info")) { - // Remove

, because it just gets in the way. - from_box.parentNode.removeChild(p); - } else if (p.classList.contains("help")) { - // Move help text up to the top so it isn't below the select - // boxes or wrapped off on the side to the right of the add - // button: - from_box.parentNode.insertBefore(p, from_box.parentNode.firstChild); - } - } - //

or
const selector_div = quickElement('div', from_box.parentNode); // Make sure the selector div appears between the label and the add link.