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 @@ ${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h} % endif - - % if enrollment.mode == "verified": - - ${_("Enrolled as: ")} - ID Verified Ribbon/Badge - ${_("ID Verified")} - + % if settings.FEATURES.get('VERIFIED_CERTIFICATES'): + % if enrollment.mode == "verified": + + ${_("Enrolled as: ")} + ID Verified Ribbon/Badge + ${_("Verified")} + + % elif enrollment.mode == "honor": + + ${_("Enrolled as: ")} + ${_("Honor Code")} + + % elif enrollment.mode == "audit": + + ${_("Enrolled as: ")} + ${_("Auditing")} + + % endif % endif