Commit ed83881e authored by Aymeric Augustin's avatar Aymeric Augustin
Browse files

Fixed #23820 -- Supported per-database time zone.

The primary use case is to interact with a third-party database (not
primarily managed by Django) that doesn't support time zones and where
datetimes are stored in local time when USE_TZ is True.

Configuring a PostgreSQL database with the TIME_ZONE option while USE_TZ
is False used to result in silent data corruption. Now this is an error.
parent 54026f1e
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -20,10 +20,9 @@ router = ConnectionRouter()
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
# for backend bits.

# DatabaseWrapper.__init__() takes a dictionary, not a settings module, so
# we manually create the dictionary from the settings, passing only the
# settings that the database backends care about. Note that TIME_ZONE is used
# by the PostgreSQL backends.
# DatabaseWrapper.__init__() takes a dictionary, not a settings module, so we
# manually create the dictionary from the settings, passing only the settings
# that the database backends care about.
# We load all these up for backwards compatibility, you should use
# connections['default'] instead.
class DefaultConnectionProxy(object):
+57 −0
Original line number Diff line number Diff line
@@ -4,14 +4,21 @@ from collections import deque
from contextlib import contextmanager

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import DEFAULT_DB_ALIAS
from django.db.backends import utils
from django.db.backends.signals import connection_created
from django.db.transaction import TransactionManagementError
from django.db.utils import DatabaseError, DatabaseErrorWrapper
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.six.moves import _thread as thread

try:
    import pytz
except ImportError:
    pytz = None

NO_DB_ALIAS = '__no_db__'


@@ -71,6 +78,39 @@ class BaseDatabaseWrapper(object):
        self.allow_thread_sharing = allow_thread_sharing
        self._thread_ident = thread.get_ident()

    @cached_property
    def timezone(self):
        """
        Time zone for datetimes stored as naive values in the database.

        Returns a tzinfo object or None.

        This is only needed when time zone support is enabled and the database
        doesn't support time zones. (When the database supports time zones,
        the adapter handles aware datetimes so Django doesn't need to.)
        """
        if not settings.USE_TZ:
            return None
        elif self.features.supports_timezones:
            return None
        elif self.settings_dict['TIME_ZONE'] is None:
            return timezone.utc
        else:
            # Only this branch requires pytz.
            return pytz.timezone(self.settings_dict['TIME_ZONE'])

    @cached_property
    def timezone_name(self):
        """
        Name of the time zone of the database connection.
        """
        if not settings.USE_TZ:
            return settings.TIME_ZONE
        elif self.settings_dict['TIME_ZONE'] is None:
            return 'UTC'
        else:
            return self.settings_dict['TIME_ZONE']

    @property
    def queries_logged(self):
        return self.force_debug_cursor or settings.DEBUG
@@ -105,6 +145,8 @@ class BaseDatabaseWrapper(object):

    def connect(self):
        """Connects to the database. Assumes that the connection is closed."""
        # Check for invalid configurations.
        self.check_settings()
        # In case the previous connection was closed while in an atomic block
        self.in_atomic_block = False
        self.savepoint_ids = []
@@ -121,6 +163,21 @@ class BaseDatabaseWrapper(object):
        self.init_connection_state()
        connection_created.send(sender=self.__class__, connection=self)

    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)
            elif pytz is None:
                raise ImproperlyConfigured(
                    "Connection '%s' cannot set TIME_ZONE because pytz isn't "
                    "installed." % self.alias)

    def ensure_connection(self):
        """
        Guarantees that a connection to the database is established.
+2 −0
Original line number Diff line number Diff line
@@ -61,6 +61,8 @@ def adapt_datetime_warn_on_aware_datetime(value, conv):
            "probably from cursor.execute(). Update your code to pass a "
            "naive datetime in the database connection's time zone (UTC by "
            "default).", RemovedInDjango21Warning)
        # This doesn't account for the database connection's timezone,
        # which isn't known. (That's why this adapter is deprecated.)
        value = value.astimezone(timezone.utc).replace(tzinfo=None)
    return Thing2Literal(value.strftime("%Y-%m-%d %H:%M:%S.%f"), conv)

+2 −2
Original line number Diff line number Diff line
@@ -145,7 +145,7 @@ class DatabaseOperations(BaseDatabaseOperations):
        # MySQL doesn't support tz-aware datetimes
        if timezone.is_aware(value):
            if settings.USE_TZ:
                value = value.astimezone(timezone.utc).replace(tzinfo=None)
                value = timezone.make_naive(value, self.connection.timezone)
            else:
                raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")

@@ -205,7 +205,7 @@ class DatabaseOperations(BaseDatabaseOperations):
    def convert_datetimefield_value(self, value, expression, connection, context):
        if value is not None:
            if settings.USE_TZ:
                value = value.replace(tzinfo=timezone.utc)
                value = timezone.make_aware(value, self.connection.timezone)
        return value

    def convert_uuidfield_value(self, value, expression, connection, context):
+2 −2
Original line number Diff line number Diff line
@@ -196,7 +196,7 @@ WHEN (new.%(col_name)s IS NULL)
    def convert_datetimefield_value(self, value, expression, connection, context):
        if value is not None:
            if settings.USE_TZ:
                value = value.replace(tzinfo=timezone.utc)
                value = timezone.make_aware(value, self.connection.timezone)
        return value

    def convert_datefield_value(self, value, expression, connection, context):
@@ -399,7 +399,7 @@ WHEN (new.%(col_name)s IS NULL)
        # cx_Oracle doesn't support tz-aware datetimes
        if timezone.is_aware(value):
            if settings.USE_TZ:
                value = value.astimezone(timezone.utc).replace(tzinfo=None)
                value = timezone.make_naive(value, self.connection.timezone)
            else:
                raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.")

Loading