Normalize send_at date/datetime/timestamp in BasePayload.

Interpret dates and naive datetimes as Django's
current_timezone (rather than UTC like Djrill did).
This should be more likely to behave as expected
when running with a non-UTC TIME_ZONE setting.
This commit is contained in:
medmunds
2016-03-05 11:22:14 -08:00
parent 0a5bca1426
commit a6c0eb5974
3 changed files with 64 additions and 35 deletions

View File

@@ -1,5 +1,8 @@
from datetime import date, datetime
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
from django.utils.timezone import is_naive, get_current_timezone, make_aware, utc
from ..exceptions import AnymailError, AnymailUnsupportedFeature
from ..utils import Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting
@@ -191,7 +194,7 @@ class BasePayload(object):
anymail_message_attrs = (
# Anymail expando-props
('metadata', combine, None),
('send_at', last, None), # normalize to datetime?
('send_at', last, 'aware_datetime'),
('tags', combine, None),
('track_clicks', last, None),
('track_opens', last, None),
@@ -219,6 +222,7 @@ class BasePayload(object):
if not callable(converter):
converter = getattr(self, converter)
value = converter(value)
if value is not UNSET:
if attr == 'body':
setter = self.set_html_body if message.content_subtype == 'html' else self.set_text_body
else:
@@ -246,6 +250,29 @@ class BasePayload(object):
str_encoding = self.message.encoding or settings.DEFAULT_CHARSET
return [Attachment(attachment, str_encoding) for attachment in attachments]
def aware_datetime(self, value):
"""Converts a date or datetime or timestamp to an aware datetime.
Naive datetimes are assumed to be in Django's current_timezone.
Dates are interpreted as midnight that date, in Django's current_timezone.
Integers are interpreted as POSIX timestamps (which are inherently UTC).
Anything else (e.g., str) is returned unchanged, which won't be portable.
"""
if isinstance(value, datetime):
dt = value
else:
if isinstance(value, date):
dt = datetime(value.year, value.month, value.day) # naive, midnight
else:
try:
dt = datetime.utcfromtimestamp(value).replace(tzinfo=utc)
except (TypeError, ValueError):
return value
if is_naive(dt):
dt = make_aware(dt, get_current_timezone())
return dt
#
# Abstract implementation
#

View File

@@ -57,21 +57,15 @@ def _expand_merge_vars(vardict):
def encode_date_for_mandrill(dt):
"""Format a date or datetime for use as a Mandrill API date field
"""Format a datetime for use as a Mandrill API date field
datetime becomes "YYYY-MM-DD HH:MM:SS"
converted to UTC, if timezone-aware
microseconds removed
date becomes "YYYY-MM-DD 00:00:00"
anything else gets returned intact
Mandrill expects "YYYY-MM-DD HH:MM:SS" in UTC
"""
if isinstance(dt, datetime):
dt = dt.replace(microsecond=0)
if dt.utcoffset() is not None:
dt = (dt - dt.utcoffset()).replace(tzinfo=None)
return dt.isoformat(' ')
elif isinstance(dt, date):
return dt.isoformat() + ' 00:00:00'
else:
return dt
@@ -128,9 +122,10 @@ class MandrillPayload(RequestsPayload):
def add_alternative(self, content, mimetype):
if mimetype != 'text/html':
self.unsupported_feature("alternative part with mimetype '%s'" % mimetype)
if "html" in self.data["message"]:
elif "html" in self.data["message"]:
self.unsupported_feature("multiple html parts")
self.data["message"]["html"] = content
else:
self.set_html_body(content)
def add_attachment(self, attachment):
key = "images" if attachment.inline else "attachments"

View File

@@ -8,7 +8,7 @@ import re
import six
import unittest
from base64 import b64decode
from datetime import date, datetime, timedelta, tzinfo
from datetime import date, datetime
from decimal import Decimal
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
@@ -18,6 +18,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.mail import make_msgid
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
from anymail.exceptions import (AnymailAPIError, AnymailRecipientsRefused,
AnymailSerializationError, AnymailUnsupportedFeature)
@@ -451,33 +452,39 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
])
def test_send_at(self):
# String passed unchanged
self.message.send_at = "2013-11-12 01:02:03"
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2013-11-12 01:02:03")
utc_plus_6 = get_fixed_timezone(6 * 60)
utc_minus_8 = get_fixed_timezone(-8 * 60)
# Timezone-naive datetime assumed to be UTC
self.message.send_at = datetime(2022, 10, 11, 12, 13, 14, 567)
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2022-10-11 12:13:14")
with override_current_timezone(utc_plus_6):
# Timezone-naive datetime assumed to be Django current_timezone
self.message.send_at = datetime(2022, 10, 11, 12, 13, 14, 567)
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2022-10-11 06:13:14") # 12:13 UTC+6 == 06:13 UTC
# Timezone-aware datetime converted to UTC:
class GMTminus8(tzinfo):
def utcoffset(self, dt): return timedelta(hours=-8)
def dst(self, dt): return timedelta(0)
# Timezone-aware datetime converted to UTC:
self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=utc_minus_8)
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2016-03-04 13:06:07") # 05:06 UTC-8 == 13:06 UTC
self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=GMTminus8())
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2016-03-04 13:06:07")
# Date-only treated as midnight in current timezone
self.message.send_at = date(2022, 10, 22)
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2022-10-21 18:00:00") # 00:00 UTC+6 == 18:00-1d UTC
# Date-only treated as midnight UTC
self.message.send_at = date(2022, 10, 22)
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2022-10-22 00:00:00")
# POSIX timestamp
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2022-05-06 07:08:09")
# String passed unchanged (this is *not* portable between ESPs)
self.message.send_at = "2013-11-12 01:02:03"
self.message.send()
data = self.get_api_call_data()
self.assertEqual(data['send_at'], "2013-11-12 01:02:03")
def test_default_omits_options(self):
"""Make sure by default we don't send any Mandrill-specific options.