Skip to content

Commit

Permalink
Add configurations for proctored exam ping functionality
Browse files Browse the repository at this point in the history
At the proctoring provider level, this allows us to
1) specify different intervals for pinging via configuration changes
2) allow ping failures to continue in emergency conditions via waffle
  • Loading branch information
Matt Hughes authored and matthugs committed Dec 21, 2018
1 parent 42e36a3 commit cf75b50
Show file tree
Hide file tree
Showing 15 changed files with 110 additions and 40 deletions.
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
from __future__ import absolute_import

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '1.5.0rc5'
__version__ = '1.5.0rc6'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
3 changes: 3 additions & 0 deletions edx_proctoring/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import abc
import six

from edx_proctoring import constants


class ProctoringBackendProvider(six.with_metaclass(abc.ABCMeta)):
"""
The base abstract class for all proctoring service providers
"""
verbose_name = u'Unknown'
ping_interval = constants.DEFAULT_DESKTOP_APPLICATION_PING_INTERVAL_SECONDS

@abc.abstractmethod
def register_exam_attempt(self, exam, context):
Expand Down
3 changes: 1 addition & 2 deletions edx_proctoring/backends/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import warnings
import time
import uuid
import jwt

from webpack_loader.utils import get_files
from webpack_loader.exceptions import BaseWebpackLoaderException, WebpackBundleLookupError
Expand All @@ -15,8 +16,6 @@
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_rest_api_client.client import OAuthAPIClient

import jwt

log = logging.getLogger(__name__)


Expand Down
4 changes: 4 additions & 0 deletions edx_proctoring/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@
)

MINIMUM_TIME = datetime.datetime.fromtimestamp(0)

DEFAULT_DESKTOP_APPLICATION_PING_INTERVAL_SECONDS = 60

PING_FAILURE_PASSTHROUGH_TEMPLATE = 'edx_proctoring.{}_ping_failure_passthrough'
4 changes: 2 additions & 2 deletions edx_proctoring/static/proctoring/js/exam_action_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ var edx = edx || {};
));
}
}
edx.courseware.proctored_exam.pingApplication = function() {
edx.courseware.proctored_exam.pingApplication = function(timeoutInSeconds) {
return Promise.race([
workerPromiseForEventNames(actionToMessageTypesMap.ping)(),
timeoutPromise(ONE_MINUTE_MS)
timeoutPromise(timeoutInSeconds * 1000)
]);
}
edx.courseware.proctored_exam.accessibleError = accessibleError;
Expand Down
11 changes: 7 additions & 4 deletions edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,14 @@ var edx = edx || {};
"To pass your proctored exam you must also pass the online proctoring session review.");
},
updateRemainingTime: function (self) {
var pingInterval = self.model.get('ping_interval');
self.timerTick ++;
self.secondsLeft --;
if (
self.timerTick % self.poll_interval === self.poll_interval / 2 &&
self.timerTick % pingInterval === pingInterval / 2 &&
edx.courseware.proctored_exam.configuredWorkerURL
) {
edx.courseware.proctored_exam.pingApplication().catch(self.endExamForFailureState.bind(self));
edx.courseware.proctored_exam.pingApplication(pingInterval).catch(self.endExamForFailureState.bind(self));
}
if (self.timerTick % self.poll_interval === 0) {
var url = self.model.url + '/' + self.model.get('attempt_id');
Expand Down Expand Up @@ -193,8 +194,10 @@ var edx = edx || {};
},
url: this.model.url + '/' + this.model.get('attempt_id'),
type: 'PUT'
}).done(function() {
self.reloadPage();
}).done(function(result) {
if (result.exam_attempt_id) {
self.reloadPage();
}
});
},
toggleTimerVisibility: function (event) {
Expand Down
25 changes: 23 additions & 2 deletions edx_proctoring/static/proctoring/spec/proctored_exam_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ describe('ProctoredExamView', function () {
expect(reloadPage).toHaveBeenCalled();
});
it("calls external js global function on off-beat", function() {
this.proctored_exam_view.model.set('ping_interval', 60);
edx.courseware.proctored_exam.pingApplication = jasmine.createSpy().and.returnValue(Promise.resolve());
edx.courseware.proctored_exam.configuredWorkerURL = 'nonempty/string.html';
this.proctored_exam_view.timerTick = this.proctored_exam_view.poll_interval / 2 - 1;
this.proctored_exam_view.timerTick = this.proctored_exam_view.model.get('ping_interval') / 2 - 1;
this.proctored_exam_view.updateRemainingTime(this.proctored_exam_view);
expect(edx.courseware.proctored_exam.pingApplication).toHaveBeenCalled();
delete edx.courseware.proctored_exam.pingApplication;
Expand All @@ -109,7 +110,7 @@ describe('ProctoredExamView', function () {
function(request) {
request.respond(200,
{"Content-Type": "application/json"},
"{}"
'{"exam_attempt_id": "abcde"}'
);
}
);
Expand All @@ -120,6 +121,26 @@ describe('ProctoredExamView', function () {
});
this.server.respond();
});
it("does not reload the page after failure-state ajax call when server responds with no attempt id", function(done) {
// this case mimics current behavior of the server when the
// proctoring backend is configured to not block the user for a
// failed ping.
this.server.respondWith(
function(request) {
request.respond(200,
{"Content-Type": "application/json"},
'{"exam_attempt_id": false}'
);
}
);
var reloadPage = spyOn(this.proctored_exam_view, 'reloadPage');
this.proctored_exam_view.endExamForFailureState().done(function() {
expect(reloadPage).not.toHaveBeenCalled();
done();
});
this.server.respond();
});

it("sets global variable when unset", function() {
expect(window.edx.courseware.proctored_exam.configuredWorkerURL).toBeUndefined();
this.proctored_exam_view.model.set("desktop_application_js_url", "nonempty string");
Expand Down
25 changes: 25 additions & 0 deletions edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,31 @@ def test_submit_exam_attempt(self, action, expected_status):

self.assertEqual(response.status_code, 400)

@patch('edx_proctoring.views.waffle.switch_is_active')
def test_attempt_ping_failure(self, mocked_switch_is_active):
"""
Test ping failure when backend is configured to permit ping failures
"""
attempt = self._test_exam_attempt_creation()
attempt_id = attempt['id']
attempt_initial_status = attempt['status']

mocked_switch_is_active.return_value = True
response = self.client.put(
reverse('edx_proctoring:proctored_exam.attempt', args=[attempt_id]),
json.dumps({
'action': 'error',
}),
content_type='application/json'
)

self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data['exam_attempt_id'], False)

attempt = get_exam_attempt_by_id(attempt_id)
self.assertEqual(attempt['status'], attempt_initial_status)

def test_get_exam_attempts(self):
"""
Test to get the exam attempts in a course.
Expand Down
40 changes: 27 additions & 13 deletions edx_proctoring/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import logging
import six
import waffle

from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
Expand Down Expand Up @@ -43,6 +44,8 @@
get_backend_provider,
mark_exam_attempt_as_ready,
)
from edx_proctoring.constants import PING_FAILURE_PASSTHROUGH_TEMPLATE

from edx_proctoring.exceptions import (
ProctoredBaseException,
ProctoredExamReviewAlreadyExists,
Expand Down Expand Up @@ -396,11 +399,21 @@ def put(self, request, attempt_id):
ProctoredExamStudentAttemptStatus.download_software_clicked
)
elif action == 'error':
exam_attempt_id = update_attempt_status(
attempt['proctored_exam']['id'],
request.user.id,
ProctoredExamStudentAttemptStatus.error
)
backend = attempt['proctored_exam']['backend']
waffle_name = PING_FAILURE_PASSTHROUGH_TEMPLATE.format(backend)
should_block_user = not (backend and waffle.switch_is_active(waffle_name))
if should_block_user:
exam_attempt_id = update_attempt_status(
attempt['proctored_exam']['id'],
request.user.id,
ProctoredExamStudentAttemptStatus.error
)
else:
exam_attempt_id = False
LOG.warn(u'Browser JS reported problem with proctoring desktop '
u'application. Did block user: %s, for attempt: %s',
should_block_user,
attempt['id'])
elif action == 'decline':
exam_attempt_id = update_attempt_status(
attempt['proctored_exam']['id'],
Expand Down Expand Up @@ -504,11 +517,6 @@ def get(self, request): # pylint: disable=unused-argument
critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
)

if provider:
desktop_application_js_url = provider.get_javascript()
else:
desktop_application_js_url = ''

exam_url_path = ''
try:
# resolve the LMS url, note we can't assume we're running in
Expand All @@ -535,8 +543,14 @@ def get(self, request): # pylint: disable=unused-argument
remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
),
'attempt_status': attempt['status'],
'desktop_application_js_url': desktop_application_js_url
}

if provider:
response_dict['desktop_application_js_url'] = provider.get_javascript()
response_dict['ping_interval'] = provider.ping_interval
else:
response_dict['desktop_application_js_url'] = ''

else:
response_dict = {
'in_timed_exam': False,
Expand Down Expand Up @@ -797,7 +811,7 @@ def make_review(self, attempt, data, request=None, backend=None):
if review:
if not constants.ALLOW_REVIEW_UPDATES:
err_msg = (
'We already have a review submitted from SoftwareSecure regarding '
'We already have a review submitted regarding '
'attempt_code {attempt_code}. We do not allow for updates!'.format(
attempt_code=attempt_code
)
Expand All @@ -806,7 +820,7 @@ def make_review(self, attempt, data, request=None, backend=None):

# we allow updates
warn_msg = (
'We already have a review submitted from SoftwareSecure regarding '
'We already have a review submitted from our proctoring provider regarding '
'attempt_code {attempt_code}. We have been configured to allow for '
'updates and will continue...'.format(
attempt_code=attempt_code
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@edx/edx-proctoring",
"//": "Be sure to update the version number in edx_proctoring/__init__.py",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "1.5.0-rc.5",
"version": "1.5.0-rc.6",
"main": "edx_proctoring/static/index.js",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pytz>=2018
pycryptodomex>=3.4.7
python-dateutil>=2.1
django-webpack-loader>=0.6.0
django-waffle>=0.14.0

# edX packages
event-tracking>=0.2.5
Expand Down
4 changes: 2 additions & 2 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ certifi==2018.11.29 # via requests
chardet==3.0.4 # via requests
django-ipware==2.1.0
django-model-utils==3.1.2
django-waffle==0.15.0 # via edx-django-utils, edx-drf-extensions
django-waffle==0.15.0
django-webpack-loader==0.6.0
django==1.11.17
djangorestframework-jwt==1.11.0 # via edx-drf-extensions
djangorestframework==3.6.4
edx-django-utils==1.0.1 # via edx-drf-extensions
edx-django-utils==1.0.3 # via edx-drf-extensions
edx-drf-extensions==2.0.1
edx-opaque-keys==0.4.4
edx-rest-api-client==1.9.2
Expand Down
8 changes: 4 additions & 4 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ click-log==0.1.8 # via edx-lint
click==7.0 # via click-log, edx-lint, pip-tools
configparser==3.5.0 # via pydocstyle, pylint
contextlib2==0.5.5 # via importlib-metadata
diff-cover==1.0.5
diff-cover==1.0.6
distlib==0.2.8 # via caniusepython3
django==1.11.17
docutils==0.14 # via readme-renderer
Expand All @@ -36,14 +36,14 @@ mccabe==0.6.1 # via pylint
packaging==18.0 # via caniusepython3
path.py==11.5.0 # via edx-i18n-tools
pathlib2==2.3.3 # via importlib-metadata
pip-tools==3.1.0
pip-tools==3.2.0
pkginfo==1.4.2 # via twine
pluggy==0.8.0 # via tox
polib==1.1.0 # via edx-i18n-tools
py==1.7.0 # via tox
pycodestyle==2.4.0
pydocstyle==3.0.0
pygments==2.3.0 # via diff-cover, readme-renderer
pygments==2.3.1 # via diff-cover, readme-renderer
pylint-celery==0.3 # via edx-lint
pylint-django==0.7.2 # via edx-lint
pylint-plugin-utils==0.4 # via pylint-celery, pylint-django
Expand All @@ -60,7 +60,7 @@ six==1.12.0 # via astroid, bleach, diff-cover, edx-i18n-tools, edx
snowballstemmer==1.2.1 # via pydocstyle
toml==0.10.0 # via tox
tox-battery==0.5.1
tox==3.5.3
tox==3.6.0
tqdm==4.28.1 # via twine
twine==1.12.1
urllib3==1.24.1 # via requests
Expand Down
6 changes: 3 additions & 3 deletions requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ certifi==2018.11.29 # via requests
chardet==3.0.4 # via doc8, requests
django-ipware==2.1.0
django-model-utils==3.1.2
django-waffle==0.15.0 # via edx-django-utils, edx-drf-extensions
django-waffle==0.15.0
django-webpack-loader==0.6.0
django==1.11.17
djangorestframework-jwt==1.11.0 # via edx-drf-extensions
djangorestframework==3.6.4
doc8==0.8.0
docutils==0.14 # via doc8, readme-renderer, restructuredtext-lint, sphinx
edx-django-utils==1.0.1 # via edx-drf-extensions
edx-django-utils==1.0.3 # via edx-drf-extensions
edx-drf-extensions==2.0.1
edx-opaque-keys==0.4.4
edx-rest-api-client==1.9.2
Expand All @@ -36,7 +36,7 @@ pbr==5.1.1 # via stevedore
pockets==0.7.2 # via sphinxcontrib-napoleon
psutil==1.2.1 # via edx-django-utils, edx-drf-extensions
pycryptodomex==3.7.2
pygments==2.3.0 # via readme-renderer, sphinx
pygments==2.3.1 # via readme-renderer, sphinx
pyjwkest==1.3.2 # via edx-drf-extensions
pyjwt==1.7.1 # via djangorestframework-jwt, edx-rest-api-client
pymongo==3.7.2 # via edx-opaque-keys, event-tracking
Expand Down
12 changes: 6 additions & 6 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ coverage==4.5.2 # via pytest-cov
ddt==1.2.0
django-ipware==2.1.0
django-model-utils==3.1.2
django-waffle==0.15.0 # via edx-django-utils, edx-drf-extensions
django-waffle==0.15.0
django-webpack-loader==0.6.0
djangorestframework-jwt==1.11.0 # via edx-drf-extensions
djangorestframework==3.6.4
edx-django-utils==1.0.1 # via edx-drf-extensions
edx-django-utils==1.0.3 # via edx-drf-extensions
edx-drf-extensions==2.0.1
edx-opaque-keys==0.4.4
edx-rest-api-client==1.9.2
Expand Down Expand Up @@ -49,12 +49,12 @@ pymongo==3.7.2 # via edx-opaque-keys, event-tracking
pytest-cov==2.6.0
pytest-django==3.4.4
pytest-forked==0.2 # via pytest-xdist
pytest-xdist==1.24.1
pytest==4.0.1 # via pytest-cov, pytest-django, pytest-forked, pytest-xdist
pytest-xdist==1.25.0
pytest==4.0.2 # via pytest-cov, pytest-django, pytest-forked, pytest-xdist
python-dateutil==2.7.5
pytz==2018.7
requests==2.21.0 # via edx-drf-extensions, edx-rest-api-client, httmock, pyjwkest, responses, slumber
responses==0.10.4
responses==0.10.5
rest-condition==1.0.3 # via edx-drf-extensions
scandir==1.9.0 # via pathlib2
selenium==3.141.0
Expand All @@ -63,5 +63,5 @@ six==1.12.0 # via bok-choy, edx-drf-extensions, edx-opaque-keys, f
slumber==0.7.1 # via edx-rest-api-client
stevedore==1.30.0 # via edx-opaque-keys
sure==1.2.7
testfixtures==6.3.0
testfixtures==6.4.0
urllib3==1.24.1 # via requests, selenium

0 comments on commit cf75b50

Please sign in to comment.