Commit a975b838 by aamir-khan

ECOM-1476: sending email on software secure response initial work

parent 411df0ae
......@@ -1107,6 +1107,22 @@ class VerificationStatus(models.Model):
status="submitted"
).count()
@classmethod
def get_location_id(cls, photo_verification):
""" Return the location id of xblock
Args:
photo_verification(SoftwareSecurePhotoVerification): SoftwareSecurePhotoVerification object
Return:
Location Id of xblock if any else empty string
"""
try:
ver_status = cls.objects.filter(checkpoint__photo_verification=photo_verification).latest()
return ver_status.location_id
except cls.DoesNotExist:
return ""
class InCourseReverificationConfiguration(ConfigurationModel):
"""Configure in-course re-verification.
......
......@@ -772,6 +772,40 @@ class VerificationStatusTest(ModuleStoreTestCase):
list(self.check_point2.checkpoint_status.all().values_list('location_id', flat=True))
)
def test_get_location_id(self):
""" Getting location id for a specific checkpoint """
# creating software secure attempt against checkpoint
self.check_point1.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user))
# add initial verification status for checkpoint
VerificationStatus.add_verification_status(
checkpoint=self.check_point1,
user=self.user,
status='submitted',
location_id=self.dummy_reverification_item_id_1
)
attempt = SoftwareSecurePhotoVerification.objects.filter(user=self.user)
self.assertIsNotNone(VerificationStatus.get_location_id(attempt))
self.assertEqual(VerificationStatus.get_location_id(None), '')
def test_get_user_attempts(self):
# adding verification status
VerificationStatus.add_verification_status(
checkpoint=self.check_point1,
user=self.user,
status='submitted',
location_id=self.dummy_reverification_item_id_1
)
self.assertEqual(VerificationStatus.get_user_attempts(
course_key=self.course.id,
user_id=self.user.id,
related_assessment='midterm', location_id=self.dummy_reverification_item_id_1), 1)
class SkippedReverificationTest(ModuleStoreTestCase):
"""Tests for the SkippedReverification model. """
......
......@@ -9,7 +9,8 @@ from uuid import uuid4
from django.test.utils import override_settings
import mock
from mock import patch, Mock
from mock import patch, Mock, ANY
from django.utils import timezone
import pytz
import ddt
from django.test.client import Client
......@@ -24,8 +25,10 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import check_mongo_calls
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from microsite_configuration import microsite
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY
......@@ -38,16 +41,15 @@ from embargo.test_utils import restrict_course
from util.testing import UrlResetMixin
from verify_student.views import (
checkout_with_ecommerce_service,
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW,
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY,
PayAndVerifyView,
render_to_response,
render_to_response, PayAndVerifyView, EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW,
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, _send_email, _compose_message_reverification_email
)
from verify_student.models import (
SoftwareSecurePhotoVerification, VerificationCheckpoint,
InCourseReverificationConfiguration
InCourseReverificationConfiguration, VerificationStatus
)
from reverification.tests.factories import MidcourseReverificationWindowFactory
from util.date_utils import get_default_time_display
def mock_render_to_response(*args, **kwargs):
......@@ -1531,6 +1533,121 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
self.assertEquals(response.content, 'OK!')
self.assertIsNotNone(CourseEnrollment.objects.get(course_id=self.course_id))
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
def test_in_course_reverify_disabled(self):
"""
Test for verification passed.
"""
data = {
"EdX-ID": self.receipt_id,
"Result": "PASS",
"Reason": "",
"MessageType": "You have been verified."
}
json_data = json.dumps(data)
response = self.client.post(
reverse('verify_student_results_callback'), data=json_data,
content_type='application/json',
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
HTTP_DATE='testdate'
)
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
self.assertEqual(attempt.status, u'approved')
self.assertEquals(response.content, 'OK!')
# Verify that photo submission confirmation email was sent
self.assertEqual(len(mail.outbox), 0)
user_status = VerificationStatus.objects.filter(user=self.user).count()
self.assertEqual(user_status, 0)
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
def test_pass_in_course_reverify_result(self):
"""
Test for verification passed.
"""
self.create_reverification_xblock()
incourse_reverify_enabled = InCourseReverificationConfiguration.current()
incourse_reverify_enabled.enabled = True
incourse_reverify_enabled.save()
data = {
"EdX-ID": self.receipt_id,
"Result": "PASS",
"Reason": "",
"MessageType": "You have been verified."
}
json_data = json.dumps(data)
response = self.client.post(
reverse('verify_student_results_callback'), data=json_data,
content_type='application/json',
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
HTTP_DATE='testdate'
)
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
self.assertEqual(attempt.status, u'approved')
self.assertEquals(response.content, 'OK!')
# Verify that photo re-verification status email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual("Re-verification Status", mail.outbox[0].subject)
@mock.patch('verify_student.views._send_email')
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
def test_reverification_on_callback(self, mock_send_email):
"""
Test software secure callback flow for re-verification.
"""
# Create the 'edx-reverification-block' in course tree
self.create_reverification_xblock()
# create dummy data for software secure photo verification result callback
data = {
"EdX-ID": self.receipt_id,
"Result": "PASS",
"Reason": "",
"MessageType": "You have been verified."
}
json_data = json.dumps(data)
response = 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(response.content, 'OK!')
# now check that '_send_email' method is called on result callback
# with required parameters
subject = "Re-verification Status"
mock_send_email.assert_called_once_with(self.user.id, subject, ANY)
def create_reverification_xblock(self):
""" Create the reverification xblock
"""
# Create checkpoint
checkpoint = VerificationCheckpoint(course_id=self.course_id, checkpoint_name="midterm")
checkpoint.save()
# Add a re-verification attempt
checkpoint.add_verification_attempt(self.attempt)
# Create the 'edx-reverification-block' in course tree
section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection')
vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit')
reverification = ItemFactory.create(
parent=vertical,
category='edx-reverification-block',
display_name='Test Verification Block'
)
# Add a re-verification attempt status for the user
VerificationStatus.add_verification_status(checkpoint, self.user, "submitted", reverification.location)
class TestReverifyView(ModuleStoreTestCase):
"""
......@@ -1922,3 +2039,273 @@ class TestInCourseReverifyView(ModuleStoreTestCase):
"checkpoint_name": checkpoint,
"usage_id": unicode(self.reverification_location)
})
class TestEmailMessageWithCustomICRVBlock(ModuleStoreTestCase):
"""
Test email sending on re-verification
"""
def build_course(self):
"""
Build up a course tree with a Reverificaiton xBlock.
"""
# pylint: disable=attribute-defined-outside-init
self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course")
self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
self.due_date = datetime(2015, 6, 22, tzinfo=pytz.UTC)
# Create the course modes
for mode in ('audit', 'honor', 'verified'):
min_price = 0 if mode in ["honor", "audit"] else 1
CourseModeFactory(mode_slug=mode, course_id=self.course_key, min_price=min_price)
# Create the 'edx-reverification-block' in course tree
section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection')
vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit')
self.reverification = ItemFactory.create(
parent=vertical,
category='edx-reverification-block',
display_name='Test Verification Block',
metadata={'attempts': 3, 'due': self.due_date}
)
self.section_location = section.location
self.subsection_location = subsection.location
self.vertical_location = vertical.location
self.reverification_location = self.reverification.location
self.assessment = "midterm"
self.re_verification_link = reverse(
'verify_student_incourse_reverify',
args=(
unicode(self.course_key),
unicode(self.assessment),
unicode(self.reverification_location)
)
)
def setUp(self):
super(TestEmailMessageWithCustomICRVBlock, self).setUp()
self.build_course()
self.check_point = VerificationCheckpoint.objects.create(
course_id=self.course.id, checkpoint_name=self.assessment
)
self.check_point.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user))
VerificationStatus.add_verification_status(
checkpoint=self.check_point,
user=self.user,
status='submitted',
location_id=self.reverification_location
)
self.attempt = SoftwareSecurePhotoVerification.objects.filter(user=self.user)
def test_approved_email_message(self):
subject, body = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "approved", True
)
self.assertIn(
"Your verification for course {course_name} and assessment {assessment} has been passed.".format(
course_name=self.course.display_name_with_default,
assessment=self.assessment
),
body
)
self.assertIn("Re-verification Status", subject)
def test_denied_email_message_with_valid_due_date_and_attempts_allowed(self):
__, body = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "denied", True
)
self.assertIn(
"Your verification for course {course_name} and assessment {assessment} has failed.".format(
course_name=self.course.display_name_with_default,
assessment=self.assessment
),
body
)
self.assertIn("Assessment closes on {due_date}".format(due_date=get_default_time_display(self.due_date)), body)
self.assertIn("Click on link below to re-verify", body)
self.assertIn(
"https://{}{}".format(
microsite.get_value('SITE_NAME', 'localhost'), self.re_verification_link
),
body
)
def test_denied_email_message_with_close_verification_dates(self):
return_value = datetime(2016, 1, 1, tzinfo=timezone.utc)
with patch.object(timezone, 'now', return_value=return_value):
__, body = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "denied", True
)
self.assertIn(
"Your verification for course {course_name} and assessment {assessment} has failed.".format(
course_name=self.course.display_name_with_default,
assessment=self.assessment
),
body
)
self.assertIn("Assessment date has passed and retake not allowed", body)
def test_check_num_queries(self):
# Get the re-verification block to check the call made
with check_mongo_calls(2):
ver_block = modulestore().get_item(self.reverification_location)
# Expect that the verification block is fetched
self.assertIsNotNone(ver_block)
class TestEmailMessageWithDefaultICRVBlock(ModuleStoreTestCase):
"""
Test for In-course Re-verification
"""
def build_course(self):
"""
Build up a course tree with a Reverificaiton xBlock.
"""
# pylint: disable=attribute-defined-outside-init
self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course")
self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
# Create the course modes
for mode in ('audit', 'honor', 'verified'):
min_price = 0 if mode in ["honor", "audit"] else 1
CourseModeFactory(mode_slug=mode, course_id=self.course_key, min_price=min_price)
# Create the 'edx-reverification-block' in course tree
section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection')
vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit')
self.reverification = ItemFactory.create(
parent=vertical,
category='edx-reverification-block',
display_name='Test Verification Block'
)
self.section_location = section.location
self.subsection_location = subsection.location
self.vertical_location = vertical.location
self.reverification_location = self.reverification.location
self.assessment = "midterm"
self.re_verification_link = reverse(
'verify_student_incourse_reverify',
args=(
unicode(self.course_key),
unicode(self.assessment),
unicode(self.reverification_location)
)
)
def setUp(self):
super(TestEmailMessageWithDefaultICRVBlock, self).setUp()
self.build_course()
self.check_point = VerificationCheckpoint.objects.create(
course_id=self.course.id, checkpoint_name=self.assessment
)
self.check_point.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user))
self.attempt = SoftwareSecurePhotoVerification.objects.filter(user=self.user)
def test_denied_email_message_with_no_attempt_allowed(self):
VerificationStatus.add_verification_status(
checkpoint=self.check_point,
user=self.user,
status='submitted',
location_id=self.reverification_location
)
__, body = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "denied", True
)
self.assertIn(
"Your verification for course {course_name} and assessment {assessment} has failed.".format(
course_name=self.course.display_name_with_default,
assessment=self.assessment
),
body
)
self.assertIn("You have reached your allowed attempts limit. No more retakes allowed.", body)
def test_due_date(self):
self.reverification.due = datetime.now()
self.reverification.save()
VerificationStatus.add_verification_status(
checkpoint=self.check_point,
user=self.user,
status='submitted',
location_id=self.reverification_location
)
__, body = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "denied", True
)
self.assertIn(
"Your verification for course {course_name} and assessment {assessment} has failed.".format(
course_name=self.course.display_name_with_default,
assessment=self.assessment
),
body
)
self.assertIn("You have reached your allowed attempts limit. No more retakes allowed.", body)
def test_denied_email_message_with_no_due_date(self):
VerificationStatus.add_verification_status(
checkpoint=self.check_point,
user=self.user,
status='error',
location_id=self.reverification_location
)
__, body = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "denied", True
)
self.assertIn(
"Your verification for course {course_name} and assessment {assessment} has failed.".format(
course_name=self.course.display_name_with_default,
assessment=self.assessment
),
body
)
self.assertIn("Assessment is open and you have 1 attempt(s) remaining.", body)
self.assertIn("Click on link below to re-verify", body)
self.assertIn(
"https://{}{}".format(
microsite.get_value('SITE_NAME', 'localhost'), self.re_verification_link
),
body
)
def test_error_on_compose_email(self):
resp = _compose_message_reverification_email(
self.course.id, self.user.id, "midterm", self.attempt, "denied", True
)
self.assertIsNone(resp)
......@@ -10,6 +10,7 @@ from collections import namedtuple
from pytz import UTC
from django.utils import timezone
from ipware.ip import get_ip
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -58,6 +59,7 @@ from util.date_utils import get_default_time_display
from eventtracking import tracker
import analytics
from courseware.url_helpers import get_redirect_url
from django.contrib.auth.models import User
log = logging.getLogger(__name__)
......@@ -859,6 +861,96 @@ def submit_photos_for_verification(request):
return HttpResponse(200)
def _compose_message_reverification_email(
course_key, user_id, relates_assessment, photo_verification, status, is_secure
): # pylint: disable=invalid-name
""" Composes subject and message for email
Args:
course_key(CourseKey): CourseKey object
user_id(str): User Id
relates_assessment(str): related assessment name
photo_verification(QuerySet/SoftwareSecure): A query set of SoftwareSecure objects or SoftwareSecure objec
status(str): approval status
is_secure(Bool): Is running on secure protocol or not
Returns:
None if any error occurred else Tuple of subject and message strings
"""
try:
location_id = VerificationStatus.get_location_id(photo_verification)
usage_key = UsageKey.from_string(location_id)
course = modulestore().get_course(course_key)
redirect_url = get_redirect_url(course_key, usage_key.replace(course_key=course_key))
subject = "Re-verification Status"
context = {
"status": status,
"course_name": course.display_name_with_default,
"assessment": relates_assessment,
"courseware_url": redirect_url
}
reverification_block = modulestore().get_item(usage_key)
# Allowed attempts is 1 if not set on verification block
allowed_attempts = 1 if reverification_block.attempts == 0 else reverification_block.attempts
user_attempts = VerificationStatus.get_user_attempts(user_id, course_key, relates_assessment, location_id)
left_attempts = allowed_attempts - user_attempts
is_attempt_allowed = left_attempts > 0
verification_open = True
if reverification_block.due:
verification_open = timezone.now() <= reverification_block.due
context["left_attempts"] = left_attempts
context["is_attempt_allowed"] = is_attempt_allowed
context["verification_open"] = verification_open
context["due_date"] = get_default_time_display(reverification_block.due)
context["is_secure"] = is_secure
context["site"] = microsite.get_value('SITE_NAME', 'localhost')
context['platform_name'] = microsite.get_value('platform_name', settings.PLATFORM_NAME),
re_verification_link = reverse(
'verify_student_incourse_reverify',
args=(
unicode(course_key),
unicode(relates_assessment),
unicode(location_id)
)
)
context["reverify_link"] = re_verification_link
message = render_to_string('emails/reverification_processed.txt', context)
log.info(
"Sending email to User_Id=%s. Attempts left for this user are %s. "
"Allowed attempts %s. "
"Due Date %s",
str(user_id), left_attempts, allowed_attempts, str(reverification_block.due)
)
return subject, message
# Catch all exception to avoid raising back to view
except: # pylint: disable=bare-except
log.exception("The email for re-verification sending failed for user_id %s", user_id)
def _send_email(user_id, subject, message):
""" Send email to given user
Args:
user_id(str): User Id
subject(str): Subject lines of emails
message(str): Email message body
Returns:
None
"""
from_address = microsite.get_value(
'email_from_address',
settings.DEFAULT_FROM_EMAIL
)
user = User.objects.get(id=user_id)
user.email_user(subject, message, from_address)
@require_POST
@csrf_exempt # SS does its own message signing, and their API won't have a cookie value
def results_callback(request):
......@@ -910,26 +1002,24 @@ def results_callback(request):
try:
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=receipt_id)
except SoftwareSecurePhotoVerification.DoesNotExist:
log.error("Software Secure posted back for receipt_id {}, but not found".format(receipt_id))
log.error("Software Secure posted back for receipt_id %s, but not found", receipt_id)
return HttpResponseBadRequest("edX ID {} not found".format(receipt_id))
checkpoints = VerificationCheckpoint.objects.filter(photo_verification=attempt).all()
if result == "PASS":
log.debug("Approving verification for {}".format(receipt_id))
log.debug("Approving verification for %s", receipt_id)
attempt.approve()
status = "approved"
elif result == "FAIL":
log.debug("Denying verification for {}".format(receipt_id))
log.debug("Denying verification for %s", receipt_id)
attempt.deny(json.dumps(reason), error_code=error_code)
status = "denied"
elif result == "SYSTEM FAIL":
log.debug("System failure for {} -- resetting to must_retry".format(receipt_id))
log.debug("System failure for %s -- resetting to must_retry", receipt_id)
attempt.system_error(json.dumps(reason), error_code=error_code)
status = "error"
log.error("Software Secure callback attempt for %s failed: %s", receipt_id, reason)
else:
log.error("Software Secure returned unknown result {}".format(result))
log.error("Software Secure returned unknown result %s", result)
return HttpResponseBadRequest(
"Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL".format(result)
)
......@@ -939,7 +1029,22 @@ def results_callback(request):
course_id = attempt.window.course_id
course_enrollment = CourseEnrollment.get_or_create_enrollment(attempt.user, course_id)
course_enrollment.emit_event(EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE)
incourse_reverify_enabled = InCourseReverificationConfiguration.current().enabled
if incourse_reverify_enabled:
checkpoints = VerificationCheckpoint.objects.filter(photo_verification=attempt).all()
VerificationStatus.add_status_from_checkpoints(checkpoints=checkpoints, user=attempt.user, status=status)
# If this is re-verification then send the update email
if checkpoints:
user_id = attempt.user.id
course_key = checkpoints[0].course_id
relates_assessment = checkpoints[0].checkpoint_name
subject, message = _compose_message_reverification_email(
course_key, user_id, relates_assessment, attempt, status, request.is_secure()
)
_send_email(user_id, subject, message)
return HttpResponse("OK!")
......
<%namespace file="../main.html" import="stanford_theme_enabled" />
<%! from django.utils.translation import ugettext as _ %>
% if status == "approved":
${_("Your verification for course {course_name} and assessment {assessment} "
"has been passed."
).format(course_name=course_name, assessment=assessment)}
%else:
${_("Your verification for course {course_name} and assessment {assessment} "
"has failed."
).format(course_name=course_name, assessment=assessment)}
% if not is_attempt_allowed:
${_("You have reached your allowed attempts limit. No more retakes allowed.")}
% elif not verification_open:
${_("Assessment date has passed and retake not allowed.")}
% else:
% if due_date:
${_("Assessment closes on {due_date}.".format(due_date=due_date))}
% else:
${_("Assessment is open and you have {left_attempts} attempt(s) remaining.".format(left_attempts=left_attempts))}
% endif
${_("Click on link below to re-verify:")}
% if is_secure:
https://${ site }${ reverify_link }
% else:
http://${ site }${ reverify_link }
% endif
% endif
% endif
${_("Click on link below to go to the courseware:")}
% if is_secure:
https://${ site }${ courseware_url }
% else:
http://${ site }${ courseware_url }
% endif
${_("The {platform_name} Team.").format(platform_name=platform_name)}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment