mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
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:
@@ -1,5 +1,8 @@
|
|||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail.backends.base import BaseEmailBackend
|
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 ..exceptions import AnymailError, AnymailUnsupportedFeature
|
||||||
from ..utils import Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting
|
from ..utils import Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting
|
||||||
@@ -191,7 +194,7 @@ class BasePayload(object):
|
|||||||
anymail_message_attrs = (
|
anymail_message_attrs = (
|
||||||
# Anymail expando-props
|
# Anymail expando-props
|
||||||
('metadata', combine, None),
|
('metadata', combine, None),
|
||||||
('send_at', last, None), # normalize to datetime?
|
('send_at', last, 'aware_datetime'),
|
||||||
('tags', combine, None),
|
('tags', combine, None),
|
||||||
('track_clicks', last, None),
|
('track_clicks', last, None),
|
||||||
('track_opens', last, None),
|
('track_opens', last, None),
|
||||||
@@ -219,6 +222,7 @@ class BasePayload(object):
|
|||||||
if not callable(converter):
|
if not callable(converter):
|
||||||
converter = getattr(self, converter)
|
converter = getattr(self, converter)
|
||||||
value = converter(value)
|
value = converter(value)
|
||||||
|
if value is not UNSET:
|
||||||
if attr == 'body':
|
if attr == 'body':
|
||||||
setter = self.set_html_body if message.content_subtype == 'html' else self.set_text_body
|
setter = self.set_html_body if message.content_subtype == 'html' else self.set_text_body
|
||||||
else:
|
else:
|
||||||
@@ -246,6 +250,29 @@ class BasePayload(object):
|
|||||||
str_encoding = self.message.encoding or settings.DEFAULT_CHARSET
|
str_encoding = self.message.encoding or settings.DEFAULT_CHARSET
|
||||||
return [Attachment(attachment, str_encoding) for attachment in attachments]
|
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
|
# Abstract implementation
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -57,21 +57,15 @@ def _expand_merge_vars(vardict):
|
|||||||
|
|
||||||
|
|
||||||
def encode_date_for_mandrill(dt):
|
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"
|
Mandrill expects "YYYY-MM-DD HH:MM:SS" in UTC
|
||||||
converted to UTC, if timezone-aware
|
|
||||||
microseconds removed
|
|
||||||
date becomes "YYYY-MM-DD 00:00:00"
|
|
||||||
anything else gets returned intact
|
|
||||||
"""
|
"""
|
||||||
if isinstance(dt, datetime):
|
if isinstance(dt, datetime):
|
||||||
dt = dt.replace(microsecond=0)
|
dt = dt.replace(microsecond=0)
|
||||||
if dt.utcoffset() is not None:
|
if dt.utcoffset() is not None:
|
||||||
dt = (dt - dt.utcoffset()).replace(tzinfo=None)
|
dt = (dt - dt.utcoffset()).replace(tzinfo=None)
|
||||||
return dt.isoformat(' ')
|
return dt.isoformat(' ')
|
||||||
elif isinstance(dt, date):
|
|
||||||
return dt.isoformat() + ' 00:00:00'
|
|
||||||
else:
|
else:
|
||||||
return dt
|
return dt
|
||||||
|
|
||||||
@@ -128,9 +122,10 @@ class MandrillPayload(RequestsPayload):
|
|||||||
def add_alternative(self, content, mimetype):
|
def add_alternative(self, content, mimetype):
|
||||||
if mimetype != 'text/html':
|
if mimetype != 'text/html':
|
||||||
self.unsupported_feature("alternative part with mimetype '%s'" % mimetype)
|
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.unsupported_feature("multiple html parts")
|
||||||
self.data["message"]["html"] = content
|
else:
|
||||||
|
self.set_html_body(content)
|
||||||
|
|
||||||
def add_attachment(self, attachment):
|
def add_attachment(self, attachment):
|
||||||
key = "images" if attachment.inline else "attachments"
|
key = "images" if attachment.inline else "attachments"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import re
|
|||||||
import six
|
import six
|
||||||
import unittest
|
import unittest
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from datetime import date, datetime, timedelta, tzinfo
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
from email.mime.image import MIMEImage
|
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.core.mail import make_msgid
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import override_settings
|
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,
|
from anymail.exceptions import (AnymailAPIError, AnymailRecipientsRefused,
|
||||||
AnymailSerializationError, AnymailUnsupportedFeature)
|
AnymailSerializationError, AnymailUnsupportedFeature)
|
||||||
@@ -451,34 +452,40 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def test_send_at(self):
|
def test_send_at(self):
|
||||||
# String passed unchanged
|
utc_plus_6 = get_fixed_timezone(6 * 60)
|
||||||
|
utc_minus_8 = get_fixed_timezone(-8 * 60)
|
||||||
|
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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_at = "2013-11-12 01:02:03"
|
||||||
self.message.send()
|
self.message.send()
|
||||||
data = self.get_api_call_data()
|
data = self.get_api_call_data()
|
||||||
self.assertEqual(data['send_at'], "2013-11-12 01:02:03")
|
self.assertEqual(data['send_at'], "2013-11-12 01:02:03")
|
||||||
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
# Timezone-aware datetime converted to UTC:
|
|
||||||
class GMTminus8(tzinfo):
|
|
||||||
def utcoffset(self, dt): return timedelta(hours=-8)
|
|
||||||
def dst(self, dt): return timedelta(0)
|
|
||||||
|
|
||||||
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 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")
|
|
||||||
|
|
||||||
def test_default_omits_options(self):
|
def test_default_omits_options(self):
|
||||||
"""Make sure by default we don't send any Mandrill-specific options.
|
"""Make sure by default we don't send any Mandrill-specific options.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user