Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: welcome emails for xPRO Learners #3017

Merged
merged 10 commits into from
Jul 12, 2024
1 change: 1 addition & 0 deletions courses/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def create_run_enrollments(
successful_enrollments.append(enrollment)
if enrollment.edx_enrolled:
mail_api.send_course_run_enrollment_email(enrollment)
mail_api.send_course_run_enrollment_welcome_email(enrollment)
return successful_enrollments, edx_request_success


Expand Down
1 change: 1 addition & 0 deletions courses/management/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def create_run_enrollment(
run_enrollment.edx_enrolled = True
run_enrollment.save_and_log(None)
mail_api.send_course_run_enrollment_email(run_enrollment)
mail_api.send_course_run_enrollment_welcome_email(run_enrollment)
elif not keep_failed_enrollments:
if created:
run_enrollment.delete()
Expand Down
40 changes: 39 additions & 1 deletion ecommerce/mail_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@

from courses.models import CourseRun
from ecommerce.constants import BULK_ENROLLMENT_EMAIL_TAG, CYBERSOURCE_CARD_TYPES
from ecommerce.utils import make_checkout_url
from ecommerce.utils import format_run_date, make_checkout_url
from mail import api
from mail.constants import (
EMAIL_B2B_RECEIPT,
EMAIL_BULK_ENROLL,
EMAIL_COURSE_RUN_ENROLLMENT,
EMAIL_COURSE_RUN_UNENROLLMENT,
EMAIL_PRODUCT_ORDER_RECEIPT,
EMAIL_WELCOME_COURSE_RUN_ENROLLMENT,
)
from mitxpro.utils import format_price

log = logging.getLogger()
ENROLL_ERROR_EMAIL_SUBJECT = "MIT xPRO enrollment error"
EMAIL_DATE_FORMAT = "%b %-d, %Y"
EMAIL_TIME_FORMAT = "%I:%M %p %Z"


def get_b2b_receipt_data(order):
Expand Down Expand Up @@ -191,6 +193,42 @@ def send_course_run_unenrollment_email(enrollment):
log.exception("Error sending unenrollment success email: %s", exp) # noqa: TRY401


def send_course_run_enrollment_welcome_email(enrollment):
"""
Send welcome email to the user on successful enrollment

Args:
enrollment (CourseRunEnrollment): the enrollment for which to send the welcome email
"""
if not settings.FEATURES.get("ENROLLMENT_WELCOME_EMAIL", False):
Copy link
Contributor

Choose a reason for hiding this comment

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

The feature flags are more readable when applied to the initiation of something. In this case, the flag should be placed where we are calling send_course_run_enrollment_welcome_email from. Someone looking at the code in api.py might think that the email would be sent but that would be blocked in this function. So, If we take this flag to the api.py before calling this function that would make more sense for code reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It’s a good suggestion, but we will need to make this change in both places. Actually, I tried to make this change in a common place as it is being called not only through api.py but also through courses/management/utils.py for transfer enrollment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright, Let's keep it here then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, Let's keep it here then.

Sorry I was looking into addressing your request, and I pushed commit as didn't notice we were agreed upon.
I have reverted it. Now It seems we are good and ready to go to merge.

log.info("Feature ENROLLMENT_WELCOME_EMAIL is disabled.")
return
run_start_date, run_start_time = format_run_date(enrollment.run.start_date)
run_end_date, _ = format_run_date(enrollment.run.end_date)
run_duration = (
f"{run_start_date} - {run_end_date}" if run_start_date and run_end_date else ""
)
try:
user = enrollment.user
api.send_message(
api.message_for_recipient(
user.email,
api.context_for_user(
user=user,
extra_context={
"enrollment": enrollment,
"run_start_date": run_start_date,
"run_start_time": run_start_time,
"run_date_range": run_duration,
},
),
EMAIL_WELCOME_COURSE_RUN_ENROLLMENT,
)
)
except: # noqa: E722
log.exception("Error sending welcome email")


def send_b2b_receipt_email(order):
"""
Send an email summarizing the enrollment codes purchased by a user
Expand Down
47 changes: 47 additions & 0 deletions ecommerce/mail_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
)
from ecommerce.mail_api import (
EMAIL_DATE_FORMAT,
EMAIL_TIME_FORMAT,
ENROLL_ERROR_EMAIL_SUBJECT,
send_b2b_receipt_email,
send_bulk_enroll_emails,
send_course_run_enrollment_email,
send_course_run_enrollment_welcome_email,
send_ecommerce_order_receipt,
send_enrollment_failure_message,
)
Expand All @@ -41,6 +43,7 @@
EMAIL_BULK_ENROLL,
EMAIL_COURSE_RUN_ENROLLMENT,
EMAIL_PRODUCT_ORDER_RECEIPT,
EMAIL_WELCOME_COURSE_RUN_ENROLLMENT,
)
from mitxpro.utils import format_price
from users.factories import UserFactory
Expand Down Expand Up @@ -147,6 +150,50 @@ def test_send_course_run_enrollment_email_error(mocker):
)


@pytest.mark.parametrize("enabled", [True, False])
def test_send_course_run_enrollment_welcome_email(settings, mocker, enabled):
"""send_course_run_enrollment_welcome_email should send a welcome email for the given enrollment"""
settings.FEATURES["ENROLLMENT_WELCOME_EMAIL"] = enabled
mock_log = mocker.patch("ecommerce.mail_api.log")
patched_mail_api = mocker.patch("ecommerce.mail_api.api")
enrollment = CourseRunEnrollmentFactory.create()

run_start_date = enrollment.run.start_date
run_start_time = run_start_date.astimezone(datetime.timezone.utc).strftime(
EMAIL_TIME_FORMAT
)
run_end_date = enrollment.run.end_date
date_range = (
f"{run_start_date.strftime(EMAIL_DATE_FORMAT)} - "
f"{run_end_date.strftime(EMAIL_DATE_FORMAT)}"
)

send_course_run_enrollment_welcome_email(enrollment)

if not enabled:
mock_log.info.assert_called_once_with(
"Feature ENROLLMENT_WELCOME_EMAIL is disabled."
)
else:
patched_mail_api.context_for_user.assert_called_once_with(
user=enrollment.user,
extra_context={
"enrollment": enrollment,
"run_start_date": run_start_date.strftime(EMAIL_DATE_FORMAT),
"run_start_time": run_start_time,
"run_date_range": date_range,
},
)
patched_mail_api.message_for_recipient.assert_called_once_with(
enrollment.user.email,
patched_mail_api.context_for_user.return_value,
EMAIL_WELCOME_COURSE_RUN_ENROLLMENT,
)
patched_mail_api.send_message.assert_called_once_with(
patched_mail_api.message_for_recipient.return_value
)


@pytest.mark.parametrize("has_discount", [True, False])
def test_send_b2b_receipt_email(mocker, settings, has_discount):
"""send_b2b_receipt_email should send a receipt email"""
Expand Down
22 changes: 22 additions & 0 deletions ecommerce/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Utility functions for ecommerce"""

import datetime
import logging
from urllib.parse import urlencode, urljoin

Expand All @@ -12,6 +13,7 @@
from ecommerce.exceptions import ParseException

log = logging.getLogger(__name__)
EMAIL_TIME_FORMAT = "%I:%M %p %Z"


def create_delete_rule(table_name):
Expand Down Expand Up @@ -154,3 +156,23 @@ def is_existing_coupon_code(value):
Coupon.objects.filter(coupon_code=value).exists()
or B2BCoupon.objects.filter(coupon_code=value).exists()
)


def format_run_date(run_date):
"""
Format run date to return both date and time strings.

Args:
run_date (datetime): The datetime to format.

Returns:
tuple: A tuple containing the formatted date and time strings.
"""
if run_date:
from ecommerce.mail_api import EMAIL_DATE_FORMAT
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use import here to avoid circular import


formatted_date_time = run_date.astimezone(datetime.timezone.utc).strftime(
f"{EMAIL_DATE_FORMAT}-{EMAIL_TIME_FORMAT}"
)
return tuple(formatted_date_time.split("-", 1))
return "", ""
2 changes: 2 additions & 0 deletions mail/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
EMAIL_B2B_RECEIPT = "b2b_receipt"
EMAIL_PRODUCT_ORDER_RECEIPT = "product_order_receipt"
EMAIL_CHANGE_EMAIL = "change_email"
EMAIL_WELCOME_COURSE_RUN_ENROLLMENT = "welcome_course_run_enrollment"

EMAIL_TYPE_DESCRIPTIONS = {
EMAIL_VERIFICATION: "Verify Email",
Expand All @@ -16,6 +17,7 @@
EMAIL_COURSE_RUN_ENROLLMENT: "Course Run Enrollment",
EMAIL_B2B_RECEIPT: "Enrollment Code Purchase Receipt",
EMAIL_CHANGE_EMAIL: "Change Email",
EMAIL_WELCOME_COURSE_RUN_ENROLLMENT: "Welcome Course Run Enrollment",
}

MAILGUN_API_DOMAIN = "api.mailgun.net"
Expand Down
145 changes: 145 additions & 0 deletions mail/templates/welcome_course_run_enrollment/body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
{% extends "email_base.html" %}

{% block content %}
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff">
<table
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
width="100%"
>
<tr>
<td
style="
padding: 20px;
font-family: sans-serif;
font-size: 15px;
line-height: 20px;
color: #555555;
"
>
<p style="margin: 0 0 10px">
Dear
{{ user.name }},
</p>
<p style="margin: 0 0 10px 50px">Welcome to MIT xPRO!</p>
<p style="margin: 0 0 10px">
We're thrilled to have you on board for our upcoming course,
<strong>{{ enrollment.run }}</strong>. This is a significant step
Copy link
Contributor

Choose a reason for hiding this comment

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

Any specific reason, you used run instead of course?

Suggested change
<strong>{{ enrollment.run }}</strong>. This is a significant step
<strong>{{ enrollment.run.course }}</strong>. This is a significant step

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used enrollment.run here and in all other instances you mentioned because it shows the course name along with the course run that makes it more descriptive. We've used a similar approach in other templates, which might be the reason. However, if it only needs to be the course name, we can change it accordingly.

toward advancing your expertise.
</p>
<p style="margin: 0 0 10px">
<strong>Here are some vital details about your course:</strong>
</p>
<ul style="margin: 0 0 10px; padding-left: 20px">
<li>
Course Name: <strong>{{ enrollment.run }}</strong>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Course Name: <strong>{{ enrollment.run }}</strong>
Course Name: <strong>{{ enrollment.run.course }}</strong>

</li>
{% if run_start_date %}
<li>
Start Date: <strong>{{ run_start_date }}</strong>
</li>
{% endif %}
{% if run_date_range %}
<li>
Duration: <strong>{{ run_date_range }}</strong>
</li>
{% endif %}
{% if run_start_time %}
<li>
Start Time: <strong>{{ run_start_time }}</strong>
</li>
{% endif %}
</ul>
<p style="margin: 0 0 10px">
<strong><u>Course Format:</u></strong
><br />
All MIT xPRO courses are delivered online for maximum flexibility.
The course content, including pre-recorded video lectures, is
accessible at your convenience, with no specific log-on times.
</p>
<p style="margin: 0 0 10px">
<strong><u>Accessing Your Course:</u></strong
><br />
Once you have enrolled in a course or program with MIT xPRO, you can
expect to gain access to the course materials and platform on the
scheduled start date. You will not be able to access any of the
course content before the start date and time. To log in and start
your course, visit your MIT xPRO account dashboard.
</p>
<p style="margin: 0 0 10px">
<strong><u>In-Course Support:</u></strong
><br />
Each course offers in-course discussion forums, allowing you to
engage with fellow learners and receive support. Our dedicated
course assistant (CA) team actively monitors these forums to offer
assistance and guidance.
</p>
<p style="margin: 0 0 10px">
<strong><u>Course Schedule and Deadlines:</u></strong
><br />
Please be aware that each MIT xPRO course has its own schedule, with
content becoming available after the course start date. Content
releases typically adhere to a schedule determined by the course
instructors, often on a weekly basis. Assignment deadlines are
strictly enforced, and late submissions are not accepted. The course
will conclude on a designated end date, at which point live aspects
such as discussion forums will be closed.
</p>
<p style="margin: 0 0 10px">
<strong><u>Certificate of Completion:</u></strong
><br />
Upon successfully completing the course, you'll receive a digital
Professional Certificate from MIT xPRO, validating your
accomplishment and expertise in
<strong>{{ enrollment.run }}</strong>.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<strong>{{ enrollment.run }}</strong>.
<strong>{{ enrollment.run.course }}</strong>.

</p>
<p style="margin: 0 0 10px">
<strong><u>Where to Seek Support:</u></strong
><br />
For inquiries related to in-course content, we recommend reaching
out directly to the course team. They possess the expertise and have
access to the course materials, making them best equipped to provide
accurate and detailed responses. You can find the contact
information for the course team within the courseware itself,
typically in sections such as the Syllabus, Course Guide, or FAQ.
</p>
<p style="margin: 0 0 10px">
For non-sensitive course assignment questions, posting them in the
in-course discussion forums is another valuable option. Our course
assistants (CAs) actively monitor these forums, and fellow learners
often contribute their insights and solutions.
</p>
<p style="margin: 0 0 10px">
For general inquiries not specific to course content, our xPRO
Support Team is here to assist you. Feel free to check out the
<a href="https://xpro.zendesk.com/hc/en-us">MIT xPRO Help Center</a>
or reach out to us at
<a href="mailto:[email protected]">[email protected]</a>, and
we'll be happy to help.
</p>
<p style="margin: 0 0 10px">
We look forward to supporting you during your time with MIT xPRO. If
you have any further questions or need assistance, please do not
hesitate to reach out.
</p>
<p style="margin: 0 0 10px">
Get ready to unlock your potential and excel!
</p>
<p style="margin: 0 0 10px">Best of luck with your studies,</p>
<p style="margin: 0 0 10px">MIT xPRO Team</p>
<p style="margin: 0 0 10px">
<strong>P.S.</strong> Don't forget to check out the "Getting
Started" section in your course dashboard for helpful tips and
resources to kick-start your learning experience!
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
{% endblock %}
1 change: 1 addition & 0 deletions mail/templates/welcome_course_run_enrollment/subject.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Welcome! {{ enrollment.run }} starts on {{ run_start_date }}
Loading