diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py
index 4b990984d93c..3029a9339499 100644
--- a/lms/djangoapps/verify_student/tests/test_views.py
+++ b/lms/djangoapps/verify_student/tests/test_views.py
@@ -9,11 +9,14 @@
---> To Payment
"""
+import json
+import mock
import urllib
from mock import patch, Mock
import pytz
from datetime import timedelta, datetime
+from django.test.client import Client
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
@@ -31,6 +34,13 @@
from verify_student.models import SoftwareSecurePhotoVerification
from reverification.tests.factories import MidcourseReverificationWindowFactory
+FAKE_SETTINGS = {
+ "SOFTWARE_SECURE":
+ {
+ "API_ACCESS_KEY": "BBBBBBBBBBBBBBBBBBBB",
+ "API_SECRET_KEY": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
+ }
+}
def mock_render_to_response(*args, **kwargs):
return render_to_response(*args, **kwargs)
@@ -123,6 +133,161 @@ def test_reverify_post_success(self):
self.assertTrue(context['error'])
+@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
+@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
+class TestPhotoVerificationResultsCallback(TestCase):
+ """ Tests for Photo Verification Result Callback """
+ def setUp(self):
+ self.course_id = 'Robot/999/Test_Course'
+ CourseFactory.create(org='Robot', number='999', display_name='Test Course')
+ self.user = UserFactory.create()
+ self.attempt = SoftwareSecurePhotoVerification(
+ status="submitted",
+ user=self.user
+ )
+ self.attempt.save()
+ self.receipt_id = self.attempt.receipt_id
+ self.client = Client()
+
+ def mocked_has_valid_signature(method, headers_dict, body_dict, access_key, secret_key):
+ return True
+
+ def test_invalid_json(self):
+ """
+ test for invalid json being posted by software secure
+ """
+ data = {"testing invalid"}
+ response = self.client.post(reverse('verify_student_results_callback'), data=data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB: testing', HTTP_DATE='testdate')
+ self.assertIn('Invalid JSON', response.content)
+ self.assertEqual(response.status_code, 400)
+
+ def test_invalid_dict(self):
+ """
+ test for invalid dictionary being posted by software secure
+ """
+ data = '"\\"test\\testing"'
+ response = self.client.post(reverse('verify_student_results_callback'), data=data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ self.assertIn('JSON should be dict', response.content)
+ self.assertEqual(response.status_code, 400)
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_invalid_access_key(self):
+ """
+ test for invalid access key
+ """
+ data = {"EdX-ID": self.receipt_id,
+ "Result": "testing",
+ "Reason": "testing",
+ "MessageType": "testing"}
+ json_data = json.dumps(data)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test testing:testing', HTTP_DATE='testdate')
+ self.assertIn('Access key invalid', response.content)
+ self.assertEqual(response.status_code, 400)
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_wrong_edx_id(self):
+ """
+ test for wrong id of Software secure verification attempt
+ """
+ data = {"EdX-ID": "testing",
+ "Result": "testing",
+ "Reason": "testing",
+ "MessageType": "testing"}
+ json_data = json.dumps(data)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ self.assertIn('edX ID testing not found', response.content)
+ self.assertEqual(response.status_code, 400)
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_pass_result(self):
+ """
+ test for verification passed
+ """
+ data = {"EdX-ID": self.receipt_id,
+ "Result": "PASS",
+ "Reason": "Testing",
+ "MessageType": "you have been verified"}
+ json_data = json.dumps(data)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
+ self.assertEqual(attempt.status, u'approved')
+ self.assertEquals(response.content, 'OK!')
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_fail_result(self):
+ """
+ test for verification failed
+ """
+ data = {"EdX-ID": self.receipt_id,
+ "Result": 'FAIL',
+ "Reason": 'Invalid Photo',
+ "MessageType": 'Your photo doesn\'t meet standards'}
+ json_data = json.dumps(data)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
+ self.assertEqual(attempt.status, u'denied')
+ self.assertEqual(attempt.error_code, u'Your photo doesn\'t meet standards')
+ self.assertEqual(attempt.error_msg, u'"Invalid Photo"')
+ self.assertEquals(response.content, 'OK!')
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_system_fail_result(self):
+ """
+ test for software secure result system failure
+ """
+ data = {"EdX-ID": self.receipt_id,
+ "Result": 'SYSTEM FAIL',
+ "Reason": 'Memory overflow',
+ "MessageType": 'you must retry the verification'}
+ json_data = json.dumps(data)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
+ self.assertEqual(attempt.status, u'must_retry')
+ self.assertEqual(attempt.error_code, u'you must retry the verification')
+ self.assertEqual(attempt.error_msg, u'"Memory overflow"')
+ self.assertEquals(response.content, 'OK!')
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_unknown_result(self):
+ """
+ test for unknown software secure result
+ """
+ data = {"EdX-ID": self.receipt_id,
+ "Result": 'unknown',
+ "Reason": 'unknown reason',
+ "MessageType": 'unknown message'}
+ json_data = json.dumps(data)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ self.assertIn('Result unknown not understood', response.content)
+
+ @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
+ def test_reverification(self):
+ """
+ Test software secure result for reverification window
+ """
+ data = {"EdX-ID": self.receipt_id,
+ "Result": "PASS",
+ "Reason": "Testing",
+ "MessageType": "you have been verified"}
+ window = MidcourseReverificationWindowFactory(course_id=self.course_id)
+ self.attempt.window = window
+ self.attempt.save()
+ json_data = json.dumps(data)
+ self.assertEqual(CourseEnrollment.objects.filter(course_id=self.course_id).count(), 0)
+ response = self.client.post(reverse('verify_student_results_callback'), data=json_data,
+ content_type='application/json', HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', HTTP_DATE='testdate')
+ self.assertEquals(response.content, 'OK!')
+ self.assertIsNotNone(CourseEnrollment.objects.get(course_id=self.course_id))
+
+
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestMidCourseReverifyView(TestCase):
""" Tests for the midcourse reverification views """
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 2cafa4cd64ba..ed867dbf94ca 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -256,7 +256,7 @@ def results_callback(request):
# If this is a reverification, log an event
if attempt.window:
- course_id = window.course_id
+ course_id = attempt.window.course_id
course = course_from_id(course_id)
course_enrollment = CourseEnrollment.get_or_create_enrollment(attempt.user, course_id)
course_enrollment.emit_event(EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index a5be087ba1ae..88aa3d49f8ac 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -171,7 +171,7 @@
# Enable instructor dash beta version link
'ENABLE_INSTRUCTOR_BETA_DASHBOARD': True,
-
+ 'VERIFIED_CERTIFICATES' : True,
# Allow use of the hint managment instructor view.
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss
index 369fe1027a5d..86f8db425658 100644
--- a/lms/static/sass/base/_variables.scss
+++ b/lms/static/sass/base/_variables.scss
@@ -168,6 +168,15 @@ $verified-color-lvl3: $m-green-l2;
$verified-color-lvl4: $m-green-l3;
$verified-color-lvl5: $m-green-l4;
+// STATE: honor code
+$honorcode-color-lvl1: rgb(50, 165, 217);
+$honorcode-color-lvl2: tint($honorcode-color-lvl1, 33%);
+
+// STATE: audit
+$audit-color-lvl1: $light-gray;
+$audit-color-lvl2: tint($audit-color-lvl1, 33%);
+
+
// ====================
// ACTIONS: general
@@ -248,6 +257,7 @@ $dashboard-profile-header-image: linear-gradient(-90deg, rgb(255,255,255), rgb(2
$dashboard-profile-header-color: transparent;
$dashboard-profile-color: rgb(252,252,252);
$dot-color: $light-gray;
+$dashboard-course-cover-border: $light-gray;
// MISC: course assets
$content-wrapper-bg: $white;
@@ -321,4 +331,3 @@ $f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// SPLINT: colors
$msg-bg: $action-primary-bg;
-
diff --git a/lms/static/sass/elements/_typography.scss b/lms/static/sass/elements/_typography.scss
index 2d677700c6bd..8170ca4b88fb 100644
--- a/lms/static/sass/elements/_typography.scss
+++ b/lms/static/sass/elements/_typography.scss
@@ -277,7 +277,7 @@
}
%copy-badge {
- @extend %t-title8;
+ @extend %t-title9;
@extend %t-weight3;
border-radius: ($baseline/5);
padding: ($baseline/2) $baseline;
diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss
index 5172981661d9..f6d6becede26 100644
--- a/lms/static/sass/multicourse/_dashboard.scss
+++ b/lms/static/sass/multicourse/_dashboard.scss
@@ -357,15 +357,19 @@
.cover {
@include box-sizing(border-box);
+ @include transition(all 0.15s linear 0s);
+ overflow: hidden;
+ position: relative;
float: left;
height: 100%;
max-height: 100%;
- margin: 0px;
- overflow: hidden;
- position: relative;
- @include transition(all 0.15s linear 0s);
width: 200px;
height: 120px;
+ margin: 0px;
+ border-radius: ($baseline/10);
+ border: 1px solid $dashboard-course-cover-border;
+ border-bottom: 4px solid $dashboard-course-cover-border;
+ padding: ($baseline/10);
img {
width: 100%;
@@ -474,28 +478,43 @@
}
}
+ // "enrolled as" status
+ .sts-enrollment {
+ position: absolute;
+ top: 105px;
+ left: 0;
+ display: inline-block;
+ text-align: center;
+ width: 200px;
+
+ .label {
+ @extend %text-sr;
+ }
+
+ .sts-enrollment-value {
+ @extend %ui-depth1;
+ @extend %copy-badge;
+ border-radius: 0;
+ padding: ($baseline/4) ($baseline/2) ($baseline/4) ($baseline/2);
+ }
+ }
+
// ====================
- // STATE: course mode - verified
+ // CASE: "enrolled as" status - verified
&.verified {
- @extend %ui-depth2;
- position: relative;
+ // changes to cover
.cover {
- border-radius: ($baseline/10);
- border: 1px solid $verified-color-lvl3;
- border-bottom: 4px solid $verified-color-lvl3;
+ border-color: $verified-color-lvl3;
padding: ($baseline/10);
}
// course enrollment status message
.sts-enrollment {
- display: inline-block;
position: absolute;
- top: 105px;
left: 55px;
- bottom: ($baseline/2);
- text-align: center;
+ width: auto;
.label {
@extend %text-sr;
@@ -509,16 +528,46 @@
top: -10px;
}
+ // status message
.sts-enrollment-value {
- @extend %ui-depth1;
- @extend %copy-badge;
- border-radius: 0;
padding: ($baseline/4) ($baseline/2) ($baseline/4) $baseline;
background: $verified-color-lvl3;
- color: $white;
+ color: tint($verified-color-lvl1, 85%);
}
}
}
+
+ // CASE: "enrolled as" status - honor code
+ &.honor {
+
+ // changes to cover
+ .cover {
+ border-color: $honorcode-color-lvl2;
+ padding: ($baseline/10);
+ }
+
+ // status message
+ .sts-enrollment-value {
+ background: $honorcode-color-lvl1;
+ color: tint($honorcode-color-lvl1, 85%);
+ }
+ }
+
+ // CASE: "enrolled as" status - auditing
+ &.audit {
+
+ // changes to cover
+ .cover {
+ border-color: $audit-color-lvl2;
+ padding: ($baseline/10);
+ }
+
+ // status message
+ .sts-enrollment-value {
+ background: $audit-color-lvl1;
+ color: shade($audit-color-lvl1, 33%);
+ }
+ }
}
// ====================
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html
index 31a253a62b73..26401985d1f9 100644
--- a/lms/templates/dashboard/_dashboard_course_listing.html
+++ b/lms/templates/dashboard/_dashboard_course_listing.html
@@ -11,7 +11,7 @@
cert_name_short = course.cert_name_short
if cert_name_short == "":
cert_name_short = settings.CERT_NAME_SHORT
-
+
cert_name_long = course.cert_name_long
if cert_name_long == "":
cert_name_long = settings.CERT_NAME_LONG
@@ -20,7 +20,11 @@
<%namespace name='static' file='../static_content.html'/>
-
+ % if settings.FEATURES.get('VERIFIED_CERTIFICATES'):
+
+ % else:
+
+ %endif
<%
course_target = reverse('info', args=[course.id])
%>
@@ -34,13 +38,24 @@
% endif
-
- % if enrollment.mode == "verified":
-
- ${_("Enrolled as: ")}
-
- ${_("ID Verified")}
-
+ % if settings.FEATURES.get('VERIFIED_CERTIFICATES'):
+ % if enrollment.mode == "verified":
+
+ ${_("Enrolled as: ")}
+
+ ${_("Verified")}
+
+ % elif enrollment.mode == "honor":
+
+ ${_("Enrolled as: ")}
+ ${_("Honor Code")}
+
+ % elif enrollment.mode == "audit":
+
+ ${_("Enrolled as: ")}
+ ${_("Auditing")}
+
+ % endif
% endif