From dfb295fe3865a5d644b07c2da3616252e7286fad Mon Sep 17 00:00:00 2001 From: uzairr Date: Mon, 7 May 2018 12:30:47 +0500 Subject: [PATCH] Send verification status email through sailthru Support needs to inform a learner different states of its id-verification.In order to achieve it email will be sent to learner about its verification state using sailthru. LEARNER-617 --- .../migrations/0010_auto_20180425_0800.py | 25 ++++++++ lms/djangoapps/email_marketing/models.py | 16 +++++ .../verify_student/tests/test_views.py | 62 ++++++++----------- lms/djangoapps/verify_student/utils.py | 28 ++++----- lms/djangoapps/verify_student/views.py | 58 +++++++++-------- .../emails/failed_verification_email.txt | 11 ---- .../emails/successfull_verification_email.txt | 10 --- 7 files changed, 112 insertions(+), 98 deletions(-) create mode 100644 lms/djangoapps/email_marketing/migrations/0010_auto_20180425_0800.py delete mode 100644 lms/templates/emails/failed_verification_email.txt delete mode 100644 lms/templates/emails/successfull_verification_email.txt diff --git a/lms/djangoapps/email_marketing/migrations/0010_auto_20180425_0800.py b/lms/djangoapps/email_marketing/migrations/0010_auto_20180425_0800.py new file mode 100644 index 0000000000..ff8daf7a1e --- /dev/null +++ b/lms/djangoapps/email_marketing/migrations/0010_auto_20180425_0800.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-25 12:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('email_marketing', '0009_remove_emailmarketingconfiguration_sailthru_activation_template'), + ] + + operations = [ + migrations.AddField( + model_name='emailmarketingconfiguration', + name='sailthru_verification_failed_template', + field=models.CharField(blank=True, help_text='Sailthru send template to use on failed ID verification.', max_length=20), + ), + migrations.AddField( + model_name='emailmarketingconfiguration', + name='sailthru_verification_passed_template', + field=models.CharField(blank=True, help_text='Sailthru send template to use on passed ID verification.', max_length=20), + ), + ] diff --git a/lms/djangoapps/email_marketing/models.py b/lms/djangoapps/email_marketing/models.py index e8b2475410..3294c78886 100644 --- a/lms/djangoapps/email_marketing/models.py +++ b/lms/djangoapps/email_marketing/models.py @@ -78,6 +78,22 @@ class EmailMarketingConfiguration(ConfigurationModel): ) ) + sailthru_verification_passed_template = models.fields.CharField( + max_length=20, + blank=True, + help_text=_( + "Sailthru send template to use on passed ID verification." + ) + ) + + sailthru_verification_failed_template = models.fields.CharField( + max_length=20, + blank=True, + help_text=_( + "Sailthru send template to use on failed ID verification." + ) + ) + sailthru_upgrade_template = models.fields.CharField( max_length=20, blank=True, diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 15e7c9a55b..2b8882adf5 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -36,7 +36,12 @@ from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.tests import TEST_API_URL, TEST_PAYMENT_DATA, TEST_PUBLIC_URL_ROOT from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline -from lms.djangoapps.verify_student.views import PayAndVerifyView, checkout_with_ecommerce_service, render_to_response +from lms.djangoapps.verify_student.views import ( + PayAndVerifyView, + checkout_with_ecommerce_service, + render_to_response, + EmailMarketingConfiguration +) from openedx.core.djangoapps.embargo.test_utils import restrict_course from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme from openedx.core.djangoapps.user_api.accounts.api import get_account_settings @@ -840,7 +845,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self.assertContains(response, "verification deadline") self.assertContains(response, verification_deadline_in_past) - @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) + @patch.dict(settings.FEATURES, {'EMBARGO': True}) @ddt.data("verify_student_start_flow", "verify_student_begin_flow") def test_embargo_restrict(self, payment_flow): course = self._create_course("verified") @@ -850,7 +855,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): response = self._get_page(payment_flow, course.id, expected_status_code=302) self.assertRedirects(response, redirect_url) - @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) + @patch.dict(settings.FEATURES, {'EMBARGO': True}) @ddt.data("verify_student_start_flow", "verify_student_begin_flow") def test_embargo_allow(self, payment_flow): course = self._create_course("verified") @@ -1710,7 +1715,7 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): self.assertIn('JSON should be dict', response.content) self.assertEqual(response.status_code, 400) - @mock.patch( + @patch( 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature) ) @@ -1735,7 +1740,7 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): self.assertIn('Access key invalid', response.content) self.assertEqual(response.status_code, 400) - @mock.patch( + @patch( 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature) ) @@ -1760,14 +1765,17 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): self.assertIn('edX ID Invalid-Id not found', response.content) self.assertEqual(response.status_code, 400) - @mock.patch( + @patch( 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature) ) - def test_pass_result(self): + @patch('lms.djangoapps.verify_student.views.log.error') + @patch('lms.djangoapps.verify_student.utils.SailthruClient.send') + def test_passed_status_template(self, mock_sailthru_send, mock_log_error): """ Test for verification passed. """ + EmailMarketingConfiguration.objects.create(sailthru_verification_passed_template='test_template') data = { "EdX-ID": self.receipt_id, "Result": "PASS", @@ -1784,16 +1792,20 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) self.assertEqual(attempt.status, u'approved') self.assertEquals(response.content, 'OK!') - self.assertEqual(len(mail.outbox), 1) + self.assertFalse(mock_log_error.called) + self.assertTrue(mock_sailthru_send.call_args[1], 'test_template') - @mock.patch( + @patch( 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature) ) - def test_fail_result(self): + @patch('lms.djangoapps.verify_student.views.log.error') + @patch('sailthru.sailthru_client.SailthruClient.send') + def test_failed_status_template(self, mock_sailthru_send, mock_log_error): """ Test for failed verification. """ + EmailMarketingConfiguration.objects.create(sailthru_verification_failed_template='test_template') data = { "EdX-ID": self.receipt_id, "Result": 'FAIL', @@ -1813,9 +1825,10 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): self.assertEqual(attempt.error_code, u'Your photo doesn\'t meet standards.') self.assertEqual(attempt.error_msg, u'"Invalid photo"') self.assertEquals(response.content, 'OK!') - self.assertEqual(len(mail.outbox), 1) + self.assertFalse(mock_log_error.called) + self.assertTrue(mock_sailthru_send.call_args[1], 'test_template') - @mock.patch( + @patch( 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature) ) @@ -1841,7 +1854,7 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): self.assertEqual(attempt.error_msg, u'"Memory overflow"') self.assertEquals(response.content, 'OK!') - @mock.patch( + @patch( 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature) ) @@ -1865,29 +1878,6 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): ) self.assertIn('Result Unknown not understood', response.content) - @mock.patch( - 'lms.djangoapps.verify_student.utils.send_mail', - mock.Mock(side_effect=Exception()) - ) - def test_verification_status_email_not_sent(self): - """ - Test email is not sent in case of exception - """ - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - json_data = json.dumps(data) - self.client.post( - reverse('verify_student_results_callback'), data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - self.assertEqual(len(mail.outbox), 0) - @attr(shard=2) class TestReverifyView(TestCase): diff --git a/lms/djangoapps/verify_student/utils.py b/lms/djangoapps/verify_student/utils.py index f6f8c1b041..3193d0055d 100644 --- a/lms/djangoapps/verify_student/utils.py +++ b/lms/djangoapps/verify_student/utils.py @@ -13,6 +13,7 @@ from django.utils.translation import ugettext as _ from edxmako.shortcuts import render_to_string from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from sailthru import SailthruClient log = logging.getLogger(__name__) @@ -78,23 +79,18 @@ def verification_for_datetime(deadline, candidates): def send_verification_status_email(context): """ Send an email to inform learners about their verification status + using sailthru """ - subject = context['subject'] - message = render_to_string(context['message'], context['email_template_context']) - from_address = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) - to_address = context['email'] - - try: - send_mail(subject, message, from_address, [to_address], fail_silently=False) - except: # pylint: disable=bare-except - # We catch all exceptions and log them. - # It would be much, much worse to roll back the transaction due to an uncaught - # exception than to skip sending the notification email. - log.exception( - _("Could not send verification status email having subject: {subject} and email of user: {email}").format( - subject=context['subject'], - email=context['email'] - )) + sailthru_client = SailthruClient(context['email_config'].sailthru_key, context['email_config'].sailthru_secret) + sailthru_response = sailthru_client.send( + email=context['email'], template=context['template'], + _vars=context['template_vars'] + ) + if not sailthru_response.is_ok(): + error = sailthru_response.get_error() + log.error("Error attempting to send verification status email to user: {} via Sailthru: {}".format( + context['email'], error.get_message() + )) def most_recent_verification(photo_id_verifications, sso_id_verifications, most_recent_key): diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 12411264c0..b81c86dfc1 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -32,6 +32,7 @@ from pytz import UTC from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response, render_to_string +from email_marketing.models import EmailMarketingConfiguration from lms.djangoapps.commerce.utils import EcommerceService, is_account_activation_requirement_disabled from lms.djangoapps.verify_student.image import InvalidImageData, decode_image_data from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline @@ -1156,43 +1157,50 @@ def results_callback(request): except SoftwareSecurePhotoVerification.DoesNotExist: log.error("Software Secure posted back for receipt_id %s, but not found", receipt_id) return HttpResponseBadRequest("edX ID {} not found".format(receipt_id)) + user = attempt.user - context = { + email_config = EmailMarketingConfiguration.current() + verification_status_email_vars = { + 'platform_name': settings.PLATFORM_NAME, + } + email_context = { + 'email_config': email_config, 'email': user.email } - email_template_context = {'platform_name': settings.PLATFORM_NAME} if result == "PASS": log.debug("Approving verification for %s", receipt_id) attempt.approve() status = "approved" - expiry_date = datetime.date.today() + datetime.timedelta( - days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] - ) - email_template_context['full_name'] = user.profile.name - email_template_context['expiry_date'] = expiry_date.strftime("%m/%d/%Y") - context['email_template_context'] = email_template_context - context['subject'] = _("Your {platform_name} ID Verification Approved").format( - platform_name=settings.PLATFORM_NAME - ) - context['message'] = 'emails/successfull_verification_email.txt' - send_verification_status_email(context) + if email_config.sailthru_verification_passed_template: + expiry_date = datetime.date.today() + datetime.timedelta( + days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] + ) + verification_status_email_vars['expiry_date'] = expiry_date.strftime("%m/%d/%Y") + verification_status_email_vars['subject'] = _("Your {platform_name} ID Verification Approved").format( + platform_name=settings.PLATFORM_NAME + ) + verification_status_email_vars['full_name'] = user.profile.name + email_context['template'] = email_config.sailthru_verification_passed_template + email_context['template_vars'] = verification_status_email_vars + send_verification_status_email(email_context) elif result == "FAIL": log.debug("Denying verification for %s", receipt_id) attempt.deny(json.dumps(reason), error_code=error_code) status = "denied" - email_template_context['reason'] = reason - email_template_context['reverify_url'] = reverse("verify_student_reverify") - email_template_context['faq_url'] = configuration_helpers.get_value( - 'ID_VERIFICATION_SUPPORT_LINK', - settings.SUPPORT_SITE_LINK - ) - context['email_template_context'] = email_template_context - context['subject'] = _("Your {platform_name} Verification Has Been Denied").format( - platform_name=settings.PLATFORM_NAME - ) - context['message'] = 'emails/failed_verification_email.txt' - send_verification_status_email(context) + if email_config.sailthru_verification_failed_template: + verification_status_email_vars['reason'] = reason + verification_status_email_vars['reverify_url'] = reverse("verify_student_reverify") + verification_status_email_vars['faq_url'] = configuration_helpers.get_value( + 'ID_VERIFICATION_SUPPORT_LINK', + settings.SUPPORT_SITE_LINK + ) + verification_status_email_vars['subject'] = _("Your {platform_name} Verification Has Been Denied").format( + platform_name=settings.PLATFORM_NAME + ) + email_context['template'] = email_config.sailthru_verification_failed_template + email_context['template_vars'] = verification_status_email_vars + send_verification_status_email(email_context) elif result == "SYSTEM FAIL": log.debug("System failure for %s -- resetting to must_retry", receipt_id) diff --git a/lms/templates/emails/failed_verification_email.txt b/lms/templates/emails/failed_verification_email.txt deleted file mode 100644 index 365bce3a33..0000000000 --- a/lms/templates/emails/failed_verification_email.txt +++ /dev/null @@ -1,11 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> - -${_("Sorry! The photos you submitted for ID verification were not accepted, for the following reason(s):")} - -${_("The photo(s) of you: {reason}").format(reason=reason)} - -${_("Resubmit Verification: {reverify_url}").format(reverify_url=reverify_url)} -$(_("ID Verification FAQ: {faq_url}").format(faq_url=faq_url) - -${_("Thank you,")} -${_("The {platform_name} team").format(platform_name=platform_name)} diff --git a/lms/templates/emails/successfull_verification_email.txt b/lms/templates/emails/successfull_verification_email.txt deleted file mode 100644 index ad6e5e0969..0000000000 --- a/lms/templates/emails/successfull_verification_email.txt +++ /dev/null @@ -1,10 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> - -${_("Hi {full_name},").format(full_name=full_name)} - -${_("Congratulations! Your ID verification process was successful.")} - -${_("Your verification is effective for one year. It will expire on {expiry_date}").format(expiry_date=expiry_date)} - -${_("Thank you,")} -${_("The {platform_name} team").format(platform_name=platform_name)}