From 77d0c9d549a8d95156c833eceb6275d9b7317d4e Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 18 Jun 2026 21:00:45 +0200 Subject: [PATCH 1/2] Fixed #37168 -- Made admin display database defaults as empty values. --- django/contrib/admin/utils.py | 10 ++++++++-- tests/admin_utils/tests.py | 27 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index ed6c41f52eb4..746537e9c5f8 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -435,6 +435,7 @@ def help_text_for_field(name, model): def display_for_field(value, field, empty_value_display, avoid_link=False): from django.contrib.admin.templatetags.admin_list import _boolean_icon + from django.db.models.expressions import DatabaseDefault if field.name == "password" and field.model == get_user_model(): return render_password_as_hash(value) @@ -450,8 +451,10 @@ def display_for_field(value, field, empty_value_display, avoid_link=False): # BooleanField needs special-case null-handling, so it comes before the # general null test. elif isinstance(field, models.BooleanField): + if isinstance(value, DatabaseDefault): + return _boolean_icon(None) return _boolean_icon(value) - elif value in field.empty_values: + elif value in field.empty_values or isinstance(value, DatabaseDefault): return empty_value_display elif isinstance(field, models.DateTimeField): return formats.localize(timezone.template_localtime(value)) @@ -476,12 +479,15 @@ def display_for_field(value, field, empty_value_display, avoid_link=False): def display_for_value(value, empty_value_display, boolean=False): from django.contrib.admin.templatetags.admin_list import _boolean_icon + from django.db.models.expressions import DatabaseDefault if boolean: + if value in EMPTY_VALUES or isinstance(value, DatabaseDefault): + return _boolean_icon(None) return _boolean_icon(value) if isinstance(value, str) and not isinstance(value, SafeString): value = value.strip() - if value in EMPTY_VALUES: + if value in EMPTY_VALUES or isinstance(value, DatabaseDefault): return empty_value_display elif isinstance(value, bool): return str(value) diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py index eced9d206e99..15b09ca812ac 100644 --- a/tests/admin_utils/tests.py +++ b/tests/admin_utils/tests.py @@ -21,6 +21,7 @@ from django.contrib.auth.templatetags.auth import render_password_as_hash from django.core.validators import EMPTY_VALUES from django.db import DEFAULT_DB_ALIAS, models +from django.db.models.expressions import DatabaseDefault from django.test import SimpleTestCase, TestCase, override_settings, skipUnlessDBFeature from django.test.utils import isolate_apps from django.utils.formats import localize @@ -194,6 +195,12 @@ def test_empty_value_display_for_field(self): ) self.assertEqual(display_value, self.empty_value) + def test_empty_value_database_default_display_for_field(self): + display_value = display_for_field( + DatabaseDefault(models.Value(1)), models.IntegerField(), self.empty_value + ) + self.assertEqual(display_value, self.empty_value) + def test_empty_value_display_choices(self): model_field = models.CharField(choices=((None, "test_none"),)) display_value = display_for_field(None, model_field, self.empty_value) @@ -201,11 +208,13 @@ def test_empty_value_display_choices(self): def test_empty_value_display_booleanfield(self): model_field = models.BooleanField(null=True) - display_value = display_for_field(None, model_field, self.empty_value) expected = ( f'None' ) - self.assertHTMLEqual(display_value, expected) + for value in [None, DatabaseDefault(models.Value(True))]: + with self.subTest(empty_value=value): + display_value = display_for_field(value, model_field, self.empty_value) + self.assertHTMLEqual(display_value, expected) def test_json_display_for_field(self): tests = [ @@ -307,12 +316,26 @@ def test_list_display_for_value_boolean(self): self.assertEqual(display_for_value(True, ""), "True") self.assertEqual(display_for_value(False, ""), "False") + def test_list_display_for_value_boolean_database_default(self): + # DatabaseDefault expression is interpreted as unknown. + self.assertEqual( + display_for_value(DatabaseDefault(models.Value(True)), "", boolean=True), + 'None', + ) + self.assertEqual(display_for_value(DatabaseDefault(models.Value(True)), ""), "") + def test_list_display_for_value_empty(self): for value in EMPTY_VALUES: with self.subTest(empty_value=value): display_value = display_for_value(value, self.empty_value) self.assertEqual(display_value, self.empty_value) + def test_list_display_for_database_default(self): + display_value = display_for_value( + DatabaseDefault(models.Value("1")), self.empty_value + ) + self.assertEqual(display_value, self.empty_value) + def test_list_display_for_value_consecutive_whitespace(self): cases = [ (" ", "-empty-"), From 189c2d2ce58482db5c09d8b1e1464d1456dc9bab Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 12 Jun 2026 00:33:05 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Fixed=20#37164=20--=20Supported=20subclasse?= =?UTF-8?q?s=E2=80=99=20=5Fget=5Flines()=20in=20jsonl=20Deserializer.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- django/core/serializers/jsonl.py | 2 +- tests/serializers/test_jsonl.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/django/core/serializers/jsonl.py b/django/core/serializers/jsonl.py index 7bc9bed79f54..c9aa0d0879bd 100644 --- a/django/core/serializers/jsonl.py +++ b/django/core/serializers/jsonl.py @@ -47,7 +47,7 @@ def __init__(self, stream_or_string, **options): stream_or_string = stream_or_string.decode() if isinstance(stream_or_string, str): stream_or_string = stream_or_string.splitlines() - super().__init__(Deserializer._get_lines(stream_or_string), **options) + super().__init__(self._get_lines(stream_or_string), **options) def _handle_object(self, obj): try: diff --git a/tests/serializers/test_jsonl.py b/tests/serializers/test_jsonl.py index 3137b037a982..49b52533612f 100644 --- a/tests/serializers/test_jsonl.py +++ b/tests/serializers/test_jsonl.py @@ -4,6 +4,7 @@ from django.core import serializers from django.core.serializers.base import DeserializationError +from django.core.serializers.jsonl import Deserializer as JsonlDeserializer from django.db import models from django.test import TestCase, TransactionTestCase from django.test.utils import isolate_apps @@ -269,3 +270,22 @@ class JsonSerializerTransactionTestCase( }""", ] fwd_ref_str = "\n".join([s.replace("\n", "") for s in fwd_ref_str]) + + +class JsonlDeserializerTests(TestCase): + def test_subclass_can_override_get_lines(self): + collected = [] + + class CustomDeserializer(JsonlDeserializer): + @staticmethod + def _get_lines(stream): + for obj in JsonlDeserializer._get_lines(stream): + collected.append(obj) + yield obj + + data = ( + '{"pk": 1, "model": "serializers.author", "fields": {"name": "Jane"}}\n' + '{"pk": 2, "model": "serializers.author", "fields": {"name": "Joe"}}\n' + ) + list(CustomDeserializer(data)) + self.assertEqual(len(collected), 2)