Feature/ses enrollment email templates#294
Conversation
| 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 |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
abf07ab to
d96e195
Compare
…(no behavior change)
d96e195 to
f571e8a
Compare
fca1dad to
dc9a002
Compare
dc9a002 to
4b4e580
Compare
There was a problem hiding this comment.
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 atest_enrollment_email_sesmanagement 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.
| # 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" | ||
|
|
| site = Site.objects.get_current() | ||
| with emulate_http_request(site=site, user=user): | ||
| _send_ses_enrollment_email(user, canvas_entry_properties, language=enrollment_language) |
| '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", |
| <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;"> |
| <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;"> |
| <td width="33" align="left" valign="middle"> | ||
| <img src="{{ orange_flag_image }}" width="33" height="33" alt="Intense Icon" style="display: block; border: 0;"> |
| 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', | ||
| } |
| # 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. |
There was a problem hiding this comment.
@nthriveni-sonata-ship-it verify if this suggestion is true
There was a problem hiding this comment.
Jason, for this one I feel what I mentioned is clearer for someone to understand
| © {{ 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> |
| © {{ 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 /> |
| # | ||
| # To test the complete flow: | ||
| # 1. Uncomment the line below | ||
| # 2. Run: python manage.py lms shell -c "from django.template import engines; ..." |
There was a problem hiding this comment.
can you clarify what this step actually means? am I just running python manage.py lms shell -c?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| # ENABLE_COMPREHENSIVE_THEMING = True | ||
| # DEFAULT_SITE_THEME = 'edx.org' # Base theme for resolution | ||
| # COMPREHENSIVE_THEME_DIRS = [ | ||
| # "/edx/app/edxapp/edx-platform/themes/" |
There was a problem hiding this comment.
why did this need to be fixed? was it broken this whole time?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| # 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() |
| 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, | ||
| ) | ||
|
|
| countdown = 60 * (self.request.retries + 1) | ||
| raise self.retry(exc=braze_exc, countdown=countdown, max_retries=MAX_RETRIES) |
| countdown = 60 * (self.request.retries + 1) | ||
| raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES) |
| <td width="24" valign="middle"> | ||
| <img width="20" height="20" class="community-btn-img" src="{{ forum_icon_es }}" /> | ||
| </td> |
| <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> |
| 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) |
| <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> |
| # 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"]}' |
| </td> | ||
| <td class="td-display-block"> | ||
| <h2 class="heading text" style="color: #f0cc00;">{{ user_name|default:"Restless Learner" }}!</h2> | ||
| </td> |
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_enrollmentWaffle flag is enabled.The enrollment email workflow now:
Language-Aware Template Selection
Adds language-based template resolution using user language preferences:
enrollment_en.htmlenrollment_es.htmlTheme-First Template Resolution
Introduces support for a dedicated enrollment email theme override using:
SES_ENROLLMENT_EMAIL_THEMETemplate resolution order:
SES_ENROLLMENT_EMAIL_THEME)This allows enrollment emails to use branded templates from
edx-themeswhile preserving platform templates as a safe fallback.SES Email Rendering Support
Adds support for:
Validation Tooling
Adds a management command:
to support post-deployment validation of:
Configuration
Adds the following setting in
openedx/envs/common.py: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:
edx-themes.DEFAULT_SITE_THEME, which could impact unrelated email workflows.Testing
Automated Coverage
Added unit tests covering:
Manual Validation
Validated:
Rollout Plan
Deployment is designed for gradual activation:
Rollback requires only disabling the Waffle flag, which immediately restores the existing Braze-only workflow.
Safety Considerations