Commit d3a4747b by Diana Huang

Add new user_status functionality to PhotoVerification.

parent f3149651
......@@ -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
......@@ -175,20 +176,28 @@ class PhotoVerification(StatusModel):
##### 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):
"""
Return whether or not a user has satisfactorily proved their
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
......
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