This repository has been archived by the owner on Nov 4, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ENT-3536 | Added a new task for sending the offer usage emails. (#67)
- Loading branch information
1 parent
7c1dded
commit 8382e63
Showing
5 changed files
with
295 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
""" | ||
Notification class for sending the notification emails. | ||
""" | ||
from celery.utils.log import get_task_logger | ||
|
||
from sailthru.sailthru_error import SailthruClientError | ||
from ecommerce_worker.sailthru.v1.exceptions import SailthruError | ||
from ecommerce_worker.sailthru.v1.utils import can_retry_sailthru_request, get_sailthru_client | ||
|
||
log = get_task_logger(__name__) | ||
|
||
|
||
class Notification(object): | ||
""" | ||
This class exports the 'send' function for sending the emails. | ||
""" | ||
|
||
def __init__(self, config, emails, email_vars, logger_prefix, site_code, template): | ||
self.config = config | ||
self.emails = emails | ||
self.email_vars = email_vars | ||
self.logger_prefix = logger_prefix | ||
self.site_code = site_code | ||
self.template = template | ||
|
||
def _is_eligible_for_retry(self, response): | ||
""" | ||
Return a bool whether this task is eligible for retry or not. | ||
Also log the appropriate message according to occurred error. | ||
""" | ||
is_eligible_for_retry = False | ||
error = response.get_error() | ||
log.error( | ||
'[{logger_prefix}] A {token_error_code} - {token_error_message} error occurred while attempting to send a ' | ||
'notification. Message: {message}'.format( | ||
logger_prefix=self.logger_prefix, | ||
message=self.email_vars.get('email_body'), | ||
token_error_code=error.get_error_code(), | ||
token_error_message=error.get_message() | ||
) | ||
) | ||
if can_retry_sailthru_request(error): | ||
log.info( | ||
'[{logger_prefix}] An attempt will be made to resend the notification.' | ||
' Message: {message}'.format( | ||
logger_prefix=self.logger_prefix, | ||
message=self.email_vars.get('email_body') | ||
) | ||
) | ||
is_eligible_for_retry = True | ||
else: | ||
log.warning( | ||
'[{logger_prefix}] No further attempts will be made to send the notification.' | ||
' Failed Message: {message}'.format( | ||
logger_prefix=self.logger_prefix, | ||
message=self.email_vars.get('email_body') | ||
) | ||
) | ||
return is_eligible_for_retry | ||
|
||
@staticmethod | ||
def _get_send_callback(is_multi_send): | ||
""" | ||
Return the associated function 'send' in case of single send and 'multi_send' in case of multi_send | ||
""" | ||
return 'multi_send' if is_multi_send else 'send' | ||
|
||
def _get_client(self): | ||
""" | ||
Return the sailthru client or None if exception raised. | ||
""" | ||
sailthru_client = None | ||
try: | ||
sailthru_client = get_sailthru_client(self.site_code) | ||
except SailthruError: | ||
log.exception( | ||
'[{logger_prefix}] A client error occurred while attempting to send a notification.' | ||
' Message: {message}'.format( | ||
logger_prefix=self.logger_prefix, | ||
message=self.email_vars.get('email_body') | ||
) | ||
) | ||
return sailthru_client | ||
|
||
def _get_params(self, is_multi_send): | ||
""" | ||
Return the dict of parameters according to the given 'is_multi_send' parameter. | ||
It can be a simple 'send' function in which we will send an email | ||
to a single email address or it can be a 'multi_send' function. | ||
""" | ||
params = { | ||
'template': self.config['templates'][self.template], | ||
'_vars': self.email_vars | ||
} | ||
email_param = 'emails' if is_multi_send else 'email' | ||
params[email_param] = self.emails | ||
return params | ||
|
||
def send(self, is_multi_send=False): | ||
""" | ||
Send the notification email to single email address or comma | ||
separated emails on bases of is_multi_send parameter | ||
Returns: | ||
response: Sailthru endpoint response. | ||
is_eligible_for_retry(Bool): whether this response is eligible for retry or not. | ||
""" | ||
is_eligible_for_retry = False | ||
sailthru_client = self._get_client() | ||
if sailthru_client is None: | ||
return None, is_eligible_for_retry | ||
|
||
try: | ||
response = getattr( | ||
sailthru_client, | ||
self._get_send_callback(is_multi_send) | ||
)(**self._get_params(is_multi_send)) | ||
except SailthruClientError: | ||
log.exception( | ||
'[{logger_prefix}] A client error occurred while attempting to send a notification.' | ||
' Message: {message}'.format( | ||
logger_prefix=self.logger_prefix, | ||
message=self.email_vars.get('email_body') | ||
) | ||
) | ||
return None, is_eligible_for_retry | ||
|
||
if not response.is_ok(): | ||
is_eligible_for_retry = self._is_eligible_for_retry(response) | ||
return response, is_eligible_for_retry |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,9 @@ | |
from ecommerce_worker.sailthru.v1.exceptions import SailthruError | ||
from ecommerce_worker.sailthru.v1.tasks import ( | ||
update_course_enrollment, _update_unenrolled_list, _get_course_content, _get_course_content_from_ecommerce, | ||
send_course_refund_email, send_offer_assignment_email, send_offer_update_email, _update_assignment_email_status | ||
send_course_refund_email, send_offer_assignment_email, send_offer_update_email, send_offer_usage_email, | ||
_update_assignment_email_status, | ||
|
||
) | ||
from ecommerce_worker.utils import get_configuration | ||
|
||
|
@@ -653,8 +655,24 @@ def test_message_sent(self): | |
) | ||
|
||
|
||
class BaseSendEmailTests(TestCase): | ||
""" | ||
Base class for testing the sending notification through sailthru client. | ||
""" | ||
|
||
def mock_api_response(self, status, body): | ||
""" Mock the Sailthru send API. """ | ||
httpretty.register_uri( | ||
httpretty.POST, | ||
'https://api.sailthru.com/send', | ||
status=status, | ||
body=json.dumps(body), | ||
content_type='application/json' | ||
) | ||
|
||
|
||
@ddt.ddt | ||
class SendOfferAssignmentEmailTests(TestCase): | ||
class SendOfferAssignmentEmailTests(BaseSendEmailTests): | ||
""" Validates the send_offer_assignment_email task. """ | ||
LOG_NAME = 'ecommerce_worker.sailthru.v1.tasks' | ||
USER_EMAIL = '[email protected]' | ||
|
@@ -681,16 +699,6 @@ def execute_task(self): | |
""" Execute the send_offer_assignment_email task. """ | ||
send_offer_assignment_email(**self.ASSIGNMENT_TASK_KWARGS) | ||
|
||
def mock_api_response(self, status, body): | ||
""" Mock the Sailthru send API. """ | ||
httpretty.register_uri( | ||
httpretty.POST, | ||
'https://api.sailthru.com/send', | ||
status=status, | ||
body=json.dumps(body), | ||
content_type='application/json' | ||
) | ||
|
||
def mock_ecommerce_assignmentemail_api(self, body, status=200): | ||
""" Mock POST requests to the ecommerce assignmentemail API endpoint. """ | ||
httpretty.reset() | ||
|
@@ -861,3 +869,90 @@ def test_update_assignment_email_status(self, data, return_value): | |
'555', | ||
'1234ABC', | ||
'success'), return_value) | ||
|
||
|
||
class SendOfferUsageEmailTests(BaseSendEmailTests): | ||
""" Validates the send_offer_assignment_email task. """ | ||
LOG_NAME = 'ecommerce_worker.sailthru.v1.notification' | ||
EMAILS = '[email protected], [email protected]' | ||
SUBJECT = 'New edX course assignment' | ||
EMAIL_BODY = 'Template message with [email protected] GIL7RUEOU7VHBH7Q ' \ | ||
'http://tempurl.url/enroll 3 2012-04-23' | ||
USAGE_TASK_KWARGS = { | ||
'emails': EMAILS, | ||
'subject': SUBJECT, | ||
'email_body': EMAIL_BODY, | ||
} | ||
|
||
@patch('ecommerce_worker.sailthru.v1.notification.get_sailthru_client', Mock(side_effect=SailthruError)) | ||
def test_client_instantiation_error(self): | ||
""" Verify no message is sent if an error occurs while instantiating the Sailthru API client. """ | ||
with LogCapture(level=logging.INFO) as log: | ||
send_offer_usage_email(**self.USAGE_TASK_KWARGS) | ||
log.check( | ||
(self.LOG_NAME, 'ERROR', '[Offer Usage] A client error occurred while attempting to send' | ||
' a notification. Message: {message}'.format(message=self.EMAIL_BODY)), | ||
) | ||
|
||
@patch('ecommerce_worker.sailthru.v1.notification.log.exception') | ||
def test_api_client_error(self, mock_log): | ||
""" Verify API client errors are logged. """ | ||
with patch.object(SailthruClient, 'multi_send', side_effect=SailthruClientError): | ||
send_offer_usage_email(**self.USAGE_TASK_KWARGS) | ||
mock_log.assert_called_once_with( | ||
'[Offer Usage] A client error occurred while attempting to send a notification.' | ||
' Message: {message}'.format(message=self.EMAIL_BODY) | ||
) | ||
|
||
@httpretty.activate | ||
def test_api_error_with_retry(self): | ||
""" Verify the task is rescheduled if an API error occurs, and the request can be retried. """ | ||
error_code = 43 | ||
error_msg = 'This is a fake error.' | ||
body = { | ||
'error': error_code, | ||
'errormsg': error_msg | ||
} | ||
self.mock_api_response(429, body) | ||
with LogCapture(level=logging.INFO) as log: | ||
with self.assertRaises(Retry): | ||
send_offer_usage_email(**self.USAGE_TASK_KWARGS) | ||
log.check( | ||
(self.LOG_NAME, 'ERROR', | ||
'[Offer Usage] A {token_error_code} - {token_error_message} error occurred' | ||
' while attempting to send a notification.' | ||
' Message: {message}'.format( | ||
message=self.EMAIL_BODY, | ||
token_error_code=error_code, | ||
token_error_message=error_msg | ||
)), | ||
(self.LOG_NAME, 'INFO', | ||
'[Offer Usage] An attempt will be made to resend the notification.' | ||
' Message: {message}'.format(message=self.EMAIL_BODY)), | ||
) | ||
|
||
@httpretty.activate | ||
def test_api_error_without_retry(self): | ||
""" Verify error details are logged if an API error occurs, and the request can NOT be retried. """ | ||
error_code = 1 | ||
error_msg = 'This is a fake error.' | ||
body = { | ||
'error': error_code, | ||
'errormsg': error_msg | ||
} | ||
self.mock_api_response(429, body) | ||
with LogCapture(level=logging.INFO) as log: | ||
send_offer_usage_email(**self.USAGE_TASK_KWARGS) | ||
log.check( | ||
(self.LOG_NAME, 'ERROR', | ||
'[Offer Usage] A {token_error_code} - {token_error_message} error occurred' | ||
' while attempting to send a notification.' | ||
' Message: {message}'.format( | ||
message=self.EMAIL_BODY, | ||
token_error_code=error_code, | ||
token_error_message=error_msg | ||
)), | ||
(self.LOG_NAME, 'WARNING', | ||
'[Offer Usage] No further attempts will be made to send the notification.' | ||
' Failed Message: {message}'.format(message=self.EMAIL_BODY)), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters