diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 966324d8e6..c9d1870931 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -788,7 +788,8 @@ def dashboard(request): # Verification Attempts # Used to generate the "you must reverify for course x" banner - verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user) + verification_status, verification_error_codes = SoftwareSecurePhotoVerification.user_status(user) + verification_errors = get_verification_error_reasons_for_display(verification_error_codes) # Gets data for midcourse reverifications, if any are necessary or have failed statuses = ["approved", "denied", "pending", "must_reverify"] @@ -866,7 +867,7 @@ def dashboard(request): 'reverifications': reverifications, 'verification_status': verification_status, 'verification_status_by_course': verify_status_by_course, - 'verification_msg': verification_msg, + 'verification_errors': verification_errors, 'show_refund_option_for': show_refund_option_for, 'block_courses': block_courses, 'denied_banner': denied_banner, @@ -898,6 +899,27 @@ def dashboard(request): return response +def get_verification_error_reasons_for_display(verification_error_codes): + verification_errors = [] + verification_error_map = { + 'photos_mismatched': _('Photos are mismatched'), + 'id_image_missing_name': _('Name missing from ID photo'), + 'id_image_missing': _('ID photo not provided'), + 'id_invalid': _('ID is invalid'), + 'user_image_not_clear': _('Learner photo is blurry'), + 'name_mismatch': _('Name on ID does not match name on account'), + 'user_image_missing': _('Learner photo not provided'), + 'id_image_not_clear': _('ID photo is blurry'), + } + + for error in verification_error_codes: + error_text = verification_error_map.get(error) + if error_text: + verification_errors.append(error_text) + + return verification_errors + + def _create_recent_enrollment_message(course_enrollments, course_modes): # pylint: disable=invalid-name """ Builds a recent course enrollment message. diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index beb8b6848e..4c36e6d6da 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -18,6 +18,7 @@ from email.utils import formatdate import pytz import requests +import six from config_models.models import ConfigurationModel from django.conf import settings from django.contrib.auth.models import User @@ -742,33 +743,43 @@ class SoftwareSecurePhotoVerification(PhotoVerification): `[{"photoIdReasons": ["Not provided"]}]` - Returns a list of error messages + Returns: + str[]: List of error messages. """ - # Translates the category names and messages into something more human readable - message_dict = { - ("photoIdReasons", "Not provided"): _("No photo ID was provided."), - ("photoIdReasons", "Text not clear"): _("We couldn't read your name from your photo ID image."), - ("generalReasons", "Name mismatch"): _("The name associated with your account and the name on your ID do not match."), - ("userPhotoReasons", "Image not clear"): _("The image of your face was not clear."), - ("userPhotoReasons", "Face out of view"): _("Your face was not visible in your self-photo."), + parsed_errors = [] + error_map = { + 'EdX name not provided': 'name_mismatch', + 'Name mismatch': 'name_mismatch', + 'Photo/ID Photo mismatch': 'photos_mismatched', + 'ID name not provided': 'id_image_missing_name', + 'Invalid Id': 'id_invalid', + 'No text': 'id_invalid', + 'Not provided': 'id_image_missing', + 'Photo hidden/No photo': 'id_image_not_clear', + 'Text not clear': 'id_image_not_clear', + 'Face out of view': 'user_image_not_clear', + 'Image not clear': 'user_image_not_clear', + 'Photo not provided': 'user_image_missing', } try: - msg_json = json.loads(self.error_msg) - msg_dict = msg_json[0] + messages = set() + message_groups = json.loads(self.error_msg) - msg = [] - for category in msg_dict: - # find the messages associated with this category - category_msgs = msg_dict[category] - for category_msg in category_msgs: - msg.append(message_dict[(category, category_msg)]) - return u", ".join(msg) - except (ValueError, KeyError): - # if we can't parse the message as JSON or the category doesn't - # match one of our known categories, show a generic error - log.error('PhotoVerification: Error parsing this error message: %s', self.error_msg) - return _("There was an error verifying your ID photos.") + for message_group in message_groups: + messages = messages.union(set(*six.itervalues(message_group))) + + for message in messages: + parsed_error = error_map.get(message) + + if parsed_error: + parsed_errors.append(parsed_error) + else: + log.debug('Ignoring photo verification error message: %s', message) + except Exception: # pylint: disable=broad-except + log.exception('Failed to parse error message for SoftwareSecurePhotoVerification %d', self.pk) + + return parsed_errors def image_url(self, name, override_receipt_id=None): """ diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 35efe8b57c..8b92027792 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -325,20 +325,15 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase): self.assertEquals(status, ('none', '')) # test for when one has been created - attempt = SoftwareSecurePhotoVerification(user=user) - attempt.status = 'approved' - attempt.save() - + attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='approved') status = SoftwareSecurePhotoVerification.user_status(user) self.assertEquals(status, ('approved', '')) # create another one for the same user, make sure the right one is # returned - attempt2 = SoftwareSecurePhotoVerification(user=user) - attempt2.status = 'denied' - attempt2.error_msg = '[{"photoIdReasons": ["Not provided"]}]' - attempt2.save() - + SoftwareSecurePhotoVerification.objects.create( + user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]' + ) status = SoftwareSecurePhotoVerification.user_status(user) self.assertEquals(status, ('approved', '')) @@ -346,31 +341,26 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase): # properly attempt.delete() status = SoftwareSecurePhotoVerification.user_status(user) - self.assertEquals(status, ('must_reverify', "No photo ID was provided.")) + self.assertEquals(status, ('must_reverify', ['id_image_missing'])) + # pylint: disable=line-too-long def test_parse_error_msg_success(self): user = UserFactory.create() attempt = SoftwareSecurePhotoVerification(user=user) attempt.status = 'denied' - attempt.error_msg = '[{"photoIdReasons": ["Not provided"]}]' + attempt.error_msg = '[{"userPhotoReasons": ["Face out of view"]}, {"photoIdReasons": ["Photo hidden/No photo", "ID name not provided"]}]' parsed_error_msg = attempt.parsed_error_msg() - self.assertEquals("No photo ID was provided.", parsed_error_msg) + self.assertEquals(parsed_error_msg, ['id_image_missing_name', 'user_image_not_clear', 'id_image_not_clear']) - def test_parse_error_msg_failure(self): + @ddt.data( + 'Not Provided', + '{"IdReasons": ["Not provided"]}', + u'[{"ïḋṚëäṡöṅṡ": ["Ⓝⓞⓣ ⓟⓡⓞⓥⓘⓓⓔⓓ "]}]', + ) + def test_parse_error_msg_failure(self, msg): user = UserFactory.create() - attempt = SoftwareSecurePhotoVerification(user=user) - attempt.status = 'denied' - # when we can't parse into json - bad_messages = { - 'Not Provided', - '[{"IdReasons": ["Not provided"]}]', - '{"IdReasons": ["Not provided"]}', - u'[{"ïḋṚëäṡöṅṡ": ["Ⓝⓞⓣ ⓟⓡⓞⓥⓘⓓⓔⓓ "]}]', - } - for msg in bad_messages: - attempt.error_msg = msg - parsed_error_msg = attempt.parsed_error_msg() - self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.") + attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='denied', error_msg=msg) + self.assertEqual(attempt.parsed_error_msg(), []) def test_active_at_datetime(self): user = UserFactory.create() diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 2c900bd25e..578669b3c2 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -1179,7 +1179,7 @@ class ReverifyView(View): Most of the work is done client-side by composing the same Backbone views used in the initial verification flow. """ - status, _ = SoftwareSecurePhotoVerification.user_status(request.user) + status, __ = SoftwareSecurePhotoVerification.user_status(request.user) expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(request.user) can_reverify = False diff --git a/lms/templates/dashboard/_dashboard_status_verification.html b/lms/templates/dashboard/_dashboard_status_verification.html index 28fcd7def8..2dce1a12af 100644 --- a/lms/templates/dashboard/_dashboard_status_verification.html +++ b/lms/templates/dashboard/_dashboard_status_verification.html @@ -18,7 +18,19 @@ from django.utils.translation import ugettext as _ %elif verification_status in ['denied','must_reverify', 'must_retry']:
${_("Your verification submission was not accepted. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}
+
+ ${_("Your verification submission was not accepted. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}
+
+ %if verification_errors:
+
+ ${_("Your verification was denied for the following reasons:")}
+