From 9a31362fa5ed16c8a1b7daf092bb5d944f4eafa8 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 4 Jun 2026 12:03:28 -0700 Subject: [PATCH] fix(reports): stamp email subject date at send time, not import time (#40693) Co-authored-by: Claude Opus 4.8 --- superset/reports/notifications/email.py | 16 +++++++++++--- .../reports/notifications/email_tests.py | 21 +++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) 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