Skip to content

Feature/ses enrollment email templates#294

Open
nthriveni-sonata-ship-it wants to merge 4 commits into
edx:release-ulmofrom
nthriveni-sonata-ship-it:feature/ses-enrollment-email-templates
Open

Feature/ses enrollment email templates#294
nthriveni-sonata-ship-it wants to merge 4 commits into
edx:release-ulmofrom
nthriveni-sonata-ship-it:feature/ses-enrollment-email-templates

Conversation

@nthriveni-sonata-ship-it

@nthriveni-sonata-ship-it nthriveni-sonata-ship-it commented May 15, 2026

Copy link
Copy Markdown
Member

Context

Course enrollment emails are currently delivered through Braze Canvas. The backend sends enrollment metadata and Braze handles template rendering, localization, and delivery.

This PR introduces the backend infrastructure required to migrate course enrollment emails to AWS SES while maintaining Braze as an automatic fallback mechanism. The migration is controlled through a Waffle flag, allowing gradual rollout and immediate rollback if needed.

What This PR Contains

SES Enrollment Email Routing

Introduces SES as the primary delivery path for course enrollment emails when the student.enable_ses_for_course_enrollment Waffle flag is enabled.

The enrollment email workflow now:

  • Routes enrollment emails through SES when enabled
  • Falls back automatically to Braze if SES delivery fails
  • Prevents duplicate sends between SES and Braze
  • Supports retry logic with exponential backoff (up to 3 attempts)

Language-Aware Template Selection

Adds language-based template resolution using user language preferences:

  • English users → enrollment_en.html
  • Spanish users → enrollment_es.html
  • Defaults to English when no supported preference is found

Theme-First Template Resolution

Introduces support for a dedicated enrollment email theme override using:

SES_ENROLLMENT_EMAIL_THEME

Template resolution order:

  1. Theme-specific enrollment template (configured via SES_ENROLLMENT_EMAIL_THEME)
  2. Platform enrollment template fallback

This allows enrollment emails to use branded templates from edx-themes while preserving platform templates as a safe fallback.

SES Email Rendering Support

Adds support for:

  • HTML email rendering via Django templates
  • Absolute image URL generation for email client compatibility
  • Enrollment-specific template context generation
  • English and Spanish enrollment email templates and partials

Validation Tooling

Adds a management command:

./manage.py lms test_enrollment_email_ses

to support post-deployment validation of:

  • Template rendering
  • Language selection
  • Image URL generation
  • End-to-end enrollment email flow

Configuration

Adds the following setting in openedx/envs/common.py:

SES_ENROLLMENT_EMAIL_THEME = None

This setting provides a dedicated theme override for course enrollment emails sent through the SES workflow.

By default, the value is None, which preserves existing behavior and allows enrollment emails to fall back to platform templates. Deployment environments can override this setting through edx-internal configuration to point enrollment emails to a specific theme (for example, edx.org-next) without affecting template resolution for other email types.

This approach was chosen to:

  • Keep enrollment email theming isolated from global site theming configuration.
  • Allow enrollment emails to use branded templates from edx-themes.
  • Preserve platform templates as a fallback if themed templates are unavailable.
  • Avoid changing DEFAULT_SITE_THEME, which could impact unrelated email workflows.
  • Enable environment-specific theme selection through configuration rather than code changes.

Testing

Automated Coverage

Added unit tests covering:

  • SES routing when Waffle flag is enabled
  • Braze routing when Waffle flag is disabled
  • SES failure → Braze fallback behavior
  • Retry behavior when both delivery paths fail
  • Language selection (English and Spanish)
  • Template selection correctness
  • Duplicate prevention between SES and Braze

Manual Validation

Validated:

  • English template rendering
  • Spanish template rendering
  • Theme-first template resolution
  • Platform fallback template resolution
  • Language preference selection
  • Image URL generation
  • SES rendering path
  • Braze fallback path

Rollout Plan

Deployment is designed for gradual activation:

  1. Deploy code with Waffle flag disabled (default behavior unchanged)
  2. Deploy enrollment email theme assets
  3. Enable Waffle flag incrementally
  4. Monitor SES delivery and fallback metrics
  5. Increase rollout percentage until full migration

Rollback requires only disabling the Waffle flag, which immediately restores the existing Braze-only workflow.

Safety Considerations

  • Waffle flag disabled by default
  • Automatic Braze fallback on SES failures
  • No impact to non-enrollment email workflows
  • Backward compatible deployment
  • Platform templates remain available as fallback
  • Simple rollback path through Waffle configuration

Comment thread common/djangoapps/student/tasks.py Outdated
Build absolute URLs for enrollment email images.

This function constructs full image URLs for SES email delivery. When Braze renders
the email through SES, it needs absolute URLs because email clients cannot resolve

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sort of confused by the explanation here, since Braze shouldn't be involved in triggering an email to SES. Can you clarify what you mean by "When Braze renders the email through SES"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the wording there is incorrect.
What I intended to convey is that email clients require fully qualified image URLs since relative/static asset paths are not resolvable outside the LMS environment.
I’ll update the comment to make that clearer and remove the Braze reference.

@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/ses-enrollment-email-templates branch 2 times, most recently from abf07ab to d96e195 Compare May 19, 2026 04:11
@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/ses-enrollment-email-templates branch from d96e195 to f571e8a Compare May 21, 2026 14:29
@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/ses-enrollment-email-templates branch 2 times, most recently from fca1dad to dc9a002 Compare June 4, 2026 09:36
@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/ses-enrollment-email-templates branch from dc9a002 to 4b4e580 Compare June 4, 2026 12:06
@nthriveni-sonata-ship-it nthriveni-sonata-ship-it marked this pull request as ready for review June 4, 2026 13:32
Copilot AI review requested due to automatic review settings June 4, 2026 13:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a backend-controlled migration path for course enrollment emails from Braze Canvas rendering/delivery to Django-rendered templates delivered via AWS SES, guarded by a Waffle flag and with automatic Braze fallback on SES failures. It adds English/Spanish HTML templates (plus partials), image URL context generation, SES/Braze routing logic in the enrollment email task, unit tests for routing behavior, and a management command intended to validate rendering and delivery.

Changes:

  • Add SES-first routing for course enrollment emails behind student.enable_ses_for_course_enrollment, with Braze fallback and retry behavior.
  • Add Django HTML templates for enrollment emails (EN/ES) plus header/footer partials and supporting image URL context.
  • Add operational tooling/config scaffolding: new setting SES_ENROLLMENT_EMAIL_THEME, devstack notes, and a test_enrollment_email_ses management command.

Reviewed changes

Copilot reviewed 12 out of 35 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
openedx/envs/common.py Adds SES_ENROLLMENT_EMAIL_THEME setting documentation.
lms/envs/devstack.py Adds commented devstack guidance for testing comprehensive theming and SES enrollment templates.
lms/envs/common.py Documents a Braze canvas ID setting for enrollment emails (commented for local testing).
common/djangoapps/student/tasks.py Implements SES/Braze routing, language selection, image URL context builder, and SES email rendering/sending.
common/djangoapps/student/tests/test_tasks.py Adds/extends unit tests for SES/Braze routing, fallback, retry, and language/template selection.
common/djangoapps/student/management/commands/test_enrollment_email_ses.py Adds management command for post-deploy validation of SES migration.
common/djangoapps/student/templates/emails/enrollment_en.html Adds English Django-rendered enrollment email template.
common/djangoapps/student/templates/emails/enrollment_es.html Adds Spanish Django-rendered enrollment email template.
common/djangoapps/student/templates/emails/partials/header_en.html Adds English header partial.
common/djangoapps/student/templates/emails/partials/header_es.html Adds Spanish header partial.
common/djangoapps/student/templates/emails/partials/footer_en.html Adds English footer partial.
common/djangoapps/student/templates/emails/partials/footer_es.html Adds Spanish footer partial.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +92
# Use SES-specific theme if configured, otherwise use default theme resolution
ses_theme = getattr(settings, 'SES_ENROLLMENT_EMAIL_THEME', None)
if ses_theme:
# Attempt to load from SES-specific theme (e.g., "edx.org-next")
template_name = f"{ses_theme}/emails/enrollment_{language}.html"
else:
# Use default theme resolution (DEFAULT_SITE_THEME or platform default)
template_name = f"emails/enrollment_{language}.html"

Comment on lines +356 to +358
site = Site.objects.get_current()
with emulate_http_request(site=site, user=user):
_send_ses_enrollment_email(user, canvas_entry_properties, language=enrollment_language)
Comment on lines +223 to +225
'calendar_icon_es': f"{lms_root}/static/images/enrollment_email/calendar_icon_es.png",
'community_icon_es': f"{lms_root}/static/images/enrollment_email/community_icon_es.png",
'slant_line_es': f"{lms_root}/static/images/enrollment_email/slant_line_es.png",
Comment on lines +1365 to +1366
<td width="33" align="left" valign="middle">
<img src="{{ pink_flag_icon_image }}" width="33" height="33" alt="Casual Icon" style="display: block; border: 0;">
Comment on lines +1386 to +1387
<td width="33" align="left" valign="middle">
<img src="{{ black_flag_icon_image }}" width="33" height="33" alt="Regular Icon" style="display: block; border: 0;">
Comment on lines +1407 to +1408
<td width="33" align="left" valign="middle">
<img src="{{ orange_flag_image }}" width="33" height="33" alt="Intense Icon" style="display: block; border: 0;">
Comment on lines +150 to +156
template_path = f'emails/enrollment_{language}.html'
context = {
'course_name': 'Test Course',
'course_url': 'https://example.com/courses',
'course_run_key': 'course-v1:test+course+2024',
'platform_name': 'Test Platform',
}
Comment thread openedx/envs/common.py
Comment on lines +1997 to +2001
# When set, enrollment emails rendered via SES use this theme instead of DEFAULT_SITE_THEME.
# Allows per-email-type theme customization without affecting other emails (e.g., "edx.org-next").
# Only effective if ENABLE_COMPREHENSIVE_THEMING is True.
# .. setting_warning: Must point to a valid theme in COMPREHENSIVE_THEME_DIRS or edx-platform/themes.
# If the theme does not exist or template is not found, falls back to DEFAULT_SITE_THEME resolution.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nthriveni-sonata-ship-it verify if this suggestion is true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jason, for this one I feel what I mentioned is clearer for someone to understand

Comment on lines +111 to +114
© {{ current_year|default:'' }} edX LLC All rights reserved.<br />
<br />
<a href="{{preference_center_url}}" style="color:#000001;text-decoration:underline;" target="_blank"><span style="color:#000001;">Update Your Preferences</span></a> / <a href="{{ unsubscribe_url }}" target="_blank" style="color:#000001;text-decoration:underline;"><span style="color:#000001;">Unsubscribe</span></a>
<br><br>
Comment on lines +110 to +113
© {{ current_year|default:'' }} edX LLC Todos los derechos reservados.<br />
<br />
<a href="{{preference_center_url}}" style="color:#000001;text-decoration:underline;" target="_blank"><span style="color:#000001;">Actualiza tus preferencias</span></a> / <a href="{{unsubscribe_url}}" style="color:#000001;text-decoration:underline;" target="_blank"><span style="color:#000001;">Salir de la lista</span></a><br />
<br />
Comment thread lms/envs/devstack.py
#
# To test the complete flow:
# 1. Uncomment the line below
# 2. Run: python manage.py lms shell -c "from django.template import engines; ..."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you clarify what this step actually means? am I just running python manage.py lms shell -c?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intended as a local validation step to verify theme-based template resolution after enabling the theme configuration. The command would render a template and confirm whether Django was loading it from edx-themes or falling back to edx-platform. Since this is only local testing guidance and not part of the implementation, I'll remove it from the committed configuration.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes were added while validating whether the enrollment email templates could be resolved from edx-themes during local development. They are not required for the enrollment email implementation itself. After reviewing the existing theming documentation, it looks like theme configuration is already handled through deployment configuration and local overrides, so these changes don't need to be part of this PR. I'll remove them and continue the edx-themes investigation in the follow-up ticket.

Comment thread lms/envs/devstack.py
# ENABLE_COMPREHENSIVE_THEMING = True
# DEFAULT_SITE_THEME = 'edx.org' # Base theme for resolution
# COMPREHENSIVE_THEME_DIRS = [
# "/edx/app/edxapp/edx-platform/themes/"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did this need to be fixed? was it broken this whole time?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also just by looking at this doc, I wonder if the theming using edx-themes is already handled in deployment. And then there is a separate doc for handling themes in dev. Correct me if I'm wrong here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enrollment template implementation itself doesn't depend on these theming configuration changes. These changes were added while I was investigating whether the templates could be moved to edx-themes and resolved correctly during local development. This wasn't addressing a known issue in production, and after reviewing the theming documentation you linked, it looks like deployment already handles theme resolution and local development has a documented approach via theme configuration overrides. Based on that, I don't think these changes belong in this PR and I'll remove them.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 35 changed files in this pull request and generated 12 comments.

Comment on lines +84 to +99
# Use SES-specific theme if configured, otherwise use default theme resolution
ses_theme = getattr(settings, 'SES_ENROLLMENT_EMAIL_THEME', None)
if ses_theme:
# Attempt to load from SES-specific theme (e.g., "edx.org-next")
template_name = f"{ses_theme}/emails/enrollment_{language}.html"
else:
# Use default theme resolution (DEFAULT_SITE_THEME or platform default)
template_name = f"emails/enrollment_{language}.html"

# Work on a copy so we don't mutate the caller's dict
ctx = dict(context)
# Inject SES-specific absolute image URLs (Braze ignores these keys)
ctx.update(_build_enrollment_email_image_urls(language=language))

html_body = render_to_string(template_name, ctx)
plain_text_body = strip_tags(html_body).strip()
Comment on lines +148 to +156
recipients = [{"external_user_id": user_id}]
braze_client = get_email_client()
if braze_client:
braze_client.send_canvas_message(
canvas_id=getattr(settings, 'BRAZE_COURSE_ENROLLMENT_CANVAS_ID', None),
recipients=recipients,
canvas_entry_properties=canvas_entry_properties,
)

Comment on lines +374 to +375
countdown = 60 * (self.request.retries + 1)
raise self.retry(exc=braze_exc, countdown=countdown, max_retries=MAX_RETRIES)
Comment on lines +387 to +388
countdown = 60 * (self.request.retries + 1)
raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
Comment on lines +1328 to +1330
<td width="24" valign="middle">
<img width="20" height="20" class="community-btn-img" src="{{ forum_icon_es }}" />
</td>
Comment on lines +1406 to +1409
<td width="12" class="width_12"> </td>
<td width="33" align="left" valign="middle">
<img src="{{ orange_flag_image }}" width="33" height="33" alt="Intense Icon" style="display: block; border: 0;">
</td>
Comment on lines +149 to +157
with emulate_http_request(site=site, user=user):
template_path = f'emails/enrollment_{language}.html'
context = {
'course_name': 'Test Course',
'course_url': 'https://example.com/courses',
'course_run_key': 'course-v1:test+course+2024',
'platform_name': 'Test Platform',
}
html_body = render_to_string(template_path, context)
Comment on lines +9 to +11
<table cellpadding="0" cellspacing="0">
<tr><td align="left" valign="top"><a href="https://www.edx.org/es/" target="_blank"><img src="https://email-media.s3.amazonaws.com/edX/2021/edX_logo.png" width="112" border="0" style="display:block;height:auto;" alt="Ir a la pagina principal de edX" /></a></td></tr>
</table>
Comment on lines +267 to +290
# Build test context
context = {
'course_name': 'Test Course (SES Migration)',
'course_url': 'https://example.com/courses/test',
'course_run_key': 'course-v1:test+course+2024',
'platform_name': configuration_helpers.get_value('platform_name', 'edX'),
'support_email': 'support@example.com',
'user_email': test_email_to,
}

# Add image URLs for SES
image_urls = _build_enrollment_email_image_urls('en')
context.update(image_urls)

# Render template
site = Site.objects.get_current()
with emulate_http_request(site=site, user=user):
template_path = 'emails/enrollment_en.html'
html_body = render_to_string(template_path, context)

# Send via Django email (this requires EMAIL_BACKEND to be configured)
from django.core.mail import EmailMultiAlternatives
subject = f'You are now enrolled in {context["course_name"]}'
text_body = f'You have been enrolled in {context["course_name"]}'
Comment on lines +523 to +526
</td>
<td class="td-display-block">
<h2 class="heading text" style="color: #f0cc00;">{{ user_name|default:"Restless Learner" }}!</h2>
</td>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants