diff --git a/edx_proctoring/api.py b/edx_proctoring/api.py index eee3d2bef0c..9ea9c7d78b3 100644 --- a/edx_proctoring/api.py +++ b/edx_proctoring/api.py @@ -13,6 +13,8 @@ import pytz import six +from waffle import switch_is_active + from django.conf import settings from django.contrib.auth import get_user_model from django.core.mail.message import EmailMessage @@ -1724,7 +1726,8 @@ def _get_proctored_exam_context(exam, attempt, user_id, course_id, is_practice_e 'is_sample_attempt': is_practice_exam, 'has_due_date': has_due_date, 'has_due_date_passed': is_exam_passed_due(exam, user=user_id), - 'does_time_remain': _does_time_remain(attempt), + 'able_to_reenter_exam': _does_time_remain(attempt) and not provider.should_block_access_to_exam_material(), + 'is_rpnow4_enabled': switch_is_active(constants.RPNOWV4_WAFFLE_NAME), 'enter_exam_endpoint': reverse('edx_proctoring:proctored_exam.attempt.collection'), 'exam_started_poll_url': reverse( 'edx_proctoring:proctored_exam.attempt', @@ -1777,8 +1780,12 @@ def _get_practice_exam_view(exam, context, exam_id, user_id, course_id): if not attempt_status: student_view_template = 'practice_exam/entrance.html' elif attempt_status == ProctoredExamStudentAttemptStatus.started: - # when we're taking the exam we should not override the view - return None + provider = get_backend_provider(exam) + if provider.should_block_access_to_exam_material(): + student_view_template = 'proctored_exam/error_wrong_browser.html' + else: + # when we're taking the exam we should not override the view + return None elif attempt_status in [ProctoredExamStudentAttemptStatus.created, ProctoredExamStudentAttemptStatus.download_software_clicked]: student_view_template = 'proctored_exam/instructions.html' @@ -1919,8 +1926,12 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id): # to start timed exam emit_event(exam, 'option-presented') elif attempt_status == ProctoredExamStudentAttemptStatus.started: - # when we're taking the exam we should not override the view - return None + provider = get_backend_provider(exam) + if provider.should_block_access_to_exam_material(): + student_view_template = 'proctored_exam/error_wrong_browser.html' + else: + # when we're taking the exam we should not override the view + return None elif attempt_status in [ProctoredExamStudentAttemptStatus.created, ProctoredExamStudentAttemptStatus.download_software_clicked]: if context.get('verification_status') is not APPROVED_STATUS: diff --git a/edx_proctoring/backends/backend.py b/edx_proctoring/backends/backend.py index 434827275a3..d693e504e14 100644 --- a/edx_proctoring/backends/backend.py +++ b/edx_proctoring/backends/backend.py @@ -122,3 +122,9 @@ def retire_user(self, user_id): Returns boolean status of deletion, or None if this would be a no-op """ return None + + def should_block_access_to_exam_material(self): + """ + Whether learner access to exam content should be blocked during the exam + """ + return False diff --git a/edx_proctoring/backends/software_secure.py b/edx_proctoring/backends/software_secure.py index ab8b978c726..25c17581e12 100644 --- a/edx_proctoring/backends/software_secure.py +++ b/edx_proctoring/backends/software_secure.py @@ -16,6 +16,8 @@ import requests +from crum import get_current_request +from waffle import switch_is_active from django.conf import settings from django.urls import reverse @@ -383,3 +385,13 @@ def _send_request_to_ssi(self, data, sig, date): ) return response.status_code, response.text + + def should_block_access_to_exam_material(self): + """ + Whether learner access to exam content should be blocked during the exam + + Blocks learners from viewing exam course content from a + browser other than PSI's secure browser + """ + req = get_current_request() + return switch_is_active(constants.RPNOWV4_WAFFLE_NAME) and not req.get_signed_cookie('exam', default=False) diff --git a/edx_proctoring/backends/tests/test_software_secure.py b/edx_proctoring/backends/tests/test_software_secure.py index 46c94a19f0a..218657fa9cd 100644 --- a/edx_proctoring/backends/tests/test_software_secure.py +++ b/edx_proctoring/backends/tests/test_software_secure.py @@ -551,6 +551,32 @@ def test_mark_erroneous_proctored_exam(self): provider = get_backend_provider() self.assertIsNone(provider.mark_erroneous_exam_attempt(None, None)) + @ddt.data( + ['boop-be-boop-bop-bop', False, False], + ['boop-be-boop-bop-bop', True, False], + [False, False, False], + [False, True, True], + ) + @ddt.unpack + @patch('edx_proctoring.backends.software_secure.switch_is_active') + @patch('edx_proctoring.backends.software_secure.get_current_request') + def test_should_block_access_to_exam_material( + self, + cookie_present, + switch_active, + resultant_boolean, + mocked_get_current_request, + mocked_switch_is_active + ): + """ + Test that conditions applied for blocking user from accessing + course content are correct + """ + provider = get_backend_provider() + mocked_get_current_request.return_value.get_signed_cookie.return_value = cookie_present + mocked_switch_is_active.return_value = switch_active + assert bool(provider.should_block_access_to_exam_material()) == resultant_boolean + def test_split_fullname(self): """ Make sure we are splitting up full names correctly diff --git a/edx_proctoring/callbacks.py b/edx_proctoring/callbacks.py index 09a39941592..6472f6da33b 100644 --- a/edx_proctoring/callbacks.py +++ b/edx_proctoring/callbacks.py @@ -3,14 +3,17 @@ """ import logging -from django.template import loader +from waffle import switch_is_active from django.conf import settings -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect +from django.template import loader +from django.urls import reverse, NoReverseMatch from edx_proctoring.api import ( get_exam_attempt_by_code, mark_exam_attempt_as_ready, ) +from edx_proctoring.constants import RPNOWV4_WAFFLE_NAME from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus log = logging.getLogger(__name__) @@ -21,8 +24,6 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume A callback endpoint which is called when SoftwareSecure completes the proctoring setup and the exam should be started. - NOTE: This returns HTML as it will be displayed in an embedded browser - This is an authenticated endpoint and the attempt_code is passed in as part of the URL path @@ -41,8 +42,27 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume if attempt['status'] in [ProctoredExamStudentAttemptStatus.created, ProctoredExamStudentAttemptStatus.download_software_clicked]: mark_exam_attempt_as_ready(attempt['proctored_exam']['id'], attempt['user']['id']) + else: + log.warning("Attempted to enter proctored exam attempt {attempt_id} when status was {attempt_status}" + .format( + attempt_id=attempt['id'], + attempt_status=attempt['status'], + )) log.info("Exam %r has been marked as ready", attempt['proctored_exam']['id']) + if switch_is_active(RPNOWV4_WAFFLE_NAME): + course_id = attempt['proctored_exam']['course_id'] + content_id = attempt['proctored_exam']['content_id'] + + exam_url = '' + try: + exam_url = reverse('jump_to', args=[course_id, content_id]) + except NoReverseMatch: + log.exception("BLOCKING ERROR: Can't find course info url for course %s", course_id) + response = HttpResponseRedirect(exam_url) + response.set_signed_cookie('exam', attempt['attempt_code']) + return response + template = loader.get_template('proctored_exam/proctoring_launch_callback.html') return HttpResponse( diff --git a/edx_proctoring/constants.py b/edx_proctoring/constants.py index 4a288c613ac..bc31706aa93 100644 --- a/edx_proctoring/constants.py +++ b/edx_proctoring/constants.py @@ -63,3 +63,5 @@ DEFAULT_DESKTOP_APPLICATION_PING_INTERVAL_SECONDS = 60 PING_FAILURE_PASSTHROUGH_TEMPLATE = 'edx_proctoring.{}_ping_failure_passthrough' + +RPNOWV4_WAFFLE_NAME = 'edx_proctoring.rpnowv4_flow' diff --git a/edx_proctoring/static/proctoring/js/exam_action_handler.js b/edx_proctoring/static/proctoring/js/exam_action_handler.js index 25bd2a3aa2a..06f23c89e72 100644 --- a/edx_proctoring/static/proctoring/js/exam_action_handler.js +++ b/edx_proctoring/static/proctoring/js/exam_action_handler.js @@ -116,6 +116,21 @@ edx = edx || {}; edx.courseware = edx.courseware || {}; edx.courseware.proctored_exam = edx.courseware.proctored_exam || {}; + edx.courseware.proctored_exam.updateStatusHandler = function() { + var $this = $(this); + var actionUrl = $this.data('change-state-url'); + var action = $this.data('action'); + updateExamAttemptStatusPromise(actionUrl, action)() + .then(reloadPage) + .catch(errorHandlerGivenMessage( + $this, + gettext('Error Ending Exam'), + gettext( + 'Something has gone wrong ending your exam. ' + + 'Please reload the page and start again.' + ) + )); + }; edx.courseware.proctored_exam.examStartHandler = function(e) { var $this = $(this); var actionUrl = $this.data('change-state-url'); @@ -172,7 +187,7 @@ edx = edx || {}; gettext('Error Ending Exam'), gettext( 'Something has gone wrong ending your exam. ' + - 'Please double-check that the application is running.' + 'Please double-check that the application is running.' ) )); } else { diff --git a/edx_proctoring/templates/proctored_exam/error_wrong_browser.html b/edx_proctoring/templates/proctored_exam/error_wrong_browser.html new file mode 100644 index 00000000000..99cad4e2acd --- /dev/null +++ b/edx_proctoring/templates/proctored_exam/error_wrong_browser.html @@ -0,0 +1,32 @@ +{% load i18n %} +
+

+ {% blocktrans %} + Error with proctored exam + {% endblocktrans %} +

+ +

+ {% blocktrans %} + The content of this exam can only be viewed through the RPNow + application. If you have yet to complete your exam, please + return to the RPNow application to proceed. + {% endblocktrans %} +

+

+ {% blocktrans %} + Alternatively, you can end your exam. + {% endblocktrans %} +

+ +
+{% include 'proctored_exam/footer.html' %} +{% include 'proctored_exam/error_modal.html' %} + + diff --git a/edx_proctoring/templates/proctored_exam/instructions.html b/edx_proctoring/templates/proctored_exam/instructions.html index 548c8d0c7a0..bdefe0ec5fb 100644 --- a/edx_proctoring/templates/proctored_exam/instructions.html +++ b/edx_proctoring/templates/proctored_exam/instructions.html @@ -78,12 +78,20 @@

Step 3 {% endblocktrans %}

-

- {% blocktrans %} - When you've finished the system check and verified your identity, begin your exam. - {% endblocktrans %} -

- + {% if is_rpnow4_enabled %} +

+ {% blocktrans %} + For security and exam integrity reasons, we ask you to sign in to your edX account. Then we will direct you to the RPNow proctoring experience. + {% endblocktrans %} +

+ {% else %} +

+ {% blocktrans %} + When you've finished the system check and verified your identity, begin your exam. + {% endblocktrans %} +

+ + {% endif %} {% endif %} diff --git a/edx_proctoring/templates/proctored_exam/ready_to_submit.html b/edx_proctoring/templates/proctored_exam/ready_to_submit.html index 1e09a91150a..925daa01983 100644 --- a/edx_proctoring/templates/proctored_exam/ready_to_submit.html +++ b/edx_proctoring/templates/proctored_exam/ready_to_submit.html @@ -6,7 +6,9 @@

{% endblocktrans %}

{% block additional_exhortation %}{% endblock %} @@ -14,7 +16,7 @@

- {% if does_time_remain %} + {% if able_to_reenter_exam %}