diff --git a/cms/envs/common.py b/cms/envs/common.py index 89d190a0c6..07e17c727c 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1208,3 +1208,7 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = [] # to generating test databases will discover and try to create all tables # and this setting needs to be present OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application' + +# Used with Email sending +RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 +RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py new file mode 100644 index 0000000000..f3d6644cbf --- /dev/null +++ b/common/djangoapps/student/tasks.py @@ -0,0 +1,50 @@ +""" +This file contains celery tasks for sending email +""" +import logging +from django.conf import settings +from django.core import mail + +from celery.task import task # pylint: disable=no-name-in-module, import-error +from celery.exceptions import MaxRetriesExceededError +from boto.exception import NoAuthHandlerFound + +log = logging.getLogger('edx.celery.task') + + +@task(bind=True) +def send_activation_email(self, subject, message, from_address, dest_addr): + """ + Sending an activation email to the users. + """ + max_retries = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS + retries = self.request.retries + try: + mail.send_mail(subject, message, from_address, [dest_addr], fail_silently=False) + # Log that the Activation Email has been sent to user without an exception + log.info("Activation Email has been sent to User {user_email}".format( + user_email=dest_addr + )) + except NoAuthHandlerFound: # pylint: disable=broad-except + 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 + ) + except Exception: # pylint: disable=bare-except + log.exception( + 'Unable to send activation email to user from "%s" to "%s"', + from_address, + dest_addr, + exc_info=True + ) + raise Exception diff --git a/common/djangoapps/student/tests/test_create_account.py b/common/djangoapps/student/tests/test_create_account.py index 1aa79fed35..86f812ab58 100644 --- a/common/djangoapps/student/tests/test_create_account.py +++ b/common/djangoapps/student/tests/test_create_account.py @@ -233,7 +233,7 @@ class TestCreateAccount(TestCase): request.user = AnonymousUser() with mock.patch('edxmako.request_context.get_current_request', return_value=request): - with mock.patch('django.contrib.auth.models.User.email_user') as mock_send_mail: + with mock.patch('django.core.mail.send_mail') as mock_send_mail: student.views.create_account(request) # check that send_mail is called diff --git a/common/djangoapps/student/tests/test_tasks.py b/common/djangoapps/student/tests/test_tasks.py new file mode 100644 index 0000000000..2618a8a898 --- /dev/null +++ b/common/djangoapps/student/tests/test_tasks.py @@ -0,0 +1,54 @@ +""" +Tests for the Sending activation email celery tasks +""" + +import mock + +from django.test import TestCase +from django.conf import settings +from student.tasks import send_activation_email +from boto.exception import NoAuthHandlerFound + +from lms.djangoapps.courseware.tests.factories import UserFactory + + +class SendActivationEmailTestCase(TestCase): + """ + Test for send activation email to user + """ + def setUp(self): + """ Setup components used by each test.""" + super(SendActivationEmailTestCase, self).setUp() + self.student = UserFactory() + + @mock.patch('time.sleep', mock.Mock(return_value=None)) + @mock.patch('student.tasks.log') + @mock.patch('django.core.mail.send_mail', mock.Mock(side_effect=NoAuthHandlerFound)) + def test_send_email(self, mock_log): + """ + Tests retries when the activation email doesn't send + """ + from_address = 'task_testing@example.com' + email_max_attempts = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS + + # pylint: disable=no-member + send_activation_email.delay('Task_test', 'Task_test_message', from_address, self.student.email) + + # Asserts sending email retry logging. + for attempt in range(email_max_attempts): + mock_log.info.assert_any_call( + 'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format( + dest_addr=self.student.email, + attempt=attempt, + max_attempts=email_max_attempts + )) + self.assertEquals(mock_log.info.call_count, 6) + + # Asserts that the error was logged on crossing max retry attempts. + mock_log.error.assert_called_with( + 'Unable to send activation email to user from "%s" to "%s"', + from_address, + self.student.email, + exc_info=True + ) + self.assertEquals(mock_log.error.call_count, 1) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b7722e9287..e1db838b65 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -56,6 +56,7 @@ from student.models import ( DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED, LogoutViewConfiguration, RegistrationCookieConfiguration) from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form +from student.tasks import send_activation_email from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error from bulk_email.models import Optout, BulkEmailFlag # pylint: disable=import-error @@ -1812,21 +1813,11 @@ def create_account_with_params(request, params): 'email_from_address', settings.DEFAULT_FROM_EMAIL ) - try: - if settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL'): - dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL'] - message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) + - '-' * 80 + '\n\n' + message) - mail.send_mail(subject, message, from_address, [dest_addr], fail_silently=False) - else: - user.email_user(subject, message, from_address) - except Exception: # pylint: disable=broad-except - log.error( - u'Unable to send activation email to user from "%s" to "%s"', - from_address, - dest_addr, - exc_info=True - ) + if settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL'): + dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL'] + message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) + + '-' * 80 + '\n\n' + message) + send_activation_email.delay(subject, message, from_address, dest_addr) else: registration.activate() _enroll_user_in_pending_courses(user) # Enroll student in any pending courses diff --git a/lms/envs/common.py b/lms/envs/common.py index 52a9349f7c..80cd61d865 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -375,6 +375,9 @@ GENERATE_PROFILE_SCORES = False # Used with XQueue XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds +# Used with Email sending +RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 +RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms