Merge pull request #7864 from edx/aamir-khan/ECOM-1476-send-email-on-reverification
ECOM-1476: sending email on software secure response initial work
This commit is contained in:
@@ -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)
|
||||
VerificationStatus.add_status_from_checkpoints(checkpoints=checkpoints, user=attempt.user, status=status)
|
||||
|
||||
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!")
|
||||
|
||||
|
||||
|
||||
43
lms/templates/emails/reverification_processed.txt
Normal file
43
lms/templates/emails/reverification_processed.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
<%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)}
|
||||
Reference in New Issue
Block a user