Add new user_status functionality to PhotoVerification.
This commit is contained in:
@@ -47,6 +47,8 @@ from student.models import (
|
||||
)
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
@@ -334,6 +336,8 @@ def dashboard(request):
|
||||
CourseAuthorization.instructor_email_enabled(course.id)
|
||||
)
|
||||
)
|
||||
# Verification Attempts
|
||||
verification_status = SoftwareSecurePhotoVerification.user_status(user)
|
||||
# get info w.r.t ExternalAuthMap
|
||||
external_auth_map = None
|
||||
try:
|
||||
@@ -351,6 +355,8 @@ def dashboard(request):
|
||||
'all_course_modes': course_modes,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'verification_status': verification_status[0],
|
||||
'verification_msg': verification_status[1],
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
@@ -26,6 +26,7 @@ from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
from model_utils.models import StatusModel
|
||||
from model_utils import Choices
|
||||
|
||||
@@ -174,6 +175,17 @@ class PhotoVerification(StatusModel):
|
||||
ordering = ['-created_at']
|
||||
|
||||
##### Methods listed in the order you'd typically call them
|
||||
@classmethod
|
||||
def _earliest_allowed_date(cls):
|
||||
"""
|
||||
Returns the earliest allowed date given the settings
|
||||
|
||||
"""
|
||||
allowed_date = (
|
||||
datetime.now(pytz.UTC) - timedelta(days=cls.DAYS_GOOD_FOR)
|
||||
)
|
||||
return allowed_date
|
||||
|
||||
@classmethod
|
||||
def user_is_verified(cls, user, earliest_allowed_date=None):
|
||||
"""
|
||||
@@ -181,14 +193,11 @@ class PhotoVerification(StatusModel):
|
||||
identity. Depending on the policy, this can expire after some period of
|
||||
time, so a user might have to renew periodically.
|
||||
"""
|
||||
earliest_allowed_date = (
|
||||
earliest_allowed_date or
|
||||
datetime.now(pytz.UTC) - timedelta(days=cls.DAYS_GOOD_FOR)
|
||||
)
|
||||
return cls.objects.filter(
|
||||
user=user,
|
||||
status="approved",
|
||||
created_at__gte=earliest_allowed_date
|
||||
created_at__gte=(earliest_allowed_date
|
||||
or cls._earliest_allowed_date())
|
||||
).exists()
|
||||
|
||||
@classmethod
|
||||
@@ -201,14 +210,11 @@ class PhotoVerification(StatusModel):
|
||||
on the contents of the attempt, and we have not yet received a denial.
|
||||
"""
|
||||
valid_statuses = ['must_retry', 'submitted', 'approved']
|
||||
earliest_allowed_date = (
|
||||
earliest_allowed_date or
|
||||
datetime.now(pytz.UTC) - timedelta(days=cls.DAYS_GOOD_FOR)
|
||||
)
|
||||
return cls.objects.filter(
|
||||
user=user,
|
||||
status__in=valid_statuses,
|
||||
created_at__gte=earliest_allowed_date
|
||||
created_at__gte=(earliest_allowed_date
|
||||
or cls._earliest_allowed_date())
|
||||
).exists()
|
||||
|
||||
@classmethod
|
||||
@@ -225,6 +231,38 @@ class PhotoVerification(StatusModel):
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def user_status(cls, user):
|
||||
"""
|
||||
Returns the status of the user based on their latest verification attempt
|
||||
|
||||
If no such verification exists, returns 'none'
|
||||
If verification has expired, returns 'expired'
|
||||
"""
|
||||
try:
|
||||
attempts = cls.objects.filter(user=user).order_by('-updated_at')
|
||||
attempt = attempts[0]
|
||||
except IndexError:
|
||||
return ('none', '')
|
||||
|
||||
if attempt.created_at < cls._earliest_allowed_date():
|
||||
return ('expired', '')
|
||||
|
||||
error_msg = attempt.error_msg
|
||||
if attempt.error_msg:
|
||||
error_msg = attempt.parse_error_msg()
|
||||
|
||||
return (attempt.status, error_msg)
|
||||
|
||||
def parse_error_msg(self):
|
||||
"""
|
||||
Sometimes, the error message we've received needs to be parsed into
|
||||
something more human readable
|
||||
|
||||
The default behavior is to return the current error message as is.
|
||||
"""
|
||||
return self.error_msg
|
||||
|
||||
@status_before_must_be("created")
|
||||
def upload_face_image(self, img):
|
||||
raise NotImplementedError
|
||||
@@ -486,6 +524,37 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
self.status = "must_retry"
|
||||
self.save()
|
||||
|
||||
def parse_error_msg(self):
|
||||
"""
|
||||
Parse the error messages we receive from SoftwareSecure
|
||||
|
||||
Error messages are written in the form:
|
||||
|
||||
`[{"photoIdReasons": ["Not provided"]}]`
|
||||
|
||||
Returns a list of error messages
|
||||
"""
|
||||
# Translates the category names into something more human readable
|
||||
category_dict = {
|
||||
"photoIdReasons": _("Photo ID Issues: "),
|
||||
"generalReasons": u""
|
||||
}
|
||||
|
||||
try:
|
||||
msg_json = json.loads(self.error_msg)
|
||||
msg_dict = msg_json[0]
|
||||
|
||||
msg = []
|
||||
for category in msg_dict:
|
||||
# translate the category into a human-readable name
|
||||
category_name = category_dict[category]
|
||||
msg.append(category_name + u", ".join(msg_dict[category]))
|
||||
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
|
||||
return _("There was an error verifying your ID photos.")
|
||||
|
||||
def image_url(self, name):
|
||||
"""
|
||||
We dynamically generate this, since we want it the expiration clock to
|
||||
|
||||
@@ -17,11 +17,11 @@ from util.testing import UrlResetMixin
|
||||
import verify_student.models
|
||||
|
||||
FAKE_SETTINGS = {
|
||||
"SOFTWARE_SECURE" : {
|
||||
"SOFTWARE_SECURE": {
|
||||
"FACE_IMAGE_AES_KEY" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"API_ACCESS_KEY" : "BBBBBBBBBBBBBBBBBBBB",
|
||||
"API_SECRET_KEY" : "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
"RSA_PUBLIC_KEY" : """-----BEGIN PUBLIC KEY-----
|
||||
"API_ACCESS_KEY": "BBBBBBBBBBBBBBBBBBBB",
|
||||
"API_SECRET_KEY": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
"RSA_PUBLIC_KEY": """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu2fUn20ZQtDpa1TKeCA/
|
||||
rDA2cEeFARjEr41AP6jqP/k3O7TeqFX6DgCBkxcjojRCs5IfE8TimBHtv/bcSx9o
|
||||
7PANTq/62ZLM9xAMpfCcU6aAd4+CVqQkXSYjj5TUqamzDFBkp67US8IPmw7I2Gaa
|
||||
@@ -30,10 +30,10 @@ dyZCM9pBcvcH+60ma+nNg8GVGBAW/oLxILBtg+T3PuXSUvcu/r6lUFMHk55pU94d
|
||||
9A/T8ySJm379qU24ligMEetPk1o9CUasdaI96xfXVDyFhrzrntAmdD+HYCSPOQHz
|
||||
iwIDAQAB
|
||||
-----END PUBLIC KEY-----""",
|
||||
"API_URL" : "http://localhost/verify_student/fake_endpoint",
|
||||
"AWS_ACCESS_KEY" : "FAKEACCESSKEY",
|
||||
"AWS_SECRET_KEY" : "FAKESECRETKEY",
|
||||
"S3_BUCKET" : "fake-bucket"
|
||||
"API_URL": "http://localhost/verify_student/fake_endpoint",
|
||||
"AWS_ACCESS_KEY": "FAKEACCESSKEY",
|
||||
"AWS_SECRET_KEY": "FAKESECRETKEY",
|
||||
"S3_BUCKET": "fake-bucket"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,13 @@ class MockKey(object):
|
||||
def generate_url(self, duration):
|
||||
return "http://fake-edx-s3.edx.org/"
|
||||
|
||||
|
||||
class MockBucket(object):
|
||||
"""Mocking a boto S3 Bucket object."""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
|
||||
class MockS3Connection(object):
|
||||
"""Mocking a boto S3 Connection"""
|
||||
def __init__(self, access_key, secret_key):
|
||||
@@ -165,14 +167,14 @@ class TestPhotoVerification(TestCase):
|
||||
|
||||
# approved
|
||||
assert_raises(VerificationException, attempt.submit)
|
||||
attempt.approve() # no-op
|
||||
attempt.system_error("System error") # no-op, something processed it without error
|
||||
attempt.approve() # no-op
|
||||
attempt.system_error("System error") # no-op, something processed it without error
|
||||
attempt.deny(DENY_ERROR_MSG)
|
||||
|
||||
# denied
|
||||
assert_raises(VerificationException, attempt.submit)
|
||||
attempt.deny(DENY_ERROR_MSG) # no-op
|
||||
attempt.system_error("System error") # no-op, something processed it without error
|
||||
attempt.deny(DENY_ERROR_MSG) # no-op
|
||||
attempt.system_error("System error") # no-op, something processed it without error
|
||||
attempt.approve()
|
||||
|
||||
def test_name_freezing(self):
|
||||
@@ -307,3 +309,49 @@ class TestPhotoVerification(TestCase):
|
||||
attempt.save()
|
||||
assert_true(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user), status)
|
||||
|
||||
def test_user_status(self):
|
||||
# test for correct status when no error returned
|
||||
user = UserFactory.create()
|
||||
status = SoftwareSecurePhotoVerification.user_status(user)
|
||||
self.assertEquals(status, ('none', ''))
|
||||
|
||||
# test for when one has been created
|
||||
attempt = SoftwareSecurePhotoVerification(user=user)
|
||||
attempt.status = 'approved'
|
||||
attempt.save()
|
||||
|
||||
status = SoftwareSecurePhotoVerification.user_status(user)
|
||||
self.assertEquals(status, (attempt.status, ''))
|
||||
|
||||
# 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()
|
||||
|
||||
status = SoftwareSecurePhotoVerification.user_status(user)
|
||||
self.assertEquals(status, (attempt2.status, "Photo ID Issues: Not provided"))
|
||||
|
||||
def test_parse_error_msg_success(self):
|
||||
user = UserFactory.create()
|
||||
attempt = SoftwareSecurePhotoVerification(user=user)
|
||||
attempt.status = 'denied'
|
||||
attempt.error_msg = '[{"photoIdReasons": ["Not provided"]}]'
|
||||
parsed_error_msg = attempt.parse_error_msg()
|
||||
self.assertEquals("Photo ID Issues: Not provided", parsed_error_msg)
|
||||
|
||||
def test_parse_error_msg_failure(self):
|
||||
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"]}',
|
||||
}
|
||||
for msg in bad_messages:
|
||||
attempt.error_msg = msg
|
||||
parsed_error_msg = attempt.parse_error_msg()
|
||||
self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.")
|
||||
|
||||
@@ -14,6 +14,7 @@ from mock import patch, Mock, ANY
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
@@ -98,6 +99,7 @@ class TestReverifyView(TestCase):
|
||||
self.assertIn('photo_reverification', template)
|
||||
self.assertTrue(context['error'])
|
||||
|
||||
@patch.dict(settings.MITX_FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_reverify_post_success(self):
|
||||
url = reverse('verify_student_reverify')
|
||||
response = self.client.post(url, {'face_image': ',',
|
||||
|
||||
@@ -98,6 +98,9 @@ MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
|
||||
MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True
|
||||
MITX_FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False
|
||||
|
||||
# Don't actually send any requests to Software Secure for student identity
|
||||
# verification.
|
||||
MITX_FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True
|
||||
|
||||
# Configure the payment processor to use the fake processing page
|
||||
# Since both the fake payment page and the shoppingcart app are using
|
||||
|
||||
@@ -36,10 +36,6 @@ MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
|
||||
|
||||
MITX_FEATURES['ENABLE_SHOPPING_CART'] = True
|
||||
|
||||
# Don't actually send any requests to Software Secure for student identity
|
||||
# verification.
|
||||
MITX_FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True
|
||||
|
||||
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
|
||||
WIKI_ENABLED = True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user