Ticket #17260: dates_in_tz.2.diff

File dates_in_tz.2.diff, 12.2 KB (added by Anssi Kääriäinen, 12 years ago)

AT TIME ZONE syntax for PostgreSQL

  • django/db/backends/__init__.py

    diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
    index 7674f5c..c8f010a 100644
    a b  
     1import datetime
     2
    13from django.db.utils import DatabaseError
     4from django.utils import timezone
    25
    36try:
    47    import thread
    class BaseDatabaseOperations(object):  
    807810
    808811        `value` is an int, containing the looked-up year.
    809812        """
    810         first = '%s-01-01 00:00:00'
    811         second = '%s-12-31 23:59:59.999999'
    812         return [first % value, second % value]
     813        tz = timezone.get_current_timezone() if settings.USE_TZ else None
     814        first_dt = datetime.datetime(year=value, month=01, day=01, tzinfo=tz)
     815        second_dt = datetime.datetime(year=value, month=12, day=31, hour=23,
     816                                      minute=59, second=59, microsecond=999999,
     817                                      tzinfo=tz)
     818        if settings.USE_TZ:
     819            first_dt = first_dt.astimezone(timezone.utc)
     820            second_dt = second_dt.astimezone(timezone.utc)
     821        first = first_dt.strftime('%Y-%m-%d %H:%M:%S')
     822        second = second_dt.strftime('%Y-%m-%d %H:%M:%S.%f')
     823        return [first, second]
    813824
    814825    def year_lookup_bounds_for_date_field(self, value):
    815826        """
  • django/db/backends/postgresql_psycopg2/operations.py

    diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py
    index 949a05c..47a89b9 100644
    a b  
    11from django.db.backends import BaseDatabaseOperations
     2from django.conf import settings
     3from django.utils import timezone
    24
    35
    46class DatabaseOperations(BaseDatabaseOperations):
    class DatabaseOperations(BaseDatabaseOperations):  
    79
    810    def date_extract_sql(self, lookup_type, field_name):
    911        # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
     12        field_sql = field_name
     13        if settings.USE_TZ:
     14            tzname = timezone.get_db_timezone_name(timezone.get_current_timezone())
     15            # SQL-injection warning...
     16            field_sql = "%s AT TIME ZONE '%s'" % (field_sql, tzname)
    1017        if lookup_type == 'week_day':
    1118            # For consistency across backends, we return Sunday=1, Saturday=7.
    12             return "EXTRACT('dow' FROM %s) + 1" % field_name
     19            return "EXTRACT('dow' FROM %s) + 1" % field_sql
    1320        else:
    14             return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name)
     21            return "EXTRACT('%s' FROM %s)" % (lookup_type, field_sql)
    1522
    1623    def date_interval_sql(self, sql, connector, timedelta):
    1724        """
    class DatabaseOperations(BaseDatabaseOperations):  
    3239
    3340    def date_trunc_sql(self, lookup_type, field_name):
    3441        # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
    35         return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
     42        field_sql = field_name
     43        if settings.USE_TZ:
     44            tzname = timezone.get_db_timezone_name(timezone.get_current_timezone())
     45            # This more than slightly obscure construction is to get the date truncation done in the
     46            # correct time zone. However the AT TIME ZONE clause will cause the timezone to be naive
     47            # So we need to make this aware again. The correct solution would be to return a date
     48            # from date truncated datetime, but that would be backwards incompatible.
     49            field_sql = "%s AT TIME ZONE '%s'" % (field_sql, tzname)
     50            return "DATE_TRUNC('%s', %s) AT TIME ZONE 'UTC'" % (lookup_type, field_sql)
     51        return "DATE_TRUNC('%s', %s)" % (lookup_type, field_sql)
    3652
    3753    def deferrable_sql(self):
    3854        return " DEFERRABLE INITIALLY DEFERRED"
  • django/db/backends/sqlite3/base.py

    diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
    index 0b19442..421d883 100644
    a b class DatabaseOperations(BaseDatabaseOperations):  
    127127        # single quotes are used because this is a string (and could otherwise
    128128        # cause a collision with a field name).
    129129        return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name)
     130 
     131    def year_lookup_bounds(self, value):
     132        """
     133        Returns a two-elements list with the lower and upper bound to be used
     134        with a BETWEEN operator to query a field value using a year lookup
     135
     136        `value` is an int, containing the looked-up year.
     137        """
     138        tz = timezone.get_current_timezone() if settings.USE_TZ else None
     139        first_dt = datetime.datetime(year=value, month=01, day=01, tzinfo=tz)
     140        second_dt = datetime.datetime(year=value, month=12, day=31, hour=23,
     141                                      minute=59, second=59, microsecond=999999,
     142                                      tzinfo=tz)
     143        if settings.USE_TZ:
     144            first_dt = first_dt.astimezone(timezone.utc)
     145            second_dt = second_dt.astimezone(timezone.utc)
     146        first = first_dt.strftime('%Y-%m-%d')
     147        second = second_dt.strftime('%Y-%m-%d %H:%M:%S.%f')
     148        return [first, second]
    130149
    131150    def drop_foreignkey_sql(self):
    132151        return ""
    class DatabaseOperations(BaseDatabaseOperations):  
    178197
    179198        return unicode(value)
    180199
    181     def year_lookup_bounds(self, value):
    182         first = '%s-01-01'
    183         second = '%s-12-31 23:59:59.999999'
    184         return [first % value, second % value]
    185 
    186200    def convert_values(self, value, field):
    187201        """SQLite returns floats when it should be returning decimals,
    188202        and gets dates and datetimes wrong.
    def _sqlite_extract(lookup_type, dt):  
    359373        dt = util.typecast_timestamp(dt)
    360374    except (ValueError, TypeError):
    361375        return None
     376    if settings.USE_TZ:
     377        tz = timezone.get_current_timezone()
     378        dt = dt.astimezone(tz)
    362379    if lookup_type == 'week_day':
    363380        return (dt.isoweekday() % 7) + 1
    364381    else:
    def _sqlite_date_trunc(lookup_type, dt):  
    369386        dt = util.typecast_timestamp(dt)
    370387    except (ValueError, TypeError):
    371388        return None
     389    if settings.USE_TZ:
     390        tz = timezone.get_current_timezone()
     391        dt = dt.astimezone(tz)
    372392    if lookup_type == 'year':
    373393        return "%i-01-01 00:00:00" % dt.year
    374394    elif lookup_type == 'month':
  • django/utils/timezone.py

    diff --git a/django/utils/timezone.py b/django/utils/timezone.py
    index 676f8f1..b3d1f6e 100644
    a b This module uses pytz when it's available and fallbacks when it isn't.  
    66from datetime import datetime, timedelta, tzinfo
    77from threading import local
    88import time as _time
     9import re
    910
    1011try:
    1112    import pytz
    def _get_timezone_name(timezone):  
    147148        local_now = datetime.now(timezone)
    148149        return timezone.tzname(local_now)
    149150
     151def get_db_timezone_name(timezone):
     152    # Realistically we would need to convert the timezone
     153    # in some way or another in the database backend.
     154    try:
     155        return timezone.zone
     156    except AttributeError:
     157        local_now = datetime.now(timezone)
     158        tzname = timezone.tzname(local_now)
     159        # me is confused: why do I need to flip signs here.
     160        if re.match('[\+-]\d\d00', tzname):
     161            # Always learn something new: +0300 is -0300 as a POSIX time zone.
     162            return ('-' if tzname[0] == '+' else '+') + tzname[1:3]
     163        return tzname
     164
    150165# Timezone selection functions.
    151166
    152167# These functions don't change os.environ['TZ'] and call time.tzset()
  • tests/modeltests/timezones/tests.py

    diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py
    index a8d2c0c..d093659 100644
    a b LegacyDatabaseTests = override_settings(USE_TZ=False)(LegacyDatabaseTests)  
    277277
    278278#@override_settings(USE_TZ=True)
    279279class NewDatabaseTests(BaseDateTimeTests):
     280    @requires_tz_support
     281    def test_day_in_current_tz(self):
     282        dt = datetime.datetime(2012, 03, 05, 01, 00, 00)
     283        dt = dt.replace(tzinfo=EAT)
     284        Event.objects.create(dt=dt)
     285        with timezone.override('UTC'):
     286            self.assertEqual(Event.objects.dates('dt', 'day')[0].day, 4)
     287        with timezone.override('Africa/Nairobi'):
     288            self.assertEqual(Event.objects.dates('dt', 'day')[0].day, 5)
    280289
    281290    @requires_tz_support
    282291    @skipIf(sys.version_info < (2, 6), "this test requires Python >= 2.6")
    class NewDatabaseTests(BaseDateTimeTests):  
    427436        # implementation is changed to perform the aggregation is local time.
    428437        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT))
    429438        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT))
    430         self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1)
    431         self.assertEqual(Event.objects.filter(dt__month=1).count(), 1)
    432         self.assertEqual(Event.objects.filter(dt__day=1).count(), 1)
    433         self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1)
     439        with timezone.override(EAT):
     440            self.assertEqual(Event.objects.filter(dt__year=2011).count(), 2)
     441            self.assertEqual(Event.objects.filter(dt__month=1).count(), 2)
     442            self.assertEqual(Event.objects.filter(dt__day=1).count(), 2)
     443            self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 2)
     444        with timezone.override(UTC):
     445            self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1)
     446            self.assertEqual(Event.objects.filter(dt__month=1).count(), 1)
     447            self.assertEqual(Event.objects.filter(dt__day=1).count(), 1)
     448            self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1)
    434449
    435450    def test_query_aggregation(self):
    436451        # Only min and max make sense for datetimes.
    class NewDatabaseTests(BaseDateTimeTests):  
    469484        # Same comment as in test_query_date_related_filters.
    470485        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT))
    471486        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT))
    472         self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
    473                 [datetime.datetime(2010, 1, 1, tzinfo=UTC),
    474                  datetime.datetime(2011, 1, 1, tzinfo=UTC)],
    475                 transform=lambda d: d)
    476         self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
    477                 [datetime.datetime(2010, 12, 1, tzinfo=UTC),
    478                  datetime.datetime(2011, 1, 1, tzinfo=UTC)],
    479                 transform=lambda d: d)
    480         self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
    481                 [datetime.datetime(2010, 12, 31, tzinfo=UTC),
    482                  datetime.datetime(2011, 1, 1, tzinfo=UTC)],
    483                 transform=lambda d: d)
     487        with timezone.override(EAT):
     488            self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
     489                    [datetime.datetime(2011, 1, 1, tzinfo=UTC)],
     490                    transform=lambda d: d)
     491            self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
     492                    [datetime.datetime(2011, 1, 1, tzinfo=UTC)],
     493                    transform=lambda d: d)
     494            self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
     495                    [datetime.datetime(2011, 1, 1, tzinfo=UTC)],
     496                    transform=lambda d: d)
     497        with timezone.override(UTC):
     498            self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
     499                    [datetime.datetime(2010, 1, 1, tzinfo=UTC),
     500                    datetime.datetime(2011, 1, 1, tzinfo=UTC)],
     501                    transform=lambda d: d)
     502            self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
     503                    [datetime.datetime(2010, 12, 1, tzinfo=UTC),
     504                    datetime.datetime(2011, 1, 1, tzinfo=UTC)],
     505                    transform=lambda d: d)
     506            self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
     507                    [datetime.datetime(2010, 12, 31, tzinfo=UTC),
     508                    datetime.datetime(2011, 1, 1, tzinfo=UTC)],
     509                    transform=lambda d: d)
    484510
    485511    def test_raw_sql(self):
    486512        # Regression test for #17755
Back to Top