diff --git a/django/conf/locale/fr_CA/formats.py b/django/conf/locale/fr_CA/formats.py index 4f1a017f168d..ecb45f5bbb14 100644 --- a/django/conf/locale/fr_CA/formats.py +++ b/django/conf/locale/fr_CA/formats.py @@ -3,12 +3,12 @@ # The *_FORMAT strings use the Django date format syntax, # see https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date DATE_FORMAT = "j F Y" # 31 janvier 2024 -TIME_FORMAT = "H\xa0h\xa0i" # 13 h 40 -DATETIME_FORMAT = "j F Y, H\xa0h\xa0i" # 31 janvier 2024, 13 h 40 +TIME_FORMAT = "H\xa0\\h\xa0i" # 13 h 40 +DATETIME_FORMAT = "j F Y, H\xa0\\h\xa0i" # 31 janvier 2024, 13 h 40 YEAR_MONTH_FORMAT = "F Y" MONTH_DAY_FORMAT = "j F" SHORT_DATE_FORMAT = "Y-m-d" -SHORT_DATETIME_FORMAT = "Y-m-d H\xa0h\xa0i" +SHORT_DATETIME_FORMAT = "Y-m-d H\xa0\\h\xa0i" FIRST_DAY_OF_WEEK = 0 # Dimanche # The *_INPUT_FORMATS strings use the Python strftime format syntax, diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index 16a6296f9b5f..9822a7fbb1c8 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -9,6 +9,7 @@ from django.db import NotSupportedError, transaction from django.db.models.expressions import Col from django.utils import timezone +from django.utils.duration import duration_microseconds from django.utils.encoding import force_str @@ -564,6 +565,16 @@ def adapt_datetimefield_value(self, value): return None return str(value) + def adapt_durationfield_value(self, value): + """ + Transform a timedelta value into an object compatible with what is + expected by the backend driver for duration columns (by default, + an integer of microseconds). + """ + if value is None: + return None + return duration_microseconds(value) + def adapt_timefield_value(self, value): """ Transform a time value to an object compatible with what is expected diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index bc152c4e6ed5..e5da5928f966 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -608,6 +608,9 @@ def adapt_datetimefield_value(self, value): return Oracle_datetime.from_datetime(value) + def adapt_durationfield_value(self, value): + return value + def adapt_timefield_value(self, value): if value is None: return None diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index 7cd868d78951..91456e3dafca 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -330,6 +330,9 @@ def adapt_datefield_value(self, value): def adapt_datetimefield_value(self, value): return value + def adapt_durationfield_value(self, value): + return value + def adapt_timefield_value(self, value): return value diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index b1939f8b358d..8d3fa5c92cde 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -309,7 +309,10 @@ def collect( protected_objects = defaultdict(list) for related in get_candidate_relations_to_delete(model._meta): # Preserve parent reverse relationships if keep_parents=True. - if keep_parents and related.model in model._meta.all_parents: + if ( + keep_parents + and related.model._meta.concrete_model in model._meta.all_parents + ): continue field = related.field on_delete = field.remote_field.on_delete diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index ee7a30cc303e..f12ae97968ea 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -30,7 +30,7 @@ parse_duration, parse_time, ) -from django.utils.duration import duration_microseconds, duration_string +from django.utils.duration import duration_string from django.utils.functional import Promise, cached_property from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address from django.utils.text import capfirst @@ -1890,11 +1890,7 @@ def to_python(self, value): ) def get_db_prep_value(self, value, connection, prepared=False): - if connection.features.has_native_duration_field: - return value - if value is None: - return None - return duration_microseconds(value) + return connection.ops.adapt_durationfield_value(value) def get_db_converters(self, connection): converters = [] diff --git a/docs/ref/contrib/postgres/search.txt b/docs/ref/contrib/postgres/search.txt index 88e3cfaeb045..0d14bc8c0588 100644 --- a/docs/ref/contrib/postgres/search.txt +++ b/docs/ref/contrib/postgres/search.txt @@ -27,7 +27,7 @@ single column in the database. For example: .. code-block:: pycon >>> Entry.objects.filter(body_text__search="Cheese") - [, ] + , ]> This creates a ``to_tsvector`` in the database from the ``body_text`` field and a ``plainto_tsquery`` from the search term ``'Cheese'``, both using the @@ -52,7 +52,7 @@ To query against both fields, use a ``SearchVector``: >>> Entry.objects.annotate( ... search=SearchVector("body_text", "blog__tagline"), ... ).filter(search="Cheese") - [, ] + , ]> The arguments to ``SearchVector`` can be any :class:`~django.db.models.Expression` or the name of a field. Multiple @@ -67,7 +67,7 @@ For example: >>> Entry.objects.annotate( ... search=SearchVector("body_text") + SearchVector("blog__tagline"), ... ).filter(search="Cheese") - [, ] + , ]> See :ref:`postgresql-fts-search-configuration` and :ref:`postgresql-fts-weighting-queries` for an explanation of the ``config`` @@ -142,7 +142,7 @@ order by relevancy: >>> vector = SearchVector("body_text") >>> query = SearchQuery("cheese") >>> Entry.objects.annotate(rank=SearchRank(vector, query)).order_by("-rank") - [, ] + , ]> See :ref:`postgresql-fts-weighting-queries` for an explanation of the ``weights`` parameter. @@ -240,7 +240,7 @@ different language parsers and dictionaries as defined by the database: >>> Entry.objects.annotate( ... search=SearchVector("body_text", config="french"), ... ).filter(search=SearchQuery("œuf", config="french")) - [] + ]> The value of ``config`` could also be stored in another column: @@ -250,7 +250,7 @@ The value of ``config`` could also be stored in another column: >>> Entry.objects.annotate( ... search=SearchVector("body_text", config=F("blog__language")), ... ).filter(search=SearchQuery("œuf", config=F("blog__language"))) - [] + ]> .. _postgresql-fts-weighting-queries: @@ -364,7 +364,7 @@ if it were an annotated ``SearchVector``: >>> Entry.objects.update(search_vector=SearchVector("body_text")) >>> Entry.objects.filter(search_vector="cheese") - [, ] + , ]> .. _PostgreSQL documentation: https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS @@ -403,7 +403,7 @@ Usage example: ... ).filter( ... similarity__gt=0.3 ... ).order_by("-similarity") - [, ] + , ]> ``TrigramWordSimilarity`` ------------------------- @@ -426,7 +426,7 @@ Usage example: ... ).filter( ... similarity__gt=0.3 ... ).order_by("-similarity") - [] + ]> ``TrigramStrictWordSimilarity`` ------------------------------- @@ -459,7 +459,7 @@ Usage example: ... ).filter( ... distance__lte=0.7 ... ).order_by("distance") - [, ] + , ]> ``TrigramWordDistance`` ----------------------- @@ -482,7 +482,7 @@ Usage example: ... ).filter( ... distance__lte=0.7 ... ).order_by("distance") - [] + ]> ``TrigramStrictWordDistance`` ----------------------------- diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index be05350d94d8..edfdacf6b7a7 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -243,7 +243,9 @@ Database backend API This section describes changes that may be needed in third-party database backends. -* ... +* The ``DatabaseOperations.adapt_durationfield_value()`` hook is added. If the + database has native support for ``DurationField``, override this method to + simply return the value. Miscellaneous ------------- diff --git a/tests/delete/models.py b/tests/delete/models.py index 4b627712bb65..7f123b339698 100644 --- a/tests/delete/models.py +++ b/tests/delete/models.py @@ -32,6 +32,11 @@ class RChild(R): pass +class RProxy(R): + class Meta: + proxy = True + + class RChildChild(RChild): pass @@ -179,7 +184,7 @@ class RelToBase(models.Model): class Origin(models.Model): - pass + r_proxy = models.ForeignKey("RProxy", models.CASCADE, null=True) class Referrer(models.Model): diff --git a/tests/delete/tests.py b/tests/delete/tests.py index 7b9dcdb079b5..59140b5c621f 100644 --- a/tests/delete/tests.py +++ b/tests/delete/tests.py @@ -34,6 +34,7 @@ RChild, RChildChild, Referrer, + RProxy, S, T, User, @@ -675,6 +676,14 @@ def receiver(instance, **kwargs): ) signal.disconnect(receiver, sender=Referrer) + def test_keep_parents_does_not_delete_proxy_related(self): + r_child = RChild.objects.create() + r_proxy = RProxy.objects.get(pk=r_child.pk) + Origin.objects.create(r_proxy=r_proxy) + self.assertEqual(Origin.objects.count(), 1) + r_child.delete(keep_parents=True) + self.assertEqual(Origin.objects.count(), 1) + class FastDeleteTests(TestCase): def test_fast_delete_all(self): diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index b4bdf160d6f8..aac56f5df451 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1158,6 +1158,27 @@ def test_l10n_enabled(self): ), ) + def test_uncommon_locale_formats(self): + testcases = { + # French Canadian locale uses 'h' as time format seperator. + ("fr-ca", time_format, (self.t, "TIME_FORMAT")): "10\xa0h\xa015", + ( + "fr-ca", + date_format, + (self.dt, "DATETIME_FORMAT"), + ): "31 décembre 2009, 20\xa0h\xa050", + ( + "fr-ca", + date_format, + (self.dt, "SHORT_DATETIME_FORMAT"), + ): "2009-12-31 20\xa0h\xa050", + } + for testcase, expected in testcases.items(): + locale, format_function, format_args = testcase + with self.subTest(locale=locale, expected=expected): + with translation.override(locale, deactivate=True): + self.assertEqual(expected, format_function(*format_args)) + def test_sub_locales(self): """ Check if sublocales fall back to the main locale