diff --git a/superset/reports/notifications/email.py b/superset/reports/notifications/email.py index 4c172afd791..b0485d21828 100644 --- a/superset/reports/notifications/email.py +++ b/superset/reports/notifications/email.py @@ -28,8 +28,8 @@ from pytz import timezone from superset import is_feature_enabled from superset.exceptions import SupersetErrorsException -from superset.reports.models import ReportRecipientType -from superset.reports.notifications.base import BaseNotification +from superset.reports.models import ReportRecipients, ReportRecipientType +from superset.reports.notifications.base import BaseNotification, NotificationContent from superset.reports.notifications.exceptions import NotificationError from superset.utils import json from superset.utils.core import HeaderDataType, send_email_smtp @@ -83,7 +83,17 @@ class EmailNotification(BaseNotification): # pylint: disable=too-few-public-met """ type = ReportRecipientType.EMAIL - now = datetime.now(timezone("UTC")) + + def __init__( + self, recipient: ReportRecipients, content: NotificationContent + ) -> None: + super().__init__(recipient, content) + # Stamp each notification with its own timestamp at construction, which + # happens per recipient immediately before the email is dispatched. The + # date rendered into the subject (when DATE_FORMAT_IN_EMAIL_SUBJECT is + # enabled) therefore tracks the dispatch time. A module- or class-level + # value would instead freeze on the first import in a long-running worker. + self.now = datetime.now(timezone("UTC")) @property def _name(self) -> str: diff --git a/tests/unit_tests/reports/notifications/email_tests.py b/tests/unit_tests/reports/notifications/email_tests.py index 54fb2d4b30e..b7dcc1df2d7 100644 --- a/tests/unit_tests/reports/notifications/email_tests.py +++ b/tests/unit_tests/reports/notifications/email_tests.py @@ -17,6 +17,7 @@ from datetime import datetime import pandas as pd +from freezegun import freeze_time from pytz import timezone from tests.unit_tests.conftest import with_feature_flags @@ -107,8 +108,6 @@ def test_email_subject_with_datetime() -> None: from superset.reports.notifications.base import NotificationContent from superset.reports.notifications.email import EmailNotification - now = datetime.now(timezone("UTC")) - datetime_pattern = "%Y-%m-%d" content = NotificationContent( @@ -130,8 +129,18 @@ def test_email_subject_with_datetime() -> None: "execution_id": "test-execution-id", }, ) - subject = EmailNotification( - recipient=ReportRecipients(type=ReportRecipientType.EMAIL), content=content - )._get_subject() + # Freeze the clock to a fixed, distinctive instant and construct the + # notification *under* the freeze. The subject date must reflect this + # frozen moment, which is only possible if the timestamp is stamped per + # instance at construction/send time. If the timestamp were a class + # attribute evaluated at import time (the regression this fixes), it would + # carry the real import-time date instead and this assertion would fail. + frozen_now = datetime(2021, 4, 22, 12, 0, 0, tzinfo=timezone("UTC")) + with freeze_time(frozen_now): + notification = EmailNotification( + recipient=ReportRecipients(type=ReportRecipientType.EMAIL), + content=content, + ) + subject = notification._get_subject() assert datetime_pattern not in subject - assert now.strftime(datetime_pattern) in subject + assert frozen_now.strftime(datetime_pattern) in subject