diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py index c13c5bf96bd8..ee5a7fc6aeae 100644 --- a/common/djangoapps/student/tasks.py +++ b/common/djangoapps/student/tasks.py @@ -2,6 +2,8 @@ Celery task for course enrollment email """ import logging +from datetime import datetime + from celery import shared_task from django.conf import settings from django.contrib.auth import get_user_model @@ -29,6 +31,76 @@ MAX_RETRIES = 3 +def _build_enrollment_email_image_urls(language='en'): + """ + 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 + relative paths or Django static file paths. + + Args: + language (str): Language code ('en' or 'es') to select correct image variants + + Returns: + dict: Mapping of image variable names to absolute URLs + + How it works: + 1. settings.LMS_ROOT_URL is evaluated at runtime (e.g., 'https://courses.edx.org') + 2. f-string interpolates this value into the path + 3. Email client receives complete URL: 'https://courses.edx.org/static/images/enrollment_email/...' + 4. Browser/email client can fetch image directly without context about Django + + Example flow: + - Python (settings): LMS_ROOT_URL = 'https://courses.edx.org' + - f-string: f"{settings.LMS_ROOT_URL}/static/images/enrollment_email/person_icon_{language}.png" + - Result: 'https://courses.edx.org/static/images/enrollment_email/person_icon_en.png' + - Email client: Makes HTTP GET to that URL, renders image + """ + lms_root = configuration_helpers.get_value( + "LMS_ROOT_URL", settings.LMS_ROOT_URL + ).rstrip('/') + + # NOTE: Image URLs for SES templates. + # Currently not used in Braze flow — will be enabled during SES migration. + # Construct image URLs based on language + image_urls = { + 'logo_url': f"{lms_root}/static/images/edx_logo.png", + 'you_are_enrolled_en': f"{lms_root}/static/images/enrollment_email/you_are_enrolled_en.png", + 'banner_default': f"{lms_root}/static/images/enrollment_email/banner_default.png", + 'timer_icon_en': f"{lms_root}/static/images/enrollment_email/timer_icon_en.png", + 'person_icon_en': f"{lms_root}/static/images/enrollment_email/person_icon_en.png", + 'dollar_icon_en': f"{lms_root}/static/images/enrollment_email/dollar_icon_en.png", + 'goal_idea_icon_en': f"{lms_root}/static/images/enrollment_email/goal_idea_icon_en.png", + 'flag_icon_pink_en': f"{lms_root}/static/images/enrollment_email/flag_icon_pink_en.png", + 'flag_icon_black_en_es': f"{lms_root}/static/images/enrollment_email/flag_icon_black_en_es.png", + 'flag_icon_orange_en': f"{lms_root}/static/images/enrollment_email/flag_icon_orange_en.png", + 'vertical_line_white_en': f"{lms_root}/static/images/enrollment_email/vertical_line_white_en.png", + 'vertical_line_orange_en': f"{lms_root}/static/images/enrollment_email/vertical_line_orange_en.png", + 'vertical_line_black_en': f"{lms_root}/static/images/enrollment_email/vertical_line_black_en.png", + 'community_illustration_en': f"{lms_root}/static/images/enrollment_email/community_illustration_en.png", + } + + # If Spanish, override with Spanish variants + if language == 'es': + spanish_overrides = { + 'banner_default': f"{lms_root}/static/images/enrollment_email/banner_default.png", # Same for both languages + 'arrow_icon_es': f"{lms_root}/static/images/enrollment_email/arrow_icon_es.png", + 'timer_icon_es': f"{lms_root}/static/images/enrollment_email/timer_icon_es.png", + 'person_icon_es': f"{lms_root}/static/images/enrollment_email/person_icon_es.png", + 'dollar_icon_es': f"{lms_root}/static/images/enrollment_email/dollar_icon_es.png", + 'flag_icon_white_es': f"{lms_root}/static/images/enrollment_email/flag_icon_white_es.png", + 'flag_icon_grey_es': f"{lms_root}/static/images/enrollment_email/flag_icon_grey_es.png", + 'flag_icon_black_en_es': f"{lms_root}/static/images/enrollment_email/flag_icon_black_en_es.png", # Same for both + '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", + } + image_urls.update(spanish_overrides) + + return image_urls + + @shared_task(bind=True, ignore_result=True) @set_code_owner_attribute def send_course_enrollment_email( @@ -64,13 +136,16 @@ def send_course_enrollment_email( "course_price": CourseMode.min_course_price_for_currency( course_id=course_id, currency="USD" ), + # Strip trailing slashes so template concatenation like + # "{{ lms_base_url }}/courses/..." never produces a double slash. "lms_base_url": configuration_helpers.get_value( "LMS_ROOT_URL", settings.LMS_ROOT_URL - ), + ).rstrip('/'), "learning_base_url": configuration_helpers.get_value( "LEARNING_MICROFRONTEND_URL", settings.LEARNING_MICROFRONTEND_URL - ), - "track_mode": track_mode + ).rstrip('/'), + "track_mode": track_mode, + "current_year": datetime.now().year, } try: @@ -86,9 +161,14 @@ def send_course_enrollment_email( { "course_date_blocks": course_date_blocks, "goals_enabled": ENABLE_COURSE_GOALS.is_enabled(course_key), + "user_name": user.get_full_name() or user.first_name or user.username, } ) + # Inject absolute image URLs for SES rendering. Braze ignores these extra + # keys; they're consumed only when the SES path is enabled. + canvas_entry_properties.update(_build_enrollment_email_image_urls(language='en')) + try: course_uuid = get_course_uuid_for_course(course_id) if course_uuid is None: diff --git a/common/djangoapps/student/templates/emails/enrollment_en.html b/common/djangoapps/student/templates/emails/enrollment_en.html new file mode 100644 index 000000000000..5a637c5e8198 --- /dev/null +++ b/common/djangoapps/student/templates/emails/enrollment_en.html @@ -0,0 +1,1745 @@ + + + + + + + + edX + + + + + + + + + + + +
+ + + + + +
+ + + {% include "emails/partials/header_en.html" %} + + + + + + + +
+ You're enrolled, Restless Learner! +
+ + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+ + + +
+ + {% if banner_image_url %} + Course Banner + {% else %} + Course Banner + {% endif %} + + {% if partner_image_url %} +
+ Partner Logo +
+ {% endif %} + +
+
+

{{ course_title }}

+

{{ org_name }}

+ +
+ + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + +
+

+ {{ org_name }} +

+
+

{{ course_title }}

+
+

+ {{ short_description }} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Duration + + Estimated {{ weeks_to_complete }} weeks
+ + {% if min_effort and max_effort %} + {{ min_effort }} - {{ max_effort }} hours per week + {% elif min_effort %} + {% if min_effort > 1 %} + {{ min_effort }} hours per week + {% else %} + {{ min_effort }} hour per week + {% endif %} + {% elif max_effort %} + {% if max_effort > 1 %} + {{ max_effort }} hours per week + {% else %} + {{ max_effort }} hour per week + {% endif %} + {% endif %} + +
+ Schedule + + {% if pacing_type == 'instructor_paced' %} + Instructor-led + {% else %} + Self-paced + {% endif %}
+ + {% if pacing_type == 'instructor_paced' %} + On a course schedule + {% else %} + Progress at your own speed + {% endif %} + +
+ Price + + {% if course_price == 0 %} + Free + {% else %} + ${{ course_price }} + {% endif %}
+ + {% if course_price == 0 %} + Optional upgrade available + {% endif %} + +
+ + + + + + + +
+ Start + my course +
+ +
+
+
+
+
+ + + + {% if instructors %} + + + + + + + + + + + + +
+ + + + + + + {% if not instructors|length|divisibleby:2 %} + {% with instructors.0 as first_instructor %} + + + + + + + {% endwith %} + + {% else %} + + + + + + + + + {% endif %} + +
+ {% if instructors|length == 1 %} + Meet your instructor: + {% else %} + Meet your instructors: + {% endif %} +
+ + + + +
+ + + + + + +
+ + + + + + + + +
+ {{ first_instructor.name }} +
+ {{ first_instructor.organization_name }} +
+
+
+
+ + + {% for instructor in instructors %} + {% if not forloop.first %} + + {% endif %} + {% endfor %} + +
+ + + + + +
+ + + + + + +
+ + + + + + + + + + +
+ {{ instructor.name }} +
+ {{ instructor.organization_name }} +
+
+
+
+
+ + + {% for instructor in instructors %} + + {% endfor %} + +
+ + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + +
+ {{ instructor.name }} +
+ {{ instructor.organization_name }} +
+
+
+
+
+
+ {% endif %} + + + + {% if goals_enabled %} + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + + +
+ Message Icon +
+
+ + + + + + +
+ Set your learning goal +
+
+
+
+ + + + + + +
+ + Setting a goal helps you find the motivation to keep up with coursework and + make it to the finish line. If we notice you're not quite at your goal, + we'll send you an email reminder. Set your goal today. + +
+
+ + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + +
+ Casual Icon + + + + + + + + + +
+ Casual
+
+ 1 day a week +
+
+
+
+ + + + + + + + + + + + +
+ Regular Icon + + + + + + + + + +
+ Regular
+
+ 3 days a week +
+
+
+
+ + + + + + + + + + + + +
+ Intense Icon + + + + + + + + + +
+ Intense
+
+ 5 days a week +
+
+
+
+
+
+
+
+
+ {% endif %} + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + +
+ Stay on track with your personalized course schedule +
+ Track important dates, assignments, and more. +
+ + + + + +
+ View + my schedule +
+
+
+ + + + {% if course_date_blocks|length > 0 and course_date_blocks.0.due_date %} + + + + + + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + +
+ Past due
+ + + + + + +
+ Vertical Line + + + + + + + + + +
+ {{ course_date_blocks.0.due_date }} +
+ {{ course_date_blocks.0.title }} +
+
+
+ Today
+ + + + + + +
+ Vertical Line + + + + + + + + + +
+ {% if course_date_blocks|length > 1 %} + {{ course_date_blocks.1.due_date }} + {% endif %} +
+ {% if course_date_blocks|length > 1 and course_date_blocks.1.title %} + {{ course_date_blocks.1.title }} + {% else %} + No assignments due today + {% endif %} +
+
+
+ Due next
+ + + + + + +
+ Vertical Line + + + + + + + + + +
+ {% if course_date_blocks|length > 2 and course_date_blocks.2.title %} + {{ course_date_blocks.2.due_date }} + {% endif %} +
+ {% if course_date_blocks|length > 2 and course_date_blocks.2.title %} + {{ course_date_blocks.2.title }} + {% else %} + No upcoming assignments + {% endif %} +
+
+
+
+
+ + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + +
+ Engage with a global community +
+ + + + + + + + +
+ Meet the + + {% if learners_count %}{{ learners_count }}{% else %}fellow{% endif %} + + learners in your course +
+
+ + Join the discussion + +
+
+ + + + +
+ Community illustration +
+
+
+
+
+ + + + {% include "emails/partials/footer_en.html" %} + + +
+ +
+ + + + \ No newline at end of file diff --git a/common/djangoapps/student/templates/emails/enrollment_es.html b/common/djangoapps/student/templates/emails/enrollment_es.html new file mode 100644 index 000000000000..063a1cfa1a67 --- /dev/null +++ b/common/djangoapps/student/templates/emails/enrollment_es.html @@ -0,0 +1,1393 @@ + + + + + + + + edX + + + + + + + + + + + + + +
+ + + + + +
+ + + {% include "emails/partials/header_es.html" %} + + + + + + + + +
+ + + + + + + + + + + + + + + + +
  + + + + + +
+

Te has inscrito, 

+
+

{{ user_name|default:"Restless Learner" }}!

+
+
 
  + + + + + + +
+ {% if banner_image_url %} +
+ + + {% if partner_image_url %} +
+ Partner Logo +
+ {% endif %} + +
+ {% else %} +
+ + {% if partner_image_url %} +
+ Partner Logo +
+ {% endif %} + +
+ {% endif %} +
+

+ {{course_title}} +

+
+
  + + + + + + + + +
+ + {{short_description}} + +
+ + + + +
+ + + + + + + + + +
+ Empieza mi curso + + +
+
+
+
+
+
 
+
+ + + + + + + + + +
+ + + {% if weeks_to_complete %} + + {% endif %} + + {% if track_mode != 'masters' %} + + {% endif %} + +
+ + + + + + +
+ + + + + + + + + +
+

+ Se estima tardar {{weeks_to_complete}} semanas +

+
+

+ {% if min_effort and max_effort %} + {{min_effort}} - {{max_effort}} horas por semana + {% elif min_effort %} + {% if min_effort > 1 %} + {{min_effort}} horas por semana + {% else %} + {{min_effort}} hora por semana + {% endif %} + {% elif max_effort %} + {% if max_effort > 1 %} + {{max_effort}} horas por semana + {% else %} + {{max_effort}} hora por semana + {% endif %} + {% endif %} +

+
+
+
+ + + + + + +
+ + + + + + + + + +
+

+ {% if pacing_type == 'instructor_paced' %} + Al ritmo del profesor + {% else %} + Al ritmo del estudiante + {% endif %} +

+
+

+ {% if pacing_type == 'instructor_paced' %} + Sigue un horario curricular + {% else %} + Avanza a tu propio ritmo + {% endif %} +

+
+
+
+ + + + + + +
+ + + {% if course_price == 0 %} + + + + + + + +
+

+ Gratuito +

+
+

+ Opción de mejorar tu subscripción +

+
+ {% else %} + + + + +
+

+ ${{course_price}} +

+
+ {% endif %} +
+
+
+ + + + + + {% if goals_enabled %} + + + + + + + + + + + + +
  + + + + + + + + + + + +
+

Marca tus

+
+

metas de aprendizaje

+
+

Marcar una meta te anima a terminar el curso. Siempre puedes cambiarla + más tarde. Si notamos que aún no has llegado a tu meta, te enviaremos un recordatorio por + correo electrónico.

+
+
  + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ Casual +

+
+

+ 1 día a la semana +

+
+
+
+ + + + + + + + + + +
+ + +

+ Regular +

+
+

+ 3 días a la semana +

+
+
+
+ + + + + + + + + + +
+ + +

+ Intenso +

+
+

+ 5 días a la semana +

+
+
+
+
 
+ {% endif %} + + + + + + {% if instructors %} + + + + + + + + + + + {% if not instructors|length|divisibleby:2 %} + {% with instructors.0 as first_instructor %} + + + + + + + + + + + + {% endwith %} + {% else %} + + + + + + + {% endif %} + +
  + {% if instructors|length == 1 %} +

Tu instructor

+ {% else %} +

Tus instructores

+ {% endif %} +
 
  + + + + + + + + + + + + + + +
+ +
+ {{first_instructor.name}} +
+ {{first_instructor.organization_name}} +
+
+
 
  + + {% for instructor in instructors %} + {% if not forloop.first %} + + + + {% endif %} + {% endfor %} +
+ + + + + + + + + + + + + + +
+ +
+ {{instructor.name}} +
+ {{instructor.organization_name}} +
+
+
+
 
  + + {% for instructor in instructors %} + + + + {% endfor %} +
+ + + + + + + + + + + + + + +
+ +
+ {{instructor.name}} +
+ {{instructor.organization_name}} +
+
+
+
 
+ {% endif %} + + + + + + {% if course_date_blocks %} + + + + + + + + + + + + + + + + + + +
  + + + + + + + + + + + +
+

Mantiene un buen ritmo

+
+

con tu horario de curso personalizado

+
+

Recibe recordatorios sobre fechas importantes, evaluaciones y más!

+
+
  + + {% for date in course_date_blocks %} + {% if forloop.first %} + + + + + + + + {% if not date.assignment_type %} + + {% else %} + + {% endif %} + + {% elif not forloop.last %} + + + + + + + + + {% if not date.assignment_type %} + + {% else %} + + {% endif %} + + {% else %} + + + + + + + + + {% if not date.assignment_type %} + + {% else %} + + {% endif %} + + {% endif %} + {% endfor %} +
+ + + + + + + + + + +
+
+
+
+
+
+
+
{{date.due_date}}
+ + + + +
+
+
+
+
+ {{date.title}} + + Hora límite de entrega de + {{date.assignment_type}}: + {{date.title}} + a las {{date.due_time}} + {% if date.assignment_count == 0 %} + {% else %} + +{{date.assignment_count}} + más + {% endif %} +
+ + + + + + + + + + +
+
+
+
+
+
+
+
{{date.due_date}}Hoy
+ + + + +
+
+
+
+
+ {{date.title}} + + Hora límite de entrega de + {{date.assignment_type}}: + {{date.title}} + a las {{date.due_time}} + {% if date.assignment_count == 0 %} + {% else %} + +{{date.assignment_count}} + más + {% endif %} +
+ + + + + + + + + + +
+
+
+
+
+
+
+
{{date.due_date}}Próxima entrega +
+ + + + +
+
+
+
+
+ {{date.title}} + + Hora límite de entrega de {{date.assignment_type}}: + {{date.title}} + a las {{date.due_time}} + {% if date.assignment_count == 0 %} + {% else %} + +{{date.assignment_count}} + más + {% endif %} +
+
+ + + + +
+ + + + + + + + + +
+ Calendar Icon + + Ver mi horario +
+
+
+
+ {% endif %} + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + +
+ Interactúa con una +
+ comunidad global +
+
+ Divider + + + + + + + + + +
Conoce a los + {% if learners_count %}{{ learners_count }}{% else %}otros{% endif %} + estudiantes en tu + curso.
+
+
+ + + + +
+ + + + + + + + + +
+ Community Icon + + Únete al foro de discusión +
+
+
+
+ + + + + + {% include "emails/partials/footer_es.html" %} + +
+ +
+ + + + \ No newline at end of file diff --git a/common/djangoapps/student/templates/emails/partials/footer_en.html b/common/djangoapps/student/templates/emails/partials/footer_en.html new file mode 100644 index 000000000000..ccf8514b984b --- /dev/null +++ b/common/djangoapps/student/templates/emails/partials/footer_en.html @@ -0,0 +1,130 @@ + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + + + +
+
+
+
edX.org where learning moments become career milestones.
+
+ Need enterprise learning solutions? edX For Business
+
+ © {{ current_year }} edX LLC All rights reserved.
+
+ Update Your Preferences / Unsubscribe +

+ 2345 Crystal Drive, Suite 1100, Arlington, VA 22202
+
+
diff --git a/common/djangoapps/student/templates/emails/partials/footer_es.html b/common/djangoapps/student/templates/emails/partials/footer_es.html new file mode 100644 index 000000000000..e71ca66702ae --- /dev/null +++ b/common/djangoapps/student/templates/emails/partials/footer_es.html @@ -0,0 +1,129 @@ + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + + + +
+
+
+
edX es la plataforma de confianza para la educación y el aprendizaje.
+
+ edX Para Negocios — Soluciones de aprendizaje en línea para tu organización.

+ © {{ current_year }} edX LLC Todos los derechos reservados.
+
+ Actualiza tus preferencias / Salir de la lista
+
+ 2345 Crystal Drive, Suite 1100, Arlington, VA 22202
+
+
diff --git a/common/djangoapps/student/templates/emails/partials/header_en.html b/common/djangoapps/student/templates/emails/partials/header_en.html new file mode 100644 index 000000000000..6ed45b4f026b --- /dev/null +++ b/common/djangoapps/student/templates/emails/partials/header_en.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + +
 
+ + + + +
+ + + + +
edX
+
+ + + + + +
+ + + + +
+ + + Courses + + +  /  + + + Programs + + +  /  + + + My Account + + +
+
+
 
\ No newline at end of file diff --git a/common/djangoapps/student/templates/emails/partials/header_es.html b/common/djangoapps/student/templates/emails/partials/header_es.html new file mode 100644 index 000000000000..e6bfb5a6d7d4 --- /dev/null +++ b/common/djangoapps/student/templates/emails/partials/header_es.html @@ -0,0 +1,37 @@ + + + + + + +
 
+ + + + +
+ + +
+
+ + + + + +
+ + + + + + + + + +
Cursos/Programas/Mi Cuenta
+
+
 
\ No newline at end of file diff --git a/lms/static/images/enrollment_email/arrow_icon_es.png b/lms/static/images/enrollment_email/arrow_icon_es.png new file mode 100644 index 000000000000..2285352a394e Binary files /dev/null and b/lms/static/images/enrollment_email/arrow_icon_es.png differ diff --git a/lms/static/images/enrollment_email/banner_default.png b/lms/static/images/enrollment_email/banner_default.png new file mode 100644 index 000000000000..0535b905ceb4 Binary files /dev/null and b/lms/static/images/enrollment_email/banner_default.png differ diff --git a/lms/static/images/enrollment_email/calendar_icon_es.png b/lms/static/images/enrollment_email/calendar_icon_es.png new file mode 100644 index 000000000000..730ae971efbc Binary files /dev/null and b/lms/static/images/enrollment_email/calendar_icon_es.png differ diff --git a/lms/static/images/enrollment_email/community_icon_es.png b/lms/static/images/enrollment_email/community_icon_es.png new file mode 100644 index 000000000000..66006cfd5d93 Binary files /dev/null and b/lms/static/images/enrollment_email/community_icon_es.png differ diff --git a/lms/static/images/enrollment_email/community_illustration_en.png b/lms/static/images/enrollment_email/community_illustration_en.png new file mode 100644 index 000000000000..55140a8ef241 Binary files /dev/null and b/lms/static/images/enrollment_email/community_illustration_en.png differ diff --git a/lms/static/images/enrollment_email/dollar_icon_en.png b/lms/static/images/enrollment_email/dollar_icon_en.png new file mode 100644 index 000000000000..ebf7aa560829 Binary files /dev/null and b/lms/static/images/enrollment_email/dollar_icon_en.png differ diff --git a/lms/static/images/enrollment_email/dollar_icon_es.png b/lms/static/images/enrollment_email/dollar_icon_es.png new file mode 100644 index 000000000000..e406434837f2 Binary files /dev/null and b/lms/static/images/enrollment_email/dollar_icon_es.png differ diff --git a/lms/static/images/enrollment_email/flag_icon_black_en_es.png b/lms/static/images/enrollment_email/flag_icon_black_en_es.png new file mode 100644 index 000000000000..b5fc97ee32bc Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_black_en_es.png differ diff --git a/lms/static/images/enrollment_email/flag_icon_grey_es.png b/lms/static/images/enrollment_email/flag_icon_grey_es.png new file mode 100644 index 000000000000..9eff18dfc779 Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_grey_es.png differ diff --git a/lms/static/images/enrollment_email/flag_icon_orange_en.png b/lms/static/images/enrollment_email/flag_icon_orange_en.png new file mode 100644 index 000000000000..574e11d8f6cc Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_orange_en.png differ diff --git a/lms/static/images/enrollment_email/flag_icon_pink_en.png b/lms/static/images/enrollment_email/flag_icon_pink_en.png new file mode 100644 index 000000000000..86b46ef98021 Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_pink_en.png differ diff --git a/lms/static/images/enrollment_email/flag_icon_white_es.png b/lms/static/images/enrollment_email/flag_icon_white_es.png new file mode 100644 index 000000000000..9bc8536f4c56 Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_white_es.png differ diff --git a/lms/static/images/enrollment_email/goal_idea_icon_en.png b/lms/static/images/enrollment_email/goal_idea_icon_en.png new file mode 100644 index 000000000000..0d9c55ad7031 Binary files /dev/null and b/lms/static/images/enrollment_email/goal_idea_icon_en.png differ diff --git a/lms/static/images/enrollment_email/logo_url.png b/lms/static/images/enrollment_email/logo_url.png new file mode 100644 index 000000000000..419f27df2c3e Binary files /dev/null and b/lms/static/images/enrollment_email/logo_url.png differ diff --git a/lms/static/images/enrollment_email/person_icon_en.png b/lms/static/images/enrollment_email/person_icon_en.png new file mode 100644 index 000000000000..a05d7bcfd447 Binary files /dev/null and b/lms/static/images/enrollment_email/person_icon_en.png differ diff --git a/lms/static/images/enrollment_email/person_icon_es.png b/lms/static/images/enrollment_email/person_icon_es.png new file mode 100644 index 000000000000..1f79fee7028f Binary files /dev/null and b/lms/static/images/enrollment_email/person_icon_es.png differ diff --git a/lms/static/images/enrollment_email/slant_line_es.png b/lms/static/images/enrollment_email/slant_line_es.png new file mode 100644 index 000000000000..ce4e037273e3 Binary files /dev/null and b/lms/static/images/enrollment_email/slant_line_es.png differ diff --git a/lms/static/images/enrollment_email/timer_icon_en.png b/lms/static/images/enrollment_email/timer_icon_en.png new file mode 100644 index 000000000000..ca3b690b2d61 Binary files /dev/null and b/lms/static/images/enrollment_email/timer_icon_en.png differ diff --git a/lms/static/images/enrollment_email/timer_icon_es.png b/lms/static/images/enrollment_email/timer_icon_es.png new file mode 100644 index 000000000000..55f4b54ce789 Binary files /dev/null and b/lms/static/images/enrollment_email/timer_icon_es.png differ diff --git a/lms/static/images/enrollment_email/vertical_line_black_en.png b/lms/static/images/enrollment_email/vertical_line_black_en.png new file mode 100644 index 000000000000..191d1bf257ba Binary files /dev/null and b/lms/static/images/enrollment_email/vertical_line_black_en.png differ diff --git a/lms/static/images/enrollment_email/vertical_line_orange_en.png b/lms/static/images/enrollment_email/vertical_line_orange_en.png new file mode 100644 index 000000000000..459f1fe1606d Binary files /dev/null and b/lms/static/images/enrollment_email/vertical_line_orange_en.png differ diff --git a/lms/static/images/enrollment_email/vertical_line_white_en.png b/lms/static/images/enrollment_email/vertical_line_white_en.png new file mode 100644 index 000000000000..96f66d5ba919 Binary files /dev/null and b/lms/static/images/enrollment_email/vertical_line_white_en.png differ diff --git a/lms/static/images/enrollment_email/you_are_enrolled_en.png b/lms/static/images/enrollment_email/you_are_enrolled_en.png new file mode 100644 index 000000000000..e7282f071085 Binary files /dev/null and b/lms/static/images/enrollment_email/you_are_enrolled_en.png differ diff --git a/openedx/core/djangoapps/ace_common/utils.py b/openedx/core/djangoapps/ace_common/utils.py index 7cf38c821976..e84bc2230f7d 100644 --- a/openedx/core/djangoapps/ace_common/utils.py +++ b/openedx/core/djangoapps/ace_common/utils.py @@ -1,9 +1,6 @@ """ Utility functions for edx-ace. """ -import logging - -log = logging.getLogger(__name__) def setup_firebase_app(firebase_credentials, app_name='fcm-app'): diff --git a/openedx/core/djangoapps/user_authn/tasks.py b/openedx/core/djangoapps/user_authn/tasks.py index c1c781d2f26f..3c425a1de6f4 100644 --- a/openedx/core/djangoapps/user_authn/tasks.py +++ b/openedx/core/djangoapps/user_authn/tasks.py @@ -18,9 +18,24 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_authn.utils import check_pwned_password from openedx.core.lib.celery.task_utils import emulate_http_request +from edx_toggles.toggles import WaffleFlag log = logging.getLogger('edx.celery.task') +# .. toggle_name: user_authn.enable_ses_for_account_activation +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Route account activation emails via SES using ACE. +# .. toggle_use_cases: opt_in, temporary +# .. toggle_creation_date: 2026-03-31 +# .. toggle_target_removal_date: None +# .. toggle_warning: Controls SES routing for account activation emails. + +ENABLE_SES_FOR_ACCOUNT_ACTIVATION = WaffleFlag( + 'user_authn.enable_ses_for_account_activation', + __name__, +) + @shared_task @set_code_owner_attribute @@ -60,6 +75,9 @@ def send_activation_email(self, msg_string, from_address=None, site_id=None): max_retries = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS retries = self.request.retries + if msg.options is None: + msg.options = {} + if from_address is None: from_address = configuration_helpers.get_value('ACTIVATION_EMAIL_FROM_ADDRESS') or ( configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) @@ -71,28 +89,63 @@ def send_activation_email(self, msg_string, from_address=None, site_id=None): site = Site.objects.get(id=site_id) if site_id else Site.objects.get_current() user = User.objects.get(id=msg.recipient.lms_user_id) + route_via_ses = ENABLE_SES_FOR_ACCOUNT_ACTIVATION.is_enabled() + sent_via_ses = False + + if route_via_ses: + msg.options['override_default_channel'] = 'django_email' + try: with emulate_http_request(site=site, user=user): ace.send(msg) + sent_via_ses = route_via_ses + except RecoverableChannelDeliveryError: - log.info('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format( - dest_addr=dest_addr, - attempt=retries, - max_attempts=max_retries - )) - try: - self.retry(countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, max_retries=max_retries) - except MaxRetriesExceededError: - log.error( - 'Unable to send activation email to user from "%s" to "%s"', - from_address, + if route_via_ses: + log.warning( + "SES send failed for %s, falling back to default ACE channel", dest_addr, - exc_info=True + exc_info=True, ) + + msg.options.pop('override_default_channel', None) + + with emulate_http_request(site=site, user=user): + ace.send(msg) + sent_via_ses = False + + else: + log.info( + 'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format( + dest_addr=dest_addr, + attempt=retries, + max_attempts=max_retries + ) + ) + try: + self.retry( + countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, + max_retries=max_retries + ) + except MaxRetriesExceededError: + log.error( + 'Unable to send activation email to user from "%s" to "%s"', + from_address, + dest_addr, + exc_info=True + ) + return + except Exception: log.exception( 'Unable to send activation email to user from "%s" to "%s"', from_address, dest_addr, ) - raise Exception # lint-amnesty, pylint: disable=raise-missing-from + raise + + log.info( + 'Activation email for %s sent via %s', + dest_addr, + 'SES' if sent_via_ses else 'default ACE channel', + )