Commit dda92d13 by Andy Armstrong Committed by GitHub

Merge pull request #13518 from edx/andya/proctoring-verification-step

Introduce id verification step for proctored exams
parents ce1eb237 ecf4515b
""" receivers of course_published and library_updated events in order to trigger indexing task """
import logging
from datetime import datetime
from pytz import UTC
......@@ -13,6 +14,9 @@ from openedx.core.lib.gating import api as gating_api
from util.module_utils import yield_dynamic_descriptor_descendants
log = logging.getLogger(__name__)
@receiver(SignalHandler.course_published)
def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
......@@ -23,7 +27,11 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
# first is to registered exams, the credit subsystem will assume that
# all proctored exams have already been registered, so we have to do that first
register_special_exams(course_key)
try:
register_special_exams(course_key)
# pylint: disable=broad-except
except Exception as exception:
log.exception(exception)
# then call into the credit subsystem (in /openedx/djangoapps/credit)
# to perform any 'on_publish' workflow
......
......@@ -137,10 +137,11 @@ class ProctoringFields(object):
@XBlock.wants('proctoring')
@XBlock.wants('verification')
@XBlock.wants('milestones')
@XBlock.wants('credit')
@XBlock.needs("user")
@XBlock.needs("bookmarks")
@XBlock.needs('user')
@XBlock.needs('bookmarks')
class SequenceModule(SequenceFields, ProctoringFields, XModule):
"""
Layout module which lays out content in a temporal sequence
......@@ -433,6 +434,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
proctoring_service = self.runtime.service(self, 'proctoring')
credit_service = self.runtime.service(self, 'credit')
verification_service = self.runtime.service(self, 'verification')
# Is this sequence designated as a Timed Examination, which includes
# Proctored Exams
......@@ -465,6 +467,14 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'credit_state': credit_state
})
# inject verification status
if verification_service:
verification_status, __ = verification_service.get_status(user_id)
context.update({
'verification_status': verification_status,
'reverify_url': verification_service.reverify_url(),
})
# See if the edx-proctoring subsystem wants to present
# a special view to the student rather
# than the actual sequence content
......
......@@ -161,3 +161,37 @@ class FakePaymentPage(PageObject):
self.q(css="input[value='Submit']").click()
return PaymentAndVerificationFlow(self.browser, self._course_id, entry_point='payment-confirmation').wait_for_page()
class FakeSoftwareSecureVerificationPage(PageObject):
"""
This page is a page used for testing that allows the user to change the status of their most recent
verification.
"""
url = BASE_URL + '/verify_student/software-secure-fake-response'
def __init__(self, browser):
super(FakeSoftwareSecureVerificationPage, self).__init__(browser)
def is_browser_on_page(self):
""" Determine if browser is on the page. """
message = self.q(css='BODY').text[0]
match = re.search('Fake Software Secure page', message)
return True if match else False
def mark_approved(self):
""" Mark the latest verification attempt as passing. """
self.q(css='#btn_pass').click()
def mark_denied(self):
""" Mark the latest verification attempt as denied. """
self.q(css='#btn_denied').click()
def mark_error(self):
""" Mark the latest verification attempt as an error. """
self.q(css='#btn_error').click()
def mark_unkown_error(self):
""" Mark the latest verification attempt as an unknown error. """
self.q(css='#btn_unkonwn_error').click()
......@@ -17,7 +17,7 @@ from ...pages.lms.course_nav import CourseNavPage
from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage
from ...pages.lms.create_mode import ModeCreationPage
from ...pages.lms.dashboard import DashboardPage
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage, FakeSoftwareSecureVerificationPage
from ...pages.lms.problem import ProblemPage
from ...pages.lms.progress import ProgressPage
from ...pages.lms.staff_view import StaffPage
......@@ -201,6 +201,28 @@ class ProctoredExamTest(UniqueCourseTest):
# Submit payment
self.fake_payment_page.submit_payment()
def _verify_user(self):
"""
Takes user through the verification flow and then marks the verification as 'approved'.
"""
# Immediately verify the user
self.immediate_verification_page.immediate_verification()
# Take face photo and proceed to the ID photo step
self.payment_and_verification_flow.webcam_capture()
self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page)
# Take ID photo and proceed to the review photos step
self.payment_and_verification_flow.webcam_capture()
self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page)
# Submit photos and proceed to the enrollment confirmation step
self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page)
# Mark the verification as passing.
verification = FakeSoftwareSecureVerificationPage(self.browser).visit()
verification.mark_approved()
def test_can_create_proctored_exam_in_studio(self):
"""
Given that I am a staff member
......@@ -221,6 +243,7 @@ class ProctoredExamTest(UniqueCourseTest):
select advanced settings tab
When I Make the exam proctored.
And I login as a verified student.
And I verify the user's ID.
And visit the courseware as a verified student.
Then I can see an option to take the exam as a proctored exam.
"""
......@@ -235,6 +258,8 @@ class ProctoredExamTest(UniqueCourseTest):
LogoutPage(self.browser).visit()
self._login_as_a_verified_user()
self._verify_user()
self.courseware_page.visit()
self.assertTrue(self.courseware_page.can_start_proctored_exam)
......
......@@ -51,7 +51,7 @@ from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from openedx.core.djangoapps.bookmarks.services import BookmarksService
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
from lms.djangoapps.verify_student.services import ReverificationService
from lms.djangoapps.verify_student.services import VerificationService, ReverificationService
from openedx.core.djangoapps.credit.services import CreditService
from openedx.core.djangoapps.util.user_utils import SystemUser
from openedx.core.lib.xblock_utils import (
......@@ -747,7 +747,8 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
'fs': FSService(),
'field-data': field_data,
'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
"reverification": ReverificationService(),
'verification': VerificationService(),
'reverification': ReverificationService(),
'proctoring': ProctoringService(),
'milestones': milestones_helpers.get_service(),
'credit': CreditService(),
......
......@@ -67,9 +67,11 @@ from edx_proctoring.api import (
)
from edx_proctoring.runtime import set_runtime_service
from edx_proctoring.tests.test_services import MockCreditService
from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from milestones.tests.utils import MilestonesTestCaseMixin
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
......@@ -740,6 +742,7 @@ class TestProctoringRendering(SharedModuleStoreTestCase):
self.request = factory.get(chapter_url)
self.request.user = UserFactory.create()
self.user = UserFactory.create()
SoftwareSecurePhotoVerificationFactory.create(user=self.request.user)
self.modulestore = self.store._get_modulestore_for_courselike(self.course_key) # pylint: disable=protected-access
with self.modulestore.bulk_operations(self.course_key):
self.toy_course = self.store.get_course(self.course_key, depth=2)
......@@ -1020,6 +1023,7 @@ class TestProctoringRendering(SharedModuleStoreTestCase):
if attempt_status:
create_exam_attempt(exam_id, self.request.user.id, taking_as_proctored=True)
update_attempt_status(exam_id, self.request.user.id, attempt_status)
return usage_key
def _find_url_name(self, toc, url_name):
......
......@@ -13,10 +13,42 @@ from opaque_keys.edx.keys import CourseKey
from student.models import User, CourseEnrollment
from lms.djangoapps.verify_student.models import VerificationCheckpoint, VerificationStatus, SkippedReverification
from .models import SoftwareSecurePhotoVerification
log = logging.getLogger(__name__)
class VerificationService(object):
"""
Learner verification XBlock service
"""
def get_status(self, user_id):
"""
Returns the user's current photo verification status.
Args:
user_id: the user's id
Returns: one of the following strings
'none' - no such verification exists
'expired' - verification has expired
'approved' - verification has been approved
'pending' - verification process is still ongoing
'must_reverify' - verification has been denied and user must resubmit photos
"""
user = User.objects.get(id=user_id)
# TODO: provide a photo verification abstraction so that this
# isn't hard-coded to use Software Secure.
return SoftwareSecurePhotoVerification.user_status(user)
def reverify_url(self):
"""
Returns the URL for a user to verify themselves.
"""
return reverse('verify_student_reverify')
class ReverificationService(object):
"""
Reverification XBlock service
......
......@@ -132,6 +132,9 @@ FEATURES['LICENSING'] = True
# Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# Open up endpoint for faking Software Secure responses
FEATURES['ENABLE_SOFTWARE_SECURE_FAKE'] = True
########################### Entrance Exams #################################
FEATURES['MILESTONES_APP'] = True
FEATURES['ENTRANCE_EXAMS'] = True
......@@ -176,6 +179,12 @@ MOCK_SEARCH_BACKING_FILE = (
TEST_ROOT / "index_file.dat"
).abspath()
# Verify student settings
VERIFY_STUDENT["SOFTWARE_SECURE"] = {
"API_ACCESS_KEY": "BBBBBBBBBBBBBBBBBBBB",
"API_SECRET_KEY": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
}
# this secret key should be the same as cms/envs/bok_choy.py's
SECRET_KEY = "very_secret_bok_choy_key"
......
......@@ -91,7 +91,7 @@ git+https://github.com/edx/xblock-utils.git@v1.0.3#egg=xblock-utils==1.0.3
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.9#egg=xblock-lti-consumer==1.0.9
git+https://github.com/edx/edx-proctoring.git@0.13.0#egg=edx-proctoring==0.13.0
git+https://github.com/edx/edx-proctoring.git@0.14.0#egg=edx-proctoring==0.14.0
# Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
......
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