Skip to content

Commit

Permalink
Merge branch 'master' into matthugs/javascript-ci-setup
Browse files Browse the repository at this point in the history
  • Loading branch information
davestgermain authored Nov 30, 2018
2 parents 622dbd5 + ddd2a0d commit 60f41fa
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 13 deletions.
2 changes: 1 addition & 1 deletion docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Proctoring System configuration endpoint
}

The keys in the rules object should be machine readable. The values are human readable. PS should respect the HTTP request ``Accept-Language``
header and translate all human readable rules into the requested language.
header and translate all human readable rules and instructions into the requested language.

If a download_url is included in the response, Open edX will redirect learners to the address before the proctoring session starts. The address will include ``attempt={attempt_id}`` in the query string.

Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

from __future__ import absolute_import

__version__ = '1.5.0b1'
__version__ = '1.5.0b2'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
11 changes: 7 additions & 4 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,10 +1069,13 @@ def update_attempt_status(exam_id, user_id, to_status,

# call back to the backend to register the end of the exam, if necessary
backend = get_backend_provider(exam)
if to_status == ProctoredExamStudentAttemptStatus.started:
backend.start_exam_attempt(exam['external_id'], attempt['external_id'])
if to_status == ProctoredExamStudentAttemptStatus.submitted:
backend.stop_exam_attempt(exam['external_id'], attempt['external_id'])
if backend:
# only proctored exams have a backend
# timed exams have no backend
if to_status == ProctoredExamStudentAttemptStatus.started:
backend.start_exam_attempt(exam['external_id'], attempt['external_id'])
if to_status == ProctoredExamStudentAttemptStatus.submitted:
backend.stop_exam_attempt(exam['external_id'], attempt['external_id'])
# we user the 'status' field as the name of the event 'verb'
emit_event(exam, attempt['status'], attempt=attempt)

Expand Down
8 changes: 6 additions & 2 deletions edx_proctoring/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def get_backend_provider(exam=None):
Returns an instance of the configured backend provider
"""
backend_name = None
if exam and exam['backend']:
backend_name = exam['backend']
if exam:
if 'is_proctored' in exam and not exam['is_proctored']:
# timed exams don't have a backend
return None
elif exam['backend']:
backend_name = exam['backend']
return apps.get_app_config('edx_proctoring').get_backend(name=backend_name)
34 changes: 29 additions & 5 deletions edx_proctoring/backends/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import time
import uuid
import pkg_resources

from edx_proctoring.backends.backend import ProctoringBackendProvider
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_rest_api_client.client import OAuthAPIClient
Expand Down Expand Up @@ -89,7 +90,7 @@ def get_proctoring_config(self):
"""
url = self.config_url
log.debug('Requesting config from %r', url)
response = self.session.get(url).json()
response = self.session.get(url, headers=self._get_language_headers()).json()
return response

def get_exam(self, exam):
Expand Down Expand Up @@ -166,12 +167,15 @@ def on_exam_saved(self, exam):
url = self.exam_url.format(exam_id=external_id)
else:
url = self.create_exam_url
log.debug('Saving exam to %r', url)
log.info('Saving exam to %r', url)
response = None
try:
response = self.session.post(url, json=exam)
data = response.json()
except Exception: # pylint: disable=broad-except
log.exception('saving exam. %r', response.content)
except Exception as exc: # pylint: disable=broad-except
# pylint: disable=no-member
content = exc.response.content if hasattr(exc, 'response') else response.content
log.exception('failed to save exam. %r', content)
data = {}
return data.get('id')

Expand Down Expand Up @@ -200,6 +204,23 @@ def get_instructor_url(self, course_id, user, exam_id=None, attempt_id=None):
log.debug('Created instructor url for %r %r %r', course_id, exam_id, attempt_id)
return url

def _get_language_headers(self):
"""
Returns a dictionary of the Accept-Language headers
"""
# This import is here because developers writing backends which subclass this class
# may want to import this module and use the other methods, without having to run in the context
# of django settings, etc.
from django.conf import settings
from django.utils.translation import get_language

current_lang = get_language()
default_lang = settings.LANGUAGE_CODE
lang_header = default_lang
if current_lang and current_lang != default_lang:
lang_header = '{};{}'.format(current_lang, default_lang)
return {'Accept-Language': lang_header}

def _make_attempt_request(self, exam, attempt, method='POST', status=None, **payload):
"""
Calls backend attempt API
Expand All @@ -209,6 +230,9 @@ def _make_attempt_request(self, exam, attempt, method='POST', status=None, **pay
else:
payload = None
url = self.exam_attempt_url.format(exam_id=exam, attempt_id=attempt)
headers = {}
if method == 'GET':
headers.update(self._get_language_headers())
log.debug('Making %r attempt request at %r', method, url)
response = self.session.request(method, url, json=payload).json()
response = self.session.request(method, url, json=payload, headers=headers).json()
return response
11 changes: 11 additions & 0 deletions edx_proctoring/backends/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ def test_backend_choices(self):
]
self.assertEqual(choices, expected)

def test_no_backend_for_timed_exams(self):
"""
Timed exams should not return a backend, even if one has accidentally been set
"""
exam = {
'is_proctored': False,
'backend': 'test'
}
backend = get_backend_provider(exam)
self.assertIsNone(backend)

def test_invalid_configurations(self):
"""
Test that invalid backends throw the right exceptions
Expand Down
22 changes: 22 additions & 0 deletions edx_proctoring/backends/tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import responses

from django.test import TestCase
from django.utils.translation import activate

from edx_proctoring.backends.rest import BaseRestProctoringProvider

Expand Down Expand Up @@ -88,6 +89,27 @@ def test_get_attempt(self):
)
external_attempt = self.provider.get_attempt(attempt)
self.assertEqual(external_attempt, attempt)
self.assertEqual(responses.calls[1].request.headers['Accept-Language'], 'en-us')

@responses.activate
def test_get_attempt_i18n(self):
activate('es')
attempt = {
'id': 1,
'external_id': 'abcd',
'proctored_exam': self.backend_exam,
'user': 1,
'instructions': []
}
responses.add(
responses.GET,
url=self.provider.exam_attempt_url.format(
exam_id=self.backend_exam['external_id'], attempt_id=attempt['external_id']),
json=attempt
)
external_attempt = self.provider.get_attempt(attempt)
self.assertEqual(external_attempt, attempt)
self.assertEqual(responses.calls[1].request.headers['Accept-Language'], 'es;en-us')

@responses.activate
def test_on_exam_saved(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
var edx = edx || {};

(function (Backbone, $, _) {
'use strict';

edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {};
edx.instructor_dashboard.proctoring.ProctoredExamDashboardView = Backbone.View.extend({
initialize: function (options) {
this.setElement($('.student-review-dashboard-container'));
this.tempate_url = '/static/proctoring/templates/dashboard.underscore';
this.iframeHTML = null;
this.doRender = true;
this.context = {
dashboardURL: '/api/edx_proctoring/v1/instructor/' + this.$el.data('course-id')
};
var self = this;

$('#proctoring-accordion').on('accordionactivate', function(event, ui) {
self.render(ui);
});
/* Load the static template for rendering. */
this.loadTemplateData();
},
loadTemplateData: function () {
var self = this;
$.ajax({url: self.tempate_url, dataType: "html"})
.error(function (jqXHR, textStatus, errorThrown) {

})
.done(function (template_html) {
self.iframeHTML = _.template(template_html)(self.context);
});
},
render: function (ui) {
if (ui.newPanel.eq(this.$el) && this.doRender && this.iframeHTML) {
this.$el.html(this.iframeHTML);
this.doRender = false;
}
},
});
this.edx.instructor_dashboard.proctoring.ProctoredExamDashboardView = edx.instructor_dashboard.proctoring.ProctoredExamDashboardView;
}).call(this, Backbone, $, _);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="wrapper-content wrapper">
<iframe src="<%= dashboardURL %>" frameborder=0 height=1280 style="width:100%">
</div>
4 changes: 4 additions & 0 deletions edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,7 @@ def test_launch_for_course(self):
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
)

expected_url = '/instructor/%s/' % course_id
Expand All @@ -2521,6 +2522,7 @@ def test_launch_for_exam(self):
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
)
exam_id = proctored_exam.id

Expand All @@ -2540,6 +2542,7 @@ def test_error_with_multiple_backends(self):
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
backend='test',
)
ProctoredExam.objects.create(
Expand All @@ -2549,6 +2552,7 @@ def test_error_with_multiple_backends(self):
external_id='123aXqe4',
time_limit_mins=90,
is_active=True,
is_proctored=True,
backend='null',
)
response = self.client.get(
Expand Down

0 comments on commit 60f41fa

Please sign in to comment.