Displaying verification denial reasons on dashboard
Learners now see (on the dashboard) a list of reasons why their verifications were denied. LEARNER-1486
This commit is contained in:
committed by
Clinton Blackburn
parent
f5769959dd
commit
dd1f834697
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,19 @@ from django.utils.translation import ugettext as _
|
||||
%elif verification_status in ['denied','must_reverify', 'must_retry']:
|
||||
<li class="status status-verification is-denied">
|
||||
<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">
|
||||
<a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user