Sfoglia il codice sorgente

Fixed #23524 -- Allowed DATABASES['TIME_ZONE'] option on PostgreSQL.

Aymeric Augustin 5 anni fa
parent
commit
c06492dd87

+ 8 - 12
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):

+ 4 - 2
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

+ 0 - 7
django/db/backends/postgresql/utils.py

@@ -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

+ 5 - 2
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 <DATABASE-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

+ 42 - 13
docs/ref/settings.txt

@@ -632,23 +632,52 @@ 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.
+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.
 
-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 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.
 
-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.
+* 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.
 
-When :setting:`USE_TZ` is ``True`` and the database supports time zones (e.g.
-PostgreSQL), it is an error to set this option.
+  Changing the connection time zone changes how datetimes are read from and
+  written to the database.
 
-When :setting:`USE_TZ` is ``False``, it is an error to set this option.
+  * 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
 

+ 3 - 0
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 <DATABASE-TIME_ZONE>` setting is now allowed on
+  databases that support time zones.
+
 .. _backwards-incompatible-3.1:
 
 Backwards incompatible changes in 3.1

+ 9 - 23
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):