Commit dd1f8346 by Clinton Blackburn Committed by Clinton Blackburn

Displaying verification denial reasons on dashboard

Learners now see (on the dashboard) a list of reasons why their verifications were denied.

LEARNER-1486
parent f5769959
...@@ -788,7 +788,8 @@ def dashboard(request): ...@@ -788,7 +788,8 @@ def dashboard(request):
# Verification Attempts # Verification Attempts
# Used to generate the "you must reverify for course x" banner # 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 # Gets data for midcourse reverifications, if any are necessary or have failed
statuses = ["approved", "denied", "pending", "must_reverify"] statuses = ["approved", "denied", "pending", "must_reverify"]
...@@ -866,7 +867,7 @@ def dashboard(request): ...@@ -866,7 +867,7 @@ def dashboard(request):
'reverifications': reverifications, 'reverifications': reverifications,
'verification_status': verification_status, 'verification_status': verification_status,
'verification_status_by_course': verify_status_by_course, 'verification_status_by_course': verify_status_by_course,
'verification_msg': verification_msg, 'verification_errors': verification_errors,
'show_refund_option_for': show_refund_option_for, 'show_refund_option_for': show_refund_option_for,
'block_courses': block_courses, 'block_courses': block_courses,
'denied_banner': denied_banner, 'denied_banner': denied_banner,
...@@ -898,6 +899,27 @@ def dashboard(request): ...@@ -898,6 +899,27 @@ def dashboard(request):
return response 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 def _create_recent_enrollment_message(course_enrollments, course_modes): # pylint: disable=invalid-name
""" """
Builds a recent course enrollment message. Builds a recent course enrollment message.
......
...@@ -18,6 +18,7 @@ from email.utils import formatdate ...@@ -18,6 +18,7 @@ from email.utils import formatdate
import pytz import pytz
import requests import requests
import six
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -742,33 +743,43 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -742,33 +743,43 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
`[{"photoIdReasons": ["Not provided"]}]` `[{"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 parsed_errors = []
message_dict = { error_map = {
("photoIdReasons", "Not provided"): _("No photo ID was provided."), 'EdX name not provided': 'name_mismatch',
("photoIdReasons", "Text not clear"): _("We couldn't read your name from your photo ID image."), 'Name mismatch': 'name_mismatch',
("generalReasons", "Name mismatch"): _("The name associated with your account and the name on your ID do not match."), 'Photo/ID Photo mismatch': 'photos_mismatched',
("userPhotoReasons", "Image not clear"): _("The image of your face was not clear."), 'ID name not provided': 'id_image_missing_name',
("userPhotoReasons", "Face out of view"): _("Your face was not visible in your self-photo."), '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: try:
msg_json = json.loads(self.error_msg) messages = set()
msg_dict = msg_json[0] message_groups = json.loads(self.error_msg)
msg = [] for message_group in message_groups:
for category in msg_dict: messages = messages.union(set(*six.itervalues(message_group)))
# find the messages associated with this category
category_msgs = msg_dict[category] for message in messages:
for category_msg in category_msgs: parsed_error = error_map.get(message)
msg.append(message_dict[(category, category_msg)])
return u", ".join(msg) if parsed_error:
except (ValueError, KeyError): parsed_errors.append(parsed_error)
# if we can't parse the message as JSON or the category doesn't else:
# match one of our known categories, show a generic error log.debug('Ignoring photo verification error message: %s', message)
log.error('PhotoVerification: Error parsing this error message: %s', self.error_msg) except Exception: # pylint: disable=broad-except
return _("There was an error verifying your ID photos.") log.exception('Failed to parse error message for SoftwareSecurePhotoVerification %d', self.pk)
return parsed_errors
def image_url(self, name, override_receipt_id=None): def image_url(self, name, override_receipt_id=None):
""" """
......
...@@ -325,20 +325,15 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase): ...@@ -325,20 +325,15 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
self.assertEquals(status, ('none', '')) self.assertEquals(status, ('none', ''))
# test for when one has been created # test for when one has been created
attempt = SoftwareSecurePhotoVerification(user=user) attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
attempt.status = 'approved'
attempt.save()
status = SoftwareSecurePhotoVerification.user_status(user) status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('approved', '')) self.assertEquals(status, ('approved', ''))
# create another one for the same user, make sure the right one is # create another one for the same user, make sure the right one is
# returned # returned
attempt2 = SoftwareSecurePhotoVerification(user=user) SoftwareSecurePhotoVerification.objects.create(
attempt2.status = 'denied' user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
attempt2.error_msg = '[{"photoIdReasons": ["Not provided"]}]' )
attempt2.save()
status = SoftwareSecurePhotoVerification.user_status(user) status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('approved', '')) self.assertEquals(status, ('approved', ''))
...@@ -346,31 +341,26 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase): ...@@ -346,31 +341,26 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
# properly # properly
attempt.delete() attempt.delete()
status = SoftwareSecurePhotoVerification.user_status(user) 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): def test_parse_error_msg_success(self):
user = UserFactory.create() user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user) attempt = SoftwareSecurePhotoVerification(user=user)
attempt.status = 'denied' 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() 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() user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user) attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='denied', error_msg=msg)
attempt.status = 'denied' self.assertEqual(attempt.parsed_error_msg(), [])
# 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.")
def test_active_at_datetime(self): def test_active_at_datetime(self):
user = UserFactory.create() user = UserFactory.create()
......
...@@ -1179,7 +1179,7 @@ class ReverifyView(View): ...@@ -1179,7 +1179,7 @@ class ReverifyView(View):
Most of the work is done client-side by composing the same Most of the work is done client-side by composing the same
Backbone views used in the initial verification flow. 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) expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(request.user)
can_reverify = False can_reverify = False
......
...@@ -18,7 +18,19 @@ from django.utils.translation import ugettext as _ ...@@ -18,7 +18,19 @@ from django.utils.translation import ugettext as _
%elif verification_status in ['denied','must_reverify', 'must_retry']: %elif verification_status in ['denied','must_reverify', 'must_retry']:
<li class="status status-verification is-denied"> <li class="status status-verification is-denied">
<span class="title status-title">${_("Current Verification Status: Denied")}</span> <span class="title status-title">${_("Current Verification Status: Denied")}</span>
<p class="status-note">${_("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.")}</p> <p class="status-note">
${_("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:
<br><br>
${_("Your verification was denied for the following reasons:")}<br>
<ul>
%for error in verification_errors:
<li>${error}</li>
%endfor
</ul>
%endif
</p>
<div class="btn-reverify"> <div class="btn-reverify">
<a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a> <a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div> </div>
......
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