From c06492dd87342b5102db44f0d50fa0bb01cbb743 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 20 Jul 2019 15:38:43 +0200 Subject: [PATCH] Fixed #23524 -- Allowed DATABASES['TIME_ZONE'] option on PostgreSQL. --- django/db/backends/base/base.py | 20 ++++----- django/db/backends/postgresql/base.py | 6 ++- django/db/backends/postgresql/utils.py | 7 --- docs/ref/databases.txt | 7 ++- docs/ref/settings.txt | 59 +++++++++++++++++++------- docs/releases/3.1.txt | 3 ++ tests/timezones/tests.py | 32 ++++---------- 7 files changed, 73 insertions(+), 61 deletions(-) delete mode 100644 django/db/backends/postgresql/utils.py diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py index 08d5f755aa..61e0dc0ca0 100644 --- a/django/db/backends/base/base.py +++ b/django/db/backends/base/base.py @@ -124,11 +124,11 @@ class BaseDatabaseWrapper: When the database backend supports time zones, it doesn't matter which time zone Django uses, as long as aware datetimes are used everywhere. - For simplicity, Django selects UTC. + Other users connecting to the database can choose their own time zone. When the database backend doesn't support time zones, the time zone - Django uses can be selected with the TIME_ZONE configuration option, so - it can match what other users of the database expect. + Django uses may be constrained by the requirements of other users of + the database. """ if not settings.USE_TZ: return None @@ -205,15 +205,11 @@ class BaseDatabaseWrapper: self.run_on_commit = [] def check_settings(self): - if self.settings_dict['TIME_ZONE'] is not None: - if not settings.USE_TZ: - raise ImproperlyConfigured( - "Connection '%s' cannot set TIME_ZONE because USE_TZ is " - "False." % self.alias) - elif self.features.supports_timezones: - raise ImproperlyConfigured( - "Connection '%s' cannot set TIME_ZONE because its engine " - "handles time zones conversions natively." % self.alias) + if self.settings_dict['TIME_ZONE'] is not None and not settings.USE_TZ: + raise ImproperlyConfigured( + "Connection '%s' cannot set TIME_ZONE because USE_TZ is False." + % self.alias + ) @async_unsafe def ensure_connection(self): diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index bf15a13018..0ab81ced74 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -47,7 +47,6 @@ from .features import DatabaseFeatures # NOQA isort:skip from .introspection import DatabaseIntrospection # NOQA isort:skip from .operations import DatabaseOperations # NOQA isort:skip from .schema import DatabaseSchemaEditor # NOQA isort:skip -from .utils import utc_tzinfo_factory # NOQA isort:skip psycopg2.extensions.register_adapter(SafeString, psycopg2.extensions.QuotedString) psycopg2.extras.register_uuid() @@ -231,9 +230,12 @@ class DatabaseWrapper(BaseDatabaseWrapper): cursor = self.connection.cursor(name, scrollable=False, withhold=self.connection.autocommit) else: cursor = self.connection.cursor() - cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None + cursor.tzinfo_factory = self.tzinfo_factory if settings.USE_TZ else None return cursor + def tzinfo_factory(self, offset): + return self.timezone + @async_unsafe def chunked_cursor(self): self._named_cursor_idx += 1 diff --git a/django/db/backends/postgresql/utils.py b/django/db/backends/postgresql/utils.py deleted file mode 100644 index 2c03ab36cd..0000000000 --- a/django/db/backends/postgresql/utils.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.utils.timezone import utc - - -def utc_tzinfo_factory(offset): - if offset != 0: - raise AssertionError("database connection isn't set to UTC") - return utc diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 8f1de4c283..b4c190bafd 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -121,8 +121,11 @@ Django needs the following parameters for its database connections: - ``client_encoding``: ``'UTF8'``, - ``default_transaction_isolation``: ``'read committed'`` by default, or the value set in the connection options (see below), -- ``timezone``: ``'UTC'`` when :setting:`USE_TZ` is ``True``, value of - :setting:`TIME_ZONE` otherwise. +- ``timezone``: + - when :setting:`USE_TZ` is ``True``, ``'UTC'`` by default, or the + :setting:`TIME_ZONE ` value set for the connection, + - when :setting:`USE_TZ` is ``False``, the value of the global + :setting:`TIME_ZONE` setting. If these parameters already have the correct values, Django won't set them for every new connection, which improves performance slightly. You can configure diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index e04d211885..6c8ea9b762 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -632,24 +632,53 @@ default port. Not used with SQLite. Default: ``None`` -A string representing the time zone for datetimes stored in this database -(assuming that it doesn't support time zones) or ``None``. This inner option of -the :setting:`DATABASES` setting accepts the same values as the general -:setting:`TIME_ZONE` setting. - -This allows interacting with third-party databases that store datetimes in -local time rather than UTC. To avoid issues around DST changes, you shouldn't -set this option for databases managed by Django. - -When :setting:`USE_TZ` is ``True`` and the database doesn't support time zones -(e.g. SQLite, MySQL, Oracle), Django reads and writes datetimes in local time -according to this option if it is set and in UTC if it isn't. - -When :setting:`USE_TZ` is ``True`` and the database supports time zones (e.g. -PostgreSQL), it is an error to set this option. +A string representing the time zone for this database connection or ``None``. +This inner option of the :setting:`DATABASES` setting accepts the same values +as the general :setting:`TIME_ZONE` setting. +When :setting:`USE_TZ` is ``True`` and this option is set, reading datetimes +from the database returns aware datetimes in this time zone instead of UTC. When :setting:`USE_TZ` is ``False``, it is an error to set this option. +* If the database backend doesn't support time zones (e.g. SQLite, MySQL, + Oracle), Django reads and writes datetimes in local time according to this + option if it is set and in UTC if it isn't. + + Changing the connection time zone changes how datetimes are read from and + written to the database. + + * If Django manages the database and you don't have a strong reason to do + otherwise, you should leave this option unset. It's best to store datetimes + in UTC because it avoids ambiguous or nonexistent datetimes during daylight + saving time changes. Also, receiving datetimes in UTC keeps datetime + arithmetic simple — there's no need for the ``normalize()`` method provided + by pytz. + + * If you're connecting to a third-party database that stores datetimes in a + local time rather than UTC, then you must set this option to the + appropriate time zone. Likewise, if Django manages the database but + third-party systems connect to the same database and expect to find + datetimes in local time, then you must set this option. + +* If the database backend supports time zones (e.g. PostgreSQL), the + ``TIME_ZONE`` option is very rarely needed. It can be changed at any time; + the database takes care of converting datetimes to the desired time zone. + + Setting the time zone of the database connection may be useful for running + raw SQL queries involving date/time functions provided by the database, such + as ``date_trunc``, because their results depend on the time zone. + + However, this has a downside: receiving all datetimes in local time makes + datetime arithmetic more tricky — you must call the ``normalize()`` method + provided by pytz after each operation. + + Consider converting to local time explicitly with ``AT TIME ZONE`` in raw SQL + queries instead of setting the ``TIME_ZONE`` option. + +.. versionchanged:: 3.1 + + Using this option when the database backend supports time zones was allowed. + .. setting:: DATABASE-DISABLE_SERVER_SIDE_CURSORS ``DISABLE_SERVER_SIDE_CURSORS`` diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 58f14e172d..4c8cc56797 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -283,6 +283,9 @@ Miscellaneous :class:`pathlib.Path` instead of :mod:`os.path` for building filesystem paths. +* The :setting:`TIME_ZONE ` setting is now allowed on + databases that support time zones. + .. _backwards-incompatible-3.1: Backwards incompatible changes in 3.1 diff --git a/tests/timezones/tests.py b/tests/timezones/tests.py index a317af5a55..91c8f9f451 100644 --- a/tests/timezones/tests.py +++ b/tests/timezones/tests.py @@ -9,8 +9,7 @@ import pytz from django.contrib.auth.models import User from django.core import serializers -from django.core.exceptions import ImproperlyConfigured -from django.db import connection, connections +from django.db import connection from django.db.models import F, Max, Min from django.http import HttpRequest from django.template import ( @@ -532,6 +531,14 @@ class NewDatabaseTests(TestCase): cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [utc_naive_dt]) self.assertEqual(cursor.fetchall()[0][0], utc_naive_dt) + @skipUnlessDBFeature('supports_timezones') + def test_cursor_explicit_time_zone(self): + with override_database_connection_timezone('Europe/Paris'): + with connection.cursor() as cursor: + cursor.execute('SELECT CURRENT_TIMESTAMP') + now = cursor.fetchone()[0] + self.assertEqual(now.tzinfo.zone, 'Europe/Paris') + @requires_tz_support def test_filter_date_field_with_aware_datetime(self): # Regression test for #17742 @@ -595,27 +602,6 @@ class ForcedTimeZoneDatabaseTests(TransactionTestCase): self.assertEqual(event.dt, fake_dt) -@skipUnlessDBFeature('supports_timezones') -@override_settings(TIME_ZONE='Africa/Nairobi', USE_TZ=True) -class UnsupportedTimeZoneDatabaseTests(TestCase): - - def test_time_zone_parameter_not_supported_if_database_supports_timezone(self): - connections.databases['tz'] = connections.databases['default'].copy() - connections.databases['tz']['TIME_ZONE'] = 'Asia/Bangkok' - tz_conn = connections['tz'] - try: - msg = ( - "Connection 'tz' cannot set TIME_ZONE because its engine " - "handles time zones conversions natively." - ) - with self.assertRaisesMessage(ImproperlyConfigured, msg): - tz_conn.cursor() - finally: - connections['tz'].close() # in case the test fails - del connections['tz'] - del connections.databases['tz'] - - @override_settings(TIME_ZONE='Africa/Nairobi') class SerializationTests(SimpleTestCase):