diff --git a/common/djangoapps/enrollment/apps.py b/common/djangoapps/enrollment/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..adcbe65fbd1f9ada78ebb734835bdc873cb21869 --- /dev/null +++ b/common/djangoapps/enrollment/apps.py @@ -0,0 +1,20 @@ +""" +Enrollment Application Configuration + +Signal handlers are connected here. +""" + +from django.apps import AppConfig + + +class EnrollmentConfig(AppConfig): + """ + Application configuration for enrollments. + """ + name = u'enrollment' + + def ready(self): + """ + Connect signal handlers. + """ + from . import handlers # pylint: disable=unused-variable diff --git a/common/djangoapps/enrollment/handlers.py b/common/djangoapps/enrollment/handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..f004ca73f526ec8558f14d583dc58696dd60903b --- /dev/null +++ b/common/djangoapps/enrollment/handlers.py @@ -0,0 +1,72 @@ +""" +Handlers and actions related to enrollment. +""" +import logging +from smtplib import SMTPException +from urlparse import urlunsplit + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.core.mail.message import EmailMessage +from django.dispatch import receiver +from edxmako.shortcuts import render_to_string +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers + +from student.models import ENROLL_STATUS_CHANGE, EnrollStatusChange + +LOGGER = logging.getLogger(__name__) + + +@receiver(ENROLL_STATUS_CHANGE) +def send_email_to_staff_on_student_enrollment(sender, event=None, user=None, **kwargs): # pylint: disable=unused-argument + """ + Sends an e-mail to staff after a new enrollment. + This feature can be enabled by setting the e-mail of the staff in ENROLLMENT_NOTIFICATION_EMAIL in lms.env.json, + or by using a SiteConfiguration variable of the same name (which will override the env one). + Disabled by default. + """ + + if event == EnrollStatusChange.enroll: + to_email = configuration_helpers.get_value('ENROLLMENT_NOTIFICATION_EMAIL', + settings.ENROLLMENT_NOTIFICATION_EMAIL) + + if not to_email: + # feature disabled + return + + course_id = kwargs['course_id'] + + site_protocol = 'https' if settings.HTTPS == 'on' else 'http' + site_domain = configuration_helpers.get_value('site_domain', settings.SITE_NAME) + context = { + 'user': user, + # This full_name is dependent on edx-platform's profile implementation + 'user_full_name': user.profile.name if hasattr(user, 'profile') else None, + + 'course_url': urlunsplit(( + site_protocol, + site_domain, + reverse('about_course', args=[course_id.to_deprecated_string()]), + None, + None + )), + 'user_admin_url': urlunsplit(( + site_protocol, + site_domain, + reverse('admin:auth_user_change', args=[user.id]), + None, + None, + )), + } + subject = ''.join( + render_to_string('emails/new_enrollment_email_subject.txt', context).splitlines() + ) + message = render_to_string('emails/new_enrollment_email_body.txt', context) + + email = EmailMessage(subject=subject, body=message, from_email=settings.DEFAULT_FROM_EMAIL, to=[to_email]) + + try: + email.send() + except SMTPException as exception: + LOGGER.error("Failed sending e-mail about new enrollment to %s", to_email) + LOGGER.exception(exception) diff --git a/common/djangoapps/enrollment/tests/test_emails.py b/common/djangoapps/enrollment/tests/test_emails.py new file mode 100644 index 0000000000000000000000000000000000000000..7e42aaeb39c78449fe89076bc69455389ea38c38 --- /dev/null +++ b/common/djangoapps/enrollment/tests/test_emails.py @@ -0,0 +1,82 @@ +""" +Tests for e-mail notifications related to user enrollment. +""" +import unittest + +from django.conf import settings +from django.core import mail +from django.test.utils import override_settings +from nose.plugins.attrib import attr +from rest_framework.test import APITestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from course_modes.models import CourseMode +from course_modes.tests.factories import CourseModeFactory +from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin +from student.tests.factories import UserFactory +from .test_views import EnrollmentTestMixin + + +@attr(shard=3) +@override_settings(EDX_API_KEY="i am a key") +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +@override_settings(ENROLLMENT_NOTIFICATION_EMAIL="some_admins@example.com") +class EnrollmentEmailNotificationTest(EnrollmentTestMixin, + ModuleStoreTestCase, + APITestCase, + EnterpriseServiceMockMixin): + """ + Test e-mails sent to staff when a students enrolls to a course. + """ + USERNAME = "Bob" + EMAIL = "bob@example.com" + PASSWORD = "edx" + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + ENABLED_SIGNALS = ['course_published'] + + def setUp(self): + """ Create a course and user, then log in. Also creates a course mode.""" + # This function is a simplified version of test_views.EnrollmentTest.setUp + + super(EnrollmentEmailNotificationTest, self).setUp() + + # Pass emit_signals when creating the course so it would be cached + # as a CourseOverview. + self.course = CourseFactory.create(emit_signals=True) + + self.user = UserFactory.create( + username=self.USERNAME, + email=self.EMAIL, + password=self.PASSWORD, + ) + self.client.login(username=self.USERNAME, password=self.PASSWORD) + + CourseModeFactory.create( + course_id=self.course.id, + mode_slug=CourseMode.DEFAULT_MODE_SLUG, + mode_display_name=CourseMode.DEFAULT_MODE_SLUG, + ) + + def test_email_sent_to_staff_after_enrollment(self): + """ + Tests that an e-mail is sent on enrollment (but not on other events like unenrollment). + """ + assert len(mail.outbox) == 0 + + # Create an enrollment and verify some data + self.assert_enrollment_status() + + assert len(mail.outbox) == 1 + + msg = mail.outbox[0] + assert msg.subject == "New student enrollment" + assert msg.to == ["some_admins@example.com"] + + # unenroll and check that unenrollment doesn't send additional e-mails + self.assert_enrollment_status( + as_server=True, + is_active=False, + ) + assert len(mail.outbox) == 1 diff --git a/common/templates/emails/new_enrollment_email_body.txt b/common/templates/emails/new_enrollment_email_body.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae25a1b76e98e6ba562104e019ccaa7c487aa8ef --- /dev/null +++ b/common/templates/emails/new_enrollment_email_body.txt @@ -0,0 +1,10 @@ +<%! from django.utils.translation import ugettext as _ %>${_("Enrollment received for:")} + +% if user_full_name: +${_("Learner: {username} ({full_name})").format(username=user.username, full_name=user_full_name)} +% else: +${_("Learner: {username}").format(username=user.username)} +% endif +${_("Course: {url}").format(url=course_url)} + +${_("More learner data in admin: {url}").format(url=user_admin_url)} diff --git a/common/templates/emails/new_enrollment_email_subject.txt b/common/templates/emails/new_enrollment_email_subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc5e80a74f0e68d84a3d422cd1341c19bd3fb3af --- /dev/null +++ b/common/templates/emails/new_enrollment_email_subject.txt @@ -0,0 +1,2 @@ +<%! from django.utils.translation import ugettext as _ %> +${_("New student enrollment")} diff --git a/lms/envs/aws.py b/lms/envs/aws.py index fbd8e53fcbaaaa0577119537616173e05221d56e..3dee29e9a10b9bff9e0df87dc64e0e949d9052a0 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -222,6 +222,8 @@ CONTACT_MAILING_ADDRESS = ENV_TOKENS.get('CONTACT_MAILING_ADDRESS', CONTACT_MAIL # Account activation email sender address ACTIVATION_EMAIL_FROM_ADDRESS = ENV_TOKENS.get('ACTIVATION_EMAIL_FROM_ADDRESS', ACTIVATION_EMAIL_FROM_ADDRESS) +ENROLLMENT_NOTIFICATION_EMAIL = ENV_TOKENS.get('ENROLLMENT_NOTIFICATION_EMAIL', ENROLLMENT_NOTIFICATION_EMAIL) + # Currency PAID_COURSE_REGISTRATION_CURRENCY = ENV_TOKENS.get('PAID_COURSE_REGISTRATION_CURRENCY', PAID_COURSE_REGISTRATION_CURRENCY) diff --git a/lms/envs/common.py b/lms/envs/common.py index f0361c7532216a536895a7697075780236d75e96..4737c366df80f14758c2e34480f5a933fdf9fc6e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1972,6 +1972,12 @@ BULK_EMAIL_LOG_SENT_EMAILS = False # parallel, and what the SES rate is. BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS = 0.02 +############################# Enrollment E-mails #################################### + +# Used to send an e-mail when students enroll in courses. Write here the destination +# e-mail address, or '' to keep it disabled. +ENROLLMENT_NOTIFICATION_EMAIL = '' + ############################# Email Opt In #################################### # Minimum age for organization-wide email opt in @@ -2155,7 +2161,7 @@ INSTALLED_APPS = [ 'course_modes.apps.CourseModesConfig', # Enrollment API - 'enrollment', + 'enrollment.apps.EnrollmentConfig', # Entitlement API 'entitlements.apps.EntitlementsConfig',