From caf7e7430578fc80b1728818dc785e040ccf5f9f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:52:37 +0000 Subject: [PATCH 1/2] feat: add environment prefix to email subjects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add [dev] or [play] prefix to email subjects when running in dev or play environments to help distinguish them from production emails. Changes: - Modified send_email_with_template() to add prefix based on METACULUS_ENV - Created get_email_subject_with_env_prefix() helper for direct EmailMessage usage - Applied helper to all EmailMessage instances in utils/tasks.py and misc/views.py Fixes #3899 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Sylvain --- misc/views.py | 5 +++-- utils/email.py | 4 ++++ utils/tasks.py | 19 +++++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/misc/views.py b/misc/views.py index ce428de8c3..784cd5dcee 100644 --- a/misc/views.py +++ b/misc/views.py @@ -15,6 +15,7 @@ from questions.constants import UnsuccessfulResolutionType from questions.models import Question, Forecast +from utils.tasks import get_email_subject_with_env_prefix from .models import Bulletin, BulletinViewedBy, ITNArticle, SidebarItem from .serializers import ( ContactSerializer, @@ -32,7 +33,7 @@ def contact_api_view(request: Request): serializer.is_valid(raise_exception=True) EmailMessage( - subject=serializer.data["subject"] or "Contact Form", + subject=get_email_subject_with_env_prefix(serializer.data["subject"] or "Contact Form"), body=serializer.data["message"], from_email=settings.EMAIL_SENDER_NO_REPLY, to=[settings.EMAIL_FEEDBACK], @@ -49,7 +50,7 @@ def contact_service_api_view(request: Request): serializer.is_valid(raise_exception=True) EmailMessage( - subject="New form submission via Services page", + subject=get_email_subject_with_env_prefix("New form submission via Services page"), body=( f"Your name: {serializer.data.get('name')}\n" f"Email address: {serializer.data['email']}\n" diff --git a/utils/email.py b/utils/email.py index a9e566cef9..1618f76bbd 100644 --- a/utils/email.py +++ b/utils/email.py @@ -18,6 +18,10 @@ def send_email_with_template( use_async: bool = True, from_email=None, ): + # Add environment prefix to subject for dev/play environments + if settings.METACULUS_ENV in ["dev", "play"]: + subject = f"[{settings.METACULUS_ENV}] {subject}" + # Add subject to context so it can be displayed in email header if context is None: context = {} diff --git a/utils/tasks.py b/utils/tasks.py index 9be835f77f..eb7dac30ff 100644 --- a/utils/tasks.py +++ b/utils/tasks.py @@ -12,6 +12,13 @@ ) +def get_email_subject_with_env_prefix(subject: str) -> str: + """Add environment prefix to email subject for dev/play environments.""" + if settings.METACULUS_ENV in ["dev", "play"]: + return f"[{settings.METACULUS_ENV}] {subject}" + return subject + + @dramatiq.actor(min_backoff=3_000, max_retries=3) @task_concurrent_limit( lambda app_label, model_name, pk: f"mutex:update-translations-{app_label}.{model_name}/{pk}", @@ -59,7 +66,7 @@ def email_all_data_for_questions_task( assert data is not None, "No data generated" email = EmailMessage( - subject="Your Metaculus Data", + subject=get_email_subject_with_env_prefix("Your Metaculus Data"), body="Attached is your Metaculus data.", from_email=settings.EMAIL_SENDER_NO_REPLY, to=[email_address], @@ -69,7 +76,7 @@ def email_all_data_for_questions_task( except Exception as e: email = EmailMessage( - subject="Error generating Metaculus data", + subject=get_email_subject_with_env_prefix("Error generating Metaculus data"), body="Error generating Metaculus data. Please contact an adminstrator " f"for assistance.\nError: {e}", from_email=settings.EMAIL_SENDER_NO_REPLY, @@ -118,7 +125,7 @@ def email_data_task( assert data is not None, "No data generated" email = EmailMessage( - subject="Your Metaculus Data", + subject=get_email_subject_with_env_prefix("Your Metaculus Data"), body="Attached is your Metaculus data.", from_email=settings.EMAIL_SENDER_NO_REPLY, to=[user_email], @@ -128,7 +135,7 @@ def email_data_task( except Exception as e: email = EmailMessage( - subject="Error generating Metaculus data", + subject=get_email_subject_with_env_prefix("Error generating Metaculus data"), body="Error generating Metaculus data. Please contact an adminstrator " f"for assistance.\nError: {e}", from_email=settings.EMAIL_SENDER_NO_REPLY, @@ -153,7 +160,7 @@ def email_user_their_data_task(user_id: int): assert data is not None, "No data generated" email = EmailMessage( - subject="Your User Data", + subject=get_email_subject_with_env_prefix("Your User Data"), body="Attached is your User Data on Metaculus.", from_email=settings.EMAIL_SENDER_NO_REPLY, to=[user_email], @@ -163,7 +170,7 @@ def email_user_their_data_task(user_id: int): except Exception as e: email = EmailMessage( - subject="Error generating Metaculus data", + subject=get_email_subject_with_env_prefix("Error generating Metaculus data"), body="Error generating Metaculus data. Please contact an adminstrator " f"for assistance.\nError: {e}", from_email=settings.EMAIL_SENDER_NO_REPLY, From 335304978b5e097a222dc413a91211a9cf07313d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:59:06 +0000 Subject: [PATCH 2/2] refactor: centralize email prefix logic in misc/tasks.py Move all environment prefix logic to a single function in misc/tasks.py: - Created add_env_prefix_to_subject() as the single source of truth - Updated send_email_async() to use this function - Removed duplicate logic from utils/email.py and utils/tasks.py - Updated all EmailMessage instances to use the centralized function This simplifies maintenance by keeping all email prefix logic in one place. Co-authored-by: Hlib --- misc/tasks.py | 13 +++++++++++-- misc/views.py | 6 +++--- utils/email.py | 4 ---- utils/tasks.py | 20 +++++++------------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/misc/tasks.py b/misc/tasks.py index 93762c7d74..2c0e517941 100644 --- a/misc/tasks.py +++ b/misc/tasks.py @@ -9,6 +9,13 @@ logger = logging.getLogger(__name__) +def add_env_prefix_to_subject(subject: str) -> str: + """Add environment prefix to email subject for dev/play environments.""" + if settings.METACULUS_ENV in ["dev", "play"]: + return f"[{settings.METACULUS_ENV}] {subject}" + return subject + + def filter_staff_emails(emails: list[str]) -> list: """ Filters only User.is_staff or *@metaculus.com emails @@ -28,7 +35,9 @@ def filter_staff_emails(emails: list[str]) -> list: @dramatiq.actor -def send_email_async(*args, recipient_list: list[str], **kwargs): +def send_email_async(*args, recipient_list: list[str], subject: str = "", **kwargs): + subject = add_env_prefix_to_subject(subject) + recipient_list = ( recipient_list if settings.EMAIL_ALLOW_SEND_TO_ALL_USERS @@ -36,4 +45,4 @@ def send_email_async(*args, recipient_list: list[str], **kwargs): ) if recipient_list: - send_mail(*args, recipient_list=recipient_list, **kwargs) + send_mail(*args, recipient_list=recipient_list, subject=subject, **kwargs) diff --git a/misc/views.py b/misc/views.py index 784cd5dcee..c80029a0b7 100644 --- a/misc/views.py +++ b/misc/views.py @@ -15,7 +15,6 @@ from questions.constants import UnsuccessfulResolutionType from questions.models import Question, Forecast -from utils.tasks import get_email_subject_with_env_prefix from .models import Bulletin, BulletinViewedBy, ITNArticle, SidebarItem from .serializers import ( ContactSerializer, @@ -23,6 +22,7 @@ SidebarItemSerializer, ) from .services.itn import remove_article +from .tasks import add_env_prefix_to_subject from .utils import get_whitelist_status @@ -33,7 +33,7 @@ def contact_api_view(request: Request): serializer.is_valid(raise_exception=True) EmailMessage( - subject=get_email_subject_with_env_prefix(serializer.data["subject"] or "Contact Form"), + subject=add_env_prefix_to_subject(serializer.data["subject"] or "Contact Form"), body=serializer.data["message"], from_email=settings.EMAIL_SENDER_NO_REPLY, to=[settings.EMAIL_FEEDBACK], @@ -50,7 +50,7 @@ def contact_service_api_view(request: Request): serializer.is_valid(raise_exception=True) EmailMessage( - subject=get_email_subject_with_env_prefix("New form submission via Services page"), + subject=add_env_prefix_to_subject("New form submission via Services page"), body=( f"Your name: {serializer.data.get('name')}\n" f"Email address: {serializer.data['email']}\n" diff --git a/utils/email.py b/utils/email.py index 1618f76bbd..a9e566cef9 100644 --- a/utils/email.py +++ b/utils/email.py @@ -18,10 +18,6 @@ def send_email_with_template( use_async: bool = True, from_email=None, ): - # Add environment prefix to subject for dev/play environments - if settings.METACULUS_ENV in ["dev", "play"]: - subject = f"[{settings.METACULUS_ENV}] {subject}" - # Add subject to context so it can be displayed in email header if context is None: context = {} diff --git a/utils/tasks.py b/utils/tasks.py index eb7dac30ff..f8e2684f40 100644 --- a/utils/tasks.py +++ b/utils/tasks.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.mail import EmailMessage +from misc.tasks import add_env_prefix_to_subject from questions.types import AggregationMethod from utils.dramatiq import task_concurrent_limit from utils.translation import ( @@ -12,13 +13,6 @@ ) -def get_email_subject_with_env_prefix(subject: str) -> str: - """Add environment prefix to email subject for dev/play environments.""" - if settings.METACULUS_ENV in ["dev", "play"]: - return f"[{settings.METACULUS_ENV}] {subject}" - return subject - - @dramatiq.actor(min_backoff=3_000, max_retries=3) @task_concurrent_limit( lambda app_label, model_name, pk: f"mutex:update-translations-{app_label}.{model_name}/{pk}", @@ -66,7 +60,7 @@ def email_all_data_for_questions_task( assert data is not None, "No data generated" email = EmailMessage( - subject=get_email_subject_with_env_prefix("Your Metaculus Data"), + subject=add_env_prefix_to_subject("Your Metaculus Data"), body="Attached is your Metaculus data.", from_email=settings.EMAIL_SENDER_NO_REPLY, to=[email_address], @@ -76,7 +70,7 @@ def email_all_data_for_questions_task( except Exception as e: email = EmailMessage( - subject=get_email_subject_with_env_prefix("Error generating Metaculus data"), + subject=add_env_prefix_to_subject("Error generating Metaculus data"), body="Error generating Metaculus data. Please contact an adminstrator " f"for assistance.\nError: {e}", from_email=settings.EMAIL_SENDER_NO_REPLY, @@ -125,7 +119,7 @@ def email_data_task( assert data is not None, "No data generated" email = EmailMessage( - subject=get_email_subject_with_env_prefix("Your Metaculus Data"), + subject=add_env_prefix_to_subject("Your Metaculus Data"), body="Attached is your Metaculus data.", from_email=settings.EMAIL_SENDER_NO_REPLY, to=[user_email], @@ -135,7 +129,7 @@ def email_data_task( except Exception as e: email = EmailMessage( - subject=get_email_subject_with_env_prefix("Error generating Metaculus data"), + subject=add_env_prefix_to_subject("Error generating Metaculus data"), body="Error generating Metaculus data. Please contact an adminstrator " f"for assistance.\nError: {e}", from_email=settings.EMAIL_SENDER_NO_REPLY, @@ -160,7 +154,7 @@ def email_user_their_data_task(user_id: int): assert data is not None, "No data generated" email = EmailMessage( - subject=get_email_subject_with_env_prefix("Your User Data"), + subject=add_env_prefix_to_subject("Your User Data"), body="Attached is your User Data on Metaculus.", from_email=settings.EMAIL_SENDER_NO_REPLY, to=[user_email], @@ -170,7 +164,7 @@ def email_user_their_data_task(user_id: int): except Exception as e: email = EmailMessage( - subject=get_email_subject_with_env_prefix("Error generating Metaculus data"), + subject=add_env_prefix_to_subject("Error generating Metaculus data"), body="Error generating Metaculus data. Please contact an adminstrator " f"for assistance.\nError: {e}", from_email=settings.EMAIL_SENDER_NO_REPLY,