Skip to content

Commit

Permalink
Merge pull request #559 from edx/matthugs/rpnow4-exam-content-in-appl…
Browse files Browse the repository at this point in the history
…ication

Reintegrate RPNow using browser functionality
  • Loading branch information
Matt Hughes authored May 1, 2019
2 parents b730a19 + b6482ce commit 447c0bf
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 37 deletions.
21 changes: 16 additions & 5 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions edx_proctoring/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions edx_proctoring/backends/software_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
26 changes: 26 additions & 0 deletions edx_proctoring/backends/tests/test_software_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 24 additions & 4 deletions edx_proctoring/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions edx_proctoring/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
17 changes: 16 additions & 1 deletion edx_proctoring/static/proctoring/js/exam_action_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions edx_proctoring/templates/proctored_exam/error_wrong_browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% load i18n %}
<div class="failure sequence proctored-exam" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
Error with proctored exam
{% endblocktrans %}
</h3>

<p>
{% 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 %}
</p>
<p>
{% blocktrans %}
Alternatively, you can end your exam.
{% endblocktrans %}
</p>
<button class="exam-action-button btn-pl-primary" data-action="stop" data-change-state-url="{{change_state_url}}">
{% trans "End My Exam" %}
</button>
</div>
{% include 'proctored_exam/footer.html' %}
{% include 'proctored_exam/error_modal.html' %}

<script type="text/javascript">
$('.exam-action-button').click(
edx.courseware.proctored_exam.updateStatusHandler
);
</script>
20 changes: 14 additions & 6 deletions edx_proctoring/templates/proctored_exam/instructions.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,20 @@ <h4>
Step 3
{% endblocktrans %}
</h4>
<p>
{% blocktrans %}
When you've finished the system check and verified your identity, begin your exam.
{% endblocktrans %}
</p>
<button href="#" class="exam-action-button js-start-proctored-exam btn btn-secondary">{% trans "Start Exam" %}</button>
{% if is_rpnow4_enabled %}
<p>
{% 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 %}
</p>
{% else %}
<p>
{% blocktrans %}
When you've finished the system check and verified your identity, begin your exam.
{% endblocktrans %}
</p>
<button href="#" class="exam-action-button js-start-proctored-exam btn btn-secondary">{% trans "Start Exam" %}</button>
{% endif %}
{% endif %}
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions edx_proctoring/templates/proctored_exam/ready_to_submit.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ <h3>
{% endblocktrans %}
</h3>
<ul>
<li> {% trans 'Make sure that you have selected "Submit" for each answer before you submit your exam.' %}</li>
{% if able_to_reenter_exam %}
<li> {% trans 'Make sure that you have selected "Submit" for each answer before you submit your exam.' %}</li>
{% endif %}
<li> {% trans 'Once you click "Yes, end my proctored exam", the exam will be closed, and your proctoring session will be submitted for review.' %}</li>
</ul>
{% block additional_exhortation %}{% endblock %}
{% trans "Yes, end my proctored exam" as end_exam %}
<button type="button" name="submit-proctored-exam" class="exam-action-button btn btn-pl-primary btn-base" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}" data-loading-text="<span class='fa fa-circle-o-notch fa-spin'></span> {% trans 'Ending Exam' %}" data-cta-text="{{ end_exam }}">
{{ end_exam }}
</button>
{% if does_time_remain %}
{% if able_to_reenter_exam %}
<button type="button" name="goback-proctored-exam" class="exam-action-button btn btn-secondary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}" style="box-shadow: none">
{% blocktrans %}
No, I'd like to continue working
Expand Down
6 changes: 1 addition & 5 deletions edx_proctoring/tests/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,7 @@ def test_correct_edx_email(self, status, integration_specific_email,):

test_backend = get_backend_provider(name='test')

if integration_specific_email is None:
# backend does not have integration specific email
del test_backend.integration_specific_email
else:
test_backend.integration_specific_email = integration_specific_email
test_backend.integration_specific_email = integration_specific_email

update_attempt_status(
exam_attempt.proctored_exam_id,
Expand Down
29 changes: 29 additions & 0 deletions edx_proctoring/tests/test_student_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from freezegun import freeze_time
from mock import MagicMock, patch
import pytz
from waffle.testutils import override_flag

import six

Expand All @@ -23,6 +24,7 @@
get_student_view,
update_attempt_status,
)
from edx_proctoring.constants import RPNOWV4_WAFFLE_NAME
from edx_proctoring.models import (
ProctoredExam,
ProctoredExamStudentAllowance,
Expand All @@ -36,6 +38,7 @@
from .utils import ProctoredExamTestCase


@override_flag(RPNOWV4_WAFFLE_NAME, active=True)
@patch('django.urls.reverse', MagicMock)
@ddt.ddt
class ProctoredExamStudentViewTests(ProctoredExamTestCase):
Expand Down Expand Up @@ -64,6 +67,7 @@ def setUp(self):
self.proctored_exam_submitted_msg = 'You have submitted this proctored exam for review'
self.take_exam_without_proctoring_msg = 'Take this exam without proctoring'
self.ready_to_start_msg = 'Important'
self.wrong_browser_msg = 'The content of this exam can only be viewed'
self.footer_msg = 'About Proctored Exams'
self.timed_footer_msg = 'Can I request additional time to complete my exam?'

Expand Down Expand Up @@ -471,6 +475,17 @@ def test_get_studentview_started_exam(self):
rendered_response = self.render_proctored_exam()
self.assertIsNone(rendered_response)

@patch('edx_proctoring.api.get_backend_provider')
def test_get_studentview_started_from_wrong_browser(self, mocked_get_backend):
"""
Test for get_student_view proctored exam as viewed from an
insecure browser.
"""
self._create_started_exam_attempt()
mocked_get_backend.return_value.should_block_access_to_exam_material.return_value = True
rendered_response = self.render_proctored_exam()
self.assertIn(self.wrong_browser_msg, rendered_response)

def test_get_studentview_started_practice_exam(self):
"""
Test for get_student_view practice proctored exam which has started.
Expand All @@ -479,6 +494,20 @@ def test_get_studentview_started_practice_exam(self):
rendered_response = self.render_practice_exam()
self.assertIsNone(rendered_response)

@patch('edx_proctoring.api.get_backend_provider')
def test_get_studentview_practice_from_wrong_browser(self, mocked_get_backend):
"""
Test for get_student_view practice proctored exam as viewed
from an insecure browser.
"""
self._create_started_practice_exam_attempt()
mocked_get_backend.return_value.should_block_access_to_exam_material.return_value = True
# Need to make sure our mock doesn't behave like a different
# type of backend before we reach to code under test
mocked_get_backend.return_value.supports_onboarding = False
rendered_response = self.render_practice_exam()
self.assertIn(self.wrong_browser_msg, rendered_response)

def test_get_studentview_started_timed_exam(self):
"""
Test for get_student_view timed exam which has started.
Expand Down
Loading

0 comments on commit 447c0bf

Please sign in to comment.