Merge pull request #13518 from edx/andya/proctoring-verification-step
Introduce id verification step for proctored exams
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
""" 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
|
||||
|
||||
Reference in New Issue
Block a user