From c068f000be7c486abb8b17fc565383679d7d4f82 Mon Sep 17 00:00:00 2001 From: Chaitanya Rahalkar Date: Tue, 17 Dec 2024 23:17:17 -0600 Subject: [PATCH 1/7] Fixed #36014 -- Supported international domains in EmailValidator. --- django/core/validators.py | 20 +++++++------------ .../field_tests/test_emailfield.py | 3 ++- tests/validators/tests.py | 14 +++++++++++++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/django/core/validators.py b/django/core/validators.py index c4e734c1d82a..ff9573b1726d 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -6,7 +6,6 @@ from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible -from django.utils.encoding import punycode from django.utils.ipv6 import is_valid_ipv6_address from django.utils.regex_helper import _lazy_re_compile from django.utils.translation import gettext_lazy as _ @@ -76,14 +75,14 @@ class DomainNameValidator(RegexValidator): # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1. domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?', ValidationError), + (validate_email, "email@xn--4ca9at.com", None), + (validate_email, "email@öäü.com", None), + (validate_email, "email@עִתוֹן.example.il", None), + (validate_email, "email@މިހާރު.example.mv", None), + (validate_email, "email@漢字.example.com", None), + (validate_email, "editor@މިހާރު.example.mv", None), + (validate_email, "@domain.com", ValidationError), + (validate_email, "email.domain.com", ValidationError), + (validate_email, "email@domain@domain.com", ValidationError), + (validate_email, "email@domain..com", ValidationError), + (validate_email, "email@.domain.com", ValidationError), + (validate_email, "email@-domain.com", ValidationError), + (validate_email, "email@domain-.com", ValidationError), + (validate_email, "email@domain.com-", ValidationError), # Quoted-string format (CR not allowed) (validate_email, '"\\\011"@here.com', None), (validate_email, '"\\\012"@here.com', ValidationError), From 7617d5be94a6e348d5ddf4644985b24235822034 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 7 Jan 2025 10:01:40 -0500 Subject: [PATCH 2/7] Refs #36065 -- Extracted composite primary key order by tests. --- tests/composite_pk/test_filter.py | 24 ------------ tests/composite_pk/test_order_by.py | 57 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 tests/composite_pk/test_order_by.py diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py index 06cd6781dfb9..864877483a7c 100644 --- a/tests/composite_pk/test_filter.py +++ b/tests/composite_pk/test_filter.py @@ -70,30 +70,6 @@ def test_rhs_combinable(self): ): Comment.objects.filter(text__gt=expr).count() - def test_order_comments_by_pk_asc(self): - self.assertSequenceEqual( - Comment.objects.order_by("pk"), - ( - self.comment_1, # (1, 1) - self.comment_2, # (1, 2) - self.comment_3, # (1, 3) - self.comment_5, # (1, 5) - self.comment_4, # (2, 4) - ), - ) - - def test_order_comments_by_pk_desc(self): - self.assertSequenceEqual( - Comment.objects.order_by("-pk"), - ( - self.comment_4, # (2, 4) - self.comment_5, # (1, 5) - self.comment_3, # (1, 3) - self.comment_2, # (1, 2) - self.comment_1, # (1, 1) - ), - ) - def test_filter_comments_by_pk_gt(self): c11, c12, c13, c24, c15 = ( self.comment_1, diff --git a/tests/composite_pk/test_order_by.py b/tests/composite_pk/test_order_by.py new file mode 100644 index 000000000000..9d3dec58e735 --- /dev/null +++ b/tests/composite_pk/test_order_by.py @@ -0,0 +1,57 @@ +from django.test import TestCase + +from .models import Comment, Tenant, User + + +class CompositePKOrderByTests(TestCase): + maxDiff = None + + @classmethod + def setUpTestData(cls): + cls.tenant_1 = Tenant.objects.create() + cls.tenant_2 = Tenant.objects.create() + cls.tenant_3 = Tenant.objects.create() + cls.user_1 = User.objects.create( + tenant=cls.tenant_1, + id=1, + email="user0001@example.com", + ) + cls.user_2 = User.objects.create( + tenant=cls.tenant_1, + id=2, + email="user0002@example.com", + ) + cls.user_3 = User.objects.create( + tenant=cls.tenant_2, + id=3, + email="user0003@example.com", + ) + cls.comment_1 = Comment.objects.create(id=1, user=cls.user_1) + cls.comment_2 = Comment.objects.create(id=2, user=cls.user_1) + cls.comment_3 = Comment.objects.create(id=3, user=cls.user_2) + cls.comment_4 = Comment.objects.create(id=4, user=cls.user_3) + cls.comment_5 = Comment.objects.create(id=5, user=cls.user_1) + + def test_order_comments_by_pk_asc(self): + self.assertSequenceEqual( + Comment.objects.order_by("pk"), + ( + self.comment_1, # (1, 1) + self.comment_2, # (1, 2) + self.comment_3, # (1, 3) + self.comment_5, # (1, 5) + self.comment_4, # (2, 4) + ), + ) + + def test_order_comments_by_pk_desc(self): + self.assertSequenceEqual( + Comment.objects.order_by("-pk"), + ( + self.comment_4, # (2, 4) + self.comment_5, # (1, 5) + self.comment_3, # (1, 3) + self.comment_2, # (1, 2) + self.comment_1, # (1, 1) + ), + ) From 42e8f264ce55710056b0033682ec6fd662a25b29 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 7 Jan 2025 00:02:31 -0500 Subject: [PATCH 3/7] Fixed #36065 -- Fixed ordering by expression referencing composite primary key. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Jacob Walls for the report and test and Csirmaz Bendegúz for the review. --- django/db/models/expressions.py | 10 ++++++++++ django/db/models/sql/compiler.py | 3 +-- tests/composite_pk/test_order_by.py | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 746fe0414361..2494ec41393c 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -1861,6 +1861,16 @@ def get_source_expressions(self): return [self.expression] def as_sql(self, compiler, connection, template=None, **extra_context): + if isinstance(self.expression, ColPairs): + sql_parts = [] + params = [] + for col in self.expression.get_cols(): + copy = self.copy() + copy.set_source_expressions([col]) + sql, col_params = compiler.compile(copy) + sql_parts.append(sql) + params.extend(col_params) + return ", ".join(sql_parts), params template = template or self.template if connection.features.supports_order_by_nulls_modifier: if self.nulls_last: diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 5bb491d823cc..251cc08e512e 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1117,10 +1117,9 @@ def find_ordering_name( ) return results targets, alias, _ = self.query.trim_joins(targets, joins, path) - target_fields = composite.unnest(targets) return [ (OrderBy(transform_function(t, alias), descending=descending), False) - for t in target_fields + for t in targets ] def _setup_joins(self, pieces, opts, alias): diff --git a/tests/composite_pk/test_order_by.py b/tests/composite_pk/test_order_by.py index 9d3dec58e735..f17d6b55e4bc 100644 --- a/tests/composite_pk/test_order_by.py +++ b/tests/composite_pk/test_order_by.py @@ -1,3 +1,4 @@ +from django.db.models import F from django.test import TestCase from .models import Comment, Tenant, User @@ -55,3 +56,17 @@ def test_order_comments_by_pk_desc(self): self.comment_1, # (1, 1) ), ) + + def test_order_comments_by_pk_expr(self): + self.assertQuerySetEqual( + Comment.objects.order_by("pk"), + Comment.objects.order_by(F("pk")), + ) + self.assertQuerySetEqual( + Comment.objects.order_by("-pk"), + Comment.objects.order_by(F("pk").desc()), + ) + self.assertQuerySetEqual( + Comment.objects.order_by("-pk"), + Comment.objects.order_by(F("pk").desc(nulls_last=True)), + ) From 007f14365988bd94c35dc34959c1ef4c2407c86f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 7 Jan 2025 20:12:09 +0100 Subject: [PATCH 4/7] Strengthened wording on supported Python versions in FAQ. --- docs/faq/install.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/faq/install.txt b/docs/faq/install.txt index 4623e288fbed..9928e2c4c016 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -60,11 +60,11 @@ For each version of Python, only the latest micro release (A.B.C) is officially supported. You can find the latest micro version for each series on the `Python download page `_. -Typically, we will support a Python version up to and including the first -Django LTS release whose security support ends after security support for that -version of Python ends. For example, Python 3.9 security support ends in -October 2025 and Django 4.2 LTS security support ends in April 2026. Therefore -Django 4.2 is the last version to support Python 3.9. +We will support a Python version up to and including the first Django LTS +release whose security support ends after security support for that version of +Python ends. For example, Python 3.9 security support ends in October 2025 and +Django 4.2 LTS security support ends in April 2026. Therefore Django 4.2 is the +last version to support Python 3.9. What Python version should I use with Django? ============================================= From 15e207ce80581ec64bd790c37cce1bc07d01a744 Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:21:28 +0100 Subject: [PATCH 5/7] Fixed #35999 -- Removed #django IRC channel references where appropriate. Some references are replaced with links to the Django Discord server. --- docs/faq/help.txt | 5 ----- docs/internals/contributing/bugs-and-features.txt | 8 ++++---- docs/internals/contributing/index.txt | 7 +++---- docs/internals/howto-release-django.txt | 3 --- docs/intro/contributing.txt | 8 ++++---- docs/intro/tutorial08.txt | 1 - docs/intro/whatsnext.txt | 6 +++--- 7 files changed, 14 insertions(+), 24 deletions(-) diff --git a/docs/faq/help.txt b/docs/faq/help.txt index a999c08d3205..7c8b39fc46b6 100644 --- a/docs/faq/help.txt +++ b/docs/faq/help.txt @@ -23,14 +23,9 @@ Then, please post it in one of the following channels: discussions. * The |django-users| mailing list. This is for email-based discussions. * The `Django Discord server`_ for chat-based discussions. -* The `#django IRC channel`_ on the Libera.Chat IRC network. This is for - chat-based discussions. If you're new to IRC, see the `Libera.Chat - documentation`_ for different ways to connect. .. _`"Using Django"`: https://forum.djangoproject.com/c/users/6 .. _`Django Discord server`: https://discord.gg/xcRH6mN4fa -.. _#django IRC channel: https://web.libera.chat/#django -.. _Libera.Chat documentation: https://libera.chat/guides/connect In all these channels please abide by the `Django Code of Conduct`_. In summary, being friendly and patient, considerate, respectful, and careful in diff --git a/docs/internals/contributing/bugs-and-features.txt b/docs/internals/contributing/bugs-and-features.txt index c59f79e6b871..d95ab96efd71 100644 --- a/docs/internals/contributing/bugs-and-features.txt +++ b/docs/internals/contributing/bugs-and-features.txt @@ -17,7 +17,7 @@ Otherwise, before reporting a bug or requesting a new feature on the `searching`_ or running `custom queries`_ in the ticket tracker. * Don't use the ticket system to ask support questions. Use the - |django-users| list or the `#django`_ IRC channel for that. + |django-users| list or the `Django Discord server`_ for that. * Don't reopen issues that have been marked "wontfix" without finding consensus to do so on the `Django Forum`_ or |django-developers| list. @@ -39,8 +39,8 @@ particular: * **Do** read the :doc:`FAQ ` to see if your issue might be a well-known question. -* **Do** ask on |django-users| or `#django`_ *first* if you're not sure if - what you're seeing is a bug. +* **Do** ask on |django-users| or the `Django Discord server`_ *first* if + you're not sure if what you're seeing is a bug. * **Do** write complete, reproducible, specific bug reports. You must include a clear, concise description of the problem, and a set of @@ -166,5 +166,5 @@ Votes on technical matters should be announced and held in public on the .. _searching: https://code.djangoproject.com/search .. _custom queries: https://code.djangoproject.com/query -.. _#django: https://web.libera.chat/#django .. _Django Forum: https://forum.djangoproject.com/ +.. _Django Discord server: https://discord.gg/xcRH6mN4fa diff --git a/docs/internals/contributing/index.txt b/docs/internals/contributing/index.txt index b547e468b713..6ac5e884bdf5 100644 --- a/docs/internals/contributing/index.txt +++ b/docs/internals/contributing/index.txt @@ -31,9 +31,9 @@ a great ecosystem to work in: friendly and helpful atmosphere. If you're new to the Django community, you should read the `posting guidelines`_. -* Join the `Django Discord server`_ or the `#django IRC channel`_ on - Libera.Chat to discuss and answer questions. By explaining Django to other - users, you're going to learn a lot about the framework yourself. +* Join the `Django Discord server`_ to discuss and answer questions. By + explaining Django to other users, you're going to learn a lot about the + framework yourself. * Blog about Django. We syndicate all the Django blogs we know about on the `community page`_; if you'd like to see your blog on that page you @@ -45,7 +45,6 @@ a great ecosystem to work in: build it! .. _posting guidelines: https://code.djangoproject.com/wiki/UsingTheMailingList -.. _#django IRC channel: https://web.libera.chat/#django .. _community page: https://www.djangoproject.com/community/ .. _Django Discord server: https://discord.gg/xcRH6mN4fa .. _Django forum: https://forum.djangoproject.com/ diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 131c60fec8c5..bedb1b88220d 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -561,9 +561,6 @@ Now you're ready to actually put the release out there. To do this: message body should include the vulnerability details, for example, the announcement blog post text. Include a link to the announcement blog post. -#. Add a link to the blog post in the topic of the ``#django`` IRC channel: - ``/msg chanserv TOPIC #django new topic goes here``. - Post-release ============ diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt index 0900fdae37e4..32c8645f20ad 100644 --- a/docs/intro/contributing.txt +++ b/docs/intro/contributing.txt @@ -41,13 +41,13 @@ so that it can be of use to the widest audience. .. admonition:: Where to get help: If you're having trouble going through this tutorial, please post a message - on the `Django Forum`_, |django-developers|, or drop by - `#django-dev on irc.libera.chat`__ to chat with other Django users who - might be able to help. + on the `Django Forum`_, |django-developers|, or drop by the + `Django Discord server`_ to chat with other Django users who might be able + to help. -__ https://web.libera.chat/#django-dev .. _Dive Into Python: https://diveintopython3.net/ .. _Django Forum: https://forum.djangoproject.com/ +.. _Django Discord server: https://discord.gg/xcRH6mN4fa What does this tutorial cover? ------------------------------ diff --git a/docs/intro/tutorial08.txt b/docs/intro/tutorial08.txt index 98bf70d330bd..261cd85d8587 100644 --- a/docs/intro/tutorial08.txt +++ b/docs/intro/tutorial08.txt @@ -71,7 +71,6 @@ resolve the issue yourself, there are options available to you. Toolbar’s is `on GitHub `_. #. Consult the `Django Forum `_. #. Join the `Django Discord server `_. -#. Join the #Django IRC channel on `Libera.chat `_. Installing other third-party packages ===================================== diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index ca55b12d7a0d..e02d90f800a8 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -123,11 +123,11 @@ ticket system and use your feedback to improve the documentation for everybody. Note, however, that tickets should explicitly relate to the documentation, rather than asking broad tech-support questions. If you need help with your -particular Django setup, try the |django-users| mailing list or the `#django -IRC channel`_ instead. +particular Django setup, try the |django-users| mailing list or the +`Django Discord server`_ instead. .. _ticket system: https://code.djangoproject.com/ -.. _#django IRC channel: https://web.libera.chat/#django +.. _Django Discord server: https://discord.gg/xcRH6mN4fa In plain text ------------- From 6a1a9c0eade674780060cf8af5f5b3375156cdd5 Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:40:05 +0100 Subject: [PATCH 6/7] Fixed #36062 -- Handled serialization of CompositePrimaryKeys. --- django/core/serializers/xml_serializer.py | 2 +- django/db/models/fields/composite.py | 25 +++++++++++++ tests/composite_pk/fixtures/tenant.json | 8 +++++ tests/composite_pk/models/__init__.py | 3 +- tests/composite_pk/models/tenant.py | 6 ++++ tests/composite_pk/tests.py | 43 ++++++++++++++++++++++- 6 files changed, 84 insertions(+), 3 deletions(-) diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 3530d443b237..360d5309d853 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -56,7 +56,7 @@ def start_object(self, obj): if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"): obj_pk = obj.pk if obj_pk is not None: - attrs["pk"] = str(obj_pk) + attrs["pk"] = obj._meta.pk.value_to_string(obj) self.xml.startElement("object", attrs) diff --git a/django/db/models/fields/composite.py b/django/db/models/fields/composite.py index 2b196f6d2ac3..4b74f90c1fc8 100644 --- a/django/db/models/fields/composite.py +++ b/django/db/models/fields/composite.py @@ -1,3 +1,5 @@ +import json + from django.core import checks from django.db.models import NOT_PROVIDED, Field from django.db.models.expressions import ColPairs @@ -13,6 +15,11 @@ from django.utils.functional import cached_property +class AttributeSetter: + def __init__(self, name, value): + setattr(self, name, value) + + class CompositeAttribute: def __init__(self, field): self.field = field @@ -130,6 +137,24 @@ def _check_field_name(self): ) ] + def value_to_string(self, obj): + values = [] + vals = self.value_from_object(obj) + for field, value in zip(self.fields, vals): + obj = AttributeSetter(field.attname, value) + values.append(field.value_to_string(obj)) + return json.dumps(values, ensure_ascii=False) + + def to_python(self, value): + if isinstance(value, str): + # Assume we're deserializing. + vals = json.loads(value) + value = [ + field.to_python(val) + for field, val in zip(self.fields, vals, strict=True) + ] + return value + CompositePrimaryKey.register_lookup(TupleExact) CompositePrimaryKey.register_lookup(TupleGreaterThan) diff --git a/tests/composite_pk/fixtures/tenant.json b/tests/composite_pk/fixtures/tenant.json index 3eeff42fefde..66a25e94f600 100644 --- a/tests/composite_pk/fixtures/tenant.json +++ b/tests/composite_pk/fixtures/tenant.json @@ -71,5 +71,13 @@ "tenant_id": 2, "id": "ffffffff-ffff-ffff-ffff-ffffffffffff" } + }, + { + "pk": [1, "2022-01-12T05:55:14.956"], + "model": "composite_pk.timestamped", + "fields": { + "id": 1, + "created": "2022-01-12T05:55:14.956" + } } ] diff --git a/tests/composite_pk/models/__init__.py b/tests/composite_pk/models/__init__.py index 35c394371696..5996ae33b0b7 100644 --- a/tests/composite_pk/models/__init__.py +++ b/tests/composite_pk/models/__init__.py @@ -1,9 +1,10 @@ -from .tenant import Comment, Post, Tenant, Token, User +from .tenant import Comment, Post, Tenant, TimeStamped, Token, User __all__ = [ "Comment", "Post", "Tenant", + "TimeStamped", "Token", "User", ] diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py index ac0b3d9715a1..810fb50db7c7 100644 --- a/tests/composite_pk/models/tenant.py +++ b/tests/composite_pk/models/tenant.py @@ -48,3 +48,9 @@ class Post(models.Model): pk = models.CompositePrimaryKey("tenant_id", "id") tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) id = models.UUIDField() + + +class TimeStamped(models.Model): + pk = models.CompositePrimaryKey("id", "created") + id = models.SmallIntegerField(unique=True) + created = models.DateTimeField(auto_now_add=True) diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py index 4ebdbc371ef5..303c6592fb52 100644 --- a/tests/composite_pk/tests.py +++ b/tests/composite_pk/tests.py @@ -17,7 +17,7 @@ from django.forms import modelform_factory from django.test import TestCase -from .models import Comment, Post, Tenant, User +from .models import Comment, Post, Tenant, TimeStamped, User class CommentForm(forms.ModelForm): @@ -224,6 +224,13 @@ def test_objects(self): self.assertEqual(post_2.tenant_id, 2) self.assertEqual(post_2.pk, (post_2.tenant_id, post_2.id)) + def assert_deserializer(self, format, users, serialized_users): + deserialized_user = list(serializers.deserialize(format, serialized_users))[0] + self.assertEqual(deserialized_user.object.email, users[0].email) + self.assertEqual(deserialized_user.object.id, users[0].id) + self.assertEqual(deserialized_user.object.tenant, users[0].tenant) + self.assertEqual(deserialized_user.object.pk, users[0].pk) + def test_serialize_user_json(self): users = User.objects.filter(pk=(1, 1)) result = serializers.serialize("json", users) @@ -241,6 +248,7 @@ def test_serialize_user_json(self): } ], ) + self.assert_deserializer(format="json", users=users, serialized_users=result) def test_serialize_user_jsonl(self): users = User.objects.filter(pk=(1, 2)) @@ -257,6 +265,7 @@ def test_serialize_user_jsonl(self): }, }, ) + self.assert_deserializer(format="jsonl", users=users, serialized_users=result) @unittest.skipUnless(HAS_YAML, "No yaml library detected") def test_serialize_user_yaml(self): @@ -276,6 +285,7 @@ def test_serialize_user_yaml(self): }, ], ) + self.assert_deserializer(format="yaml", users=users, serialized_users=result) def test_serialize_user_python(self): users = User.objects.filter(pk=(2, 4)) @@ -294,6 +304,13 @@ def test_serialize_user_python(self): }, ], ) + self.assert_deserializer(format="python", users=users, serialized_users=result) + + def test_serialize_user_xml(self): + users = User.objects.filter(pk=(1, 1)) + result = serializers.serialize("xml", users) + self.assertIn('', result) + self.assert_deserializer(format="xml", users=users, serialized_users=result) def test_serialize_post_uuid(self): posts = Post.objects.filter(pk=(2, "11111111-1111-1111-1111-111111111111")) @@ -311,3 +328,27 @@ def test_serialize_post_uuid(self): }, ], ) + + def test_serialize_datetime(self): + result = serializers.serialize("json", TimeStamped.objects.all()) + self.assertEqual( + json.loads(result), + [ + { + "model": "composite_pk.timestamped", + "pk": [1, "2022-01-12T05:55:14.956"], + "fields": { + "id": 1, + "created": "2022-01-12T05:55:14.956", + }, + }, + ], + ) + + def test_invalid_pk_extra_field(self): + json = ( + '[{"fields": {"email": "user0001@example.com", "id": 1, "tenant": 1}, ' + '"pk": [1, 1, "extra"], "model": "composite_pk.user"}]' + ) + with self.assertRaises(serializers.base.DeserializationError): + next(serializers.deserialize("json", json)) From a9c79b462923ce366101db1b5a3fff3d1dad870c Mon Sep 17 00:00:00 2001 From: antoliny0919 Date: Sun, 5 Jan 2025 18:58:03 +0900 Subject: [PATCH 7/7] Fixed #36063 -- Made a FileField navigate to the object admin change page when in list_display_links. --- django/contrib/admin/templatetags/admin_list.py | 10 ++++++++-- django/contrib/admin/utils.py | 4 ++-- tests/admin_changelist/admin.py | 10 +++++++++- tests/admin_changelist/models.py | 1 + tests/admin_changelist/tests.py | 10 ++++++++++ 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index fdf6e63f5fb9..7a4958717273 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -214,6 +214,7 @@ def link_in_col(is_first, field_name, cl): for field_index, field_name in enumerate(cl.list_display): empty_value_display = cl.model_admin.get_empty_value_display() row_classes = ["field-%s" % _coerce_field_name(field_name, field_index)] + link_to_changelist = link_in_col(first, field_name, cl) try: f, attr, value = lookup_field(field_name, result, cl.model_admin) except ObjectDoesNotExist: @@ -240,14 +241,19 @@ def link_in_col(is_first, field_name, cl): else: result_repr = field_val else: - result_repr = display_for_field(value, f, empty_value_display) + result_repr = display_for_field( + value, + f, + empty_value_display, + avoid_link=link_to_changelist, + ) if isinstance( f, (models.DateField, models.TimeField, models.ForeignKey) ): row_classes.append("nowrap") row_class = mark_safe(' class="%s"' % " ".join(row_classes)) # If list_display_links not defined, add the link tag to the first field - if link_in_col(first, field_name, cl): + if link_to_changelist: table_tag = "th" if first else "td" first = False diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index c8e722bcc8e6..0fe0e4e6e65f 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -426,7 +426,7 @@ def help_text_for_field(name, model): return help_text -def display_for_field(value, field, empty_value_display): +def display_for_field(value, field, empty_value_display, avoid_link=False): from django.contrib.admin.templatetags.admin_list import _boolean_icon if getattr(field, "flatchoices", None): @@ -452,7 +452,7 @@ def display_for_field(value, field, empty_value_display): return formats.number_format(value, field.decimal_places) elif isinstance(field, (models.IntegerField, models.FloatField)): return formats.number_format(value) - elif isinstance(field, models.FileField) and value: + elif isinstance(field, models.FileField) and value and not avoid_link: return format_html('{}', value.url, value) elif isinstance(field, models.JSONField) and value: try: diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 937beea48f34..701d60cd05f3 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import User from django.core.paginator import Paginator -from .models import Band, Child, Event, GrandChild, Parent, ProxyUser, Swallow +from .models import Band, Child, Event, Genre, GrandChild, Parent, ProxyUser, Swallow site = admin.AdminSite(name="admin") @@ -157,6 +157,14 @@ class NoListDisplayLinksParentAdmin(admin.ModelAdmin): site.register(Parent, NoListDisplayLinksParentAdmin) +class ListDisplayLinksGenreAdmin(admin.ModelAdmin): + list_display = ["name", "file"] + list_display_links = ["file"] + + +site.register(Genre, ListDisplayLinksGenreAdmin) + + class SwallowAdmin(admin.ModelAdmin): actions = None # prevent ['action_checkbox'] + list(list_display) list_display = ("origin", "load", "speed", "swallowonetoone") diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py index 78e65ab8782b..6b2fba4cedf4 100644 --- a/tests/admin_changelist/models.py +++ b/tests/admin_changelist/models.py @@ -32,6 +32,7 @@ def __html__(self): class Genre(models.Model): name = models.CharField(max_length=20) + file = models.FileField(upload_to="documents/", blank=True, null=True) class Band(models.Model): diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 0be6a54ed496..f682ac60bbd2 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -1057,6 +1057,16 @@ def test_no_list_display_links(self): link = reverse("admin:admin_changelist_parent_change", args=(p.pk,)) self.assertNotContains(response, '' % link) + def test_link_field_display_links(self): + self.client.force_login(self.superuser) + g = Genre.objects.create(name="Blues", file="documents/blues_history.txt") + response = self.client.get(reverse("admin:admin_changelist_genre_changelist")) + self.assertContains( + response, + '' + "documents/blues_history.txt" % g.pk, + ) + def test_clear_all_filters_link(self): self.client.force_login(self.superuser) url = reverse("admin:auth_user_changelist")