Commit 53614b38 by chrisndodge

Merge pull request #178 from edx/cdodge/add-additional-allowances

Add out additional allowance that will be passed to Software Secure r…
parents 19747620 46c93e5f
...@@ -347,20 +347,16 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -347,20 +347,16 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
allowed_time_limit_mins = exam['time_limit_mins'] allowed_time_limit_mins = exam['time_limit_mins']
# add in the allowed additional time # add in the allowed additional time
allowance = ProctoredExamStudentAllowance.get_allowance_for_user( allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id)
exam_id, if allowance_extra_mins:
user_id,
"Additional time (minutes)"
)
if allowance:
allowance_extra_mins = int(allowance.value)
allowed_time_limit_mins += allowance_extra_mins allowed_time_limit_mins += allowance_extra_mins
attempt_code = unicode(uuid.uuid4()).upper() attempt_code = unicode(uuid.uuid4()).upper()
external_id = None external_id = None
review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id) review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id)
review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id)
if taking_as_proctored: if taking_as_proctored:
scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http'
callback_url = '{scheme}://{hostname}{path}'.format( callback_url = '{scheme}://{hostname}{path}'.format(
...@@ -395,6 +391,14 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -395,6 +391,14 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
'review_policy': review_policy.review_policy 'review_policy': review_policy.review_policy
}) })
# see if there is a review policy exception for this *user*
# exceptions are granted on a individual basis as an
# allowance
if review_policy_exception:
context.update({
'review_policy_exception': review_policy_exception
})
# now call into the backend provider to register exam attempt # now call into the backend provider to register exam attempt
external_id = get_backend_provider().register_exam_attempt( external_id = get_backend_provider().register_exam_attempt(
exam, exam,
......
...@@ -335,6 +335,18 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider): ...@@ -335,6 +335,18 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
callback_url = context['callback_url'] callback_url = context['callback_url']
full_name = context['full_name'] full_name = context['full_name']
review_policy = context.get('review_policy', constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY) review_policy = context.get('review_policy', constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY)
review_policy_exception = context.get('review_policy_exception')
# compile the notes to the reviewer
# this is a combination of the Exam Policy which is for all students
# combined with any exceptions granted to the particular student
reviewer_notes = review_policy
if review_policy_exception:
reviewer_notes = '{notes}; {exception}'.format(
notes=reviewer_notes,
exception=review_policy_exception
)
(first_name, last_name) = self._split_fullname(full_name) (first_name, last_name) = self._split_fullname(full_name)
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
...@@ -347,7 +359,7 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider): ...@@ -347,7 +359,7 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
"reviewedExam": not is_sample_attempt, "reviewedExam": not is_sample_attempt,
# NOTE: we will have to allow these notes to be authorable in Studio # NOTE: we will have to allow these notes to be authorable in Studio
# and then we will pull this from the exam database model # and then we will pull this from the exam database model
"reviewerNotes": review_policy, "reviewerNotes": reviewer_notes,
"examPassword": self._encrypt_password(self.crypto_key, attempt_code), "examPassword": self._encrypt_password(self.crypto_key, attempt_code),
"examSponsor": self.exam_sponsor, "examSponsor": self.exam_sponsor,
"examName": exam['exam_name'], "examName": exam['exam_name'],
......
...@@ -23,6 +23,8 @@ from edx_proctoring.api import ( ...@@ -23,6 +23,8 @@ from edx_proctoring.api import (
create_exam, create_exam,
create_exam_attempt, create_exam_attempt,
remove_exam_attempt, remove_exam_attempt,
add_allowance_for_user
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
StudentExamAttemptDoesNotExistsException, StudentExamAttemptDoesNotExistsException,
...@@ -37,6 +39,7 @@ from edx_proctoring.models import ( ...@@ -37,6 +39,7 @@ from edx_proctoring.models import (
ProctoredExamSoftwareSecureReviewHistory, ProctoredExamSoftwareSecureReviewHistory,
ProctoredExamReviewPolicy, ProctoredExamReviewPolicy,
ProctoredExamStudentAttemptHistory, ProctoredExamStudentAttemptHistory,
ProctoredExamStudentAllowance
) )
from edx_proctoring.backends.tests.test_review_payload import TEST_REVIEW_PAYLOAD from edx_proctoring.backends.tests.test_review_payload import TEST_REVIEW_PAYLOAD
...@@ -141,7 +144,8 @@ class SoftwareSecureTests(TestCase): ...@@ -141,7 +144,8 @@ class SoftwareSecureTests(TestCase):
self.assertEqual(attempt['external_id'], 'foobar') self.assertEqual(attempt['external_id'], 'foobar')
self.assertIsNone(attempt['started_at']) self.assertIsNone(attempt['started_at'])
def test_attempt_with_review_policy(self): @ddt.data(None, 'additional person allowed in room')
def test_attempt_with_review_policy(self, review_policy_exception):
""" """
Create an unstarted proctoring attempt with a review policy associated with it. Create an unstarted proctoring attempt with a review policy associated with it.
""" """
...@@ -154,6 +158,14 @@ class SoftwareSecureTests(TestCase): ...@@ -154,6 +158,14 @@ class SoftwareSecureTests(TestCase):
is_proctored=True is_proctored=True
) )
if review_policy_exception:
add_allowance_for_user(
exam_id,
self.user.id,
ProctoredExamStudentAllowance.REVIEW_POLICY_EXCEPTION,
review_policy_exception
)
policy = ProctoredExamReviewPolicy.objects.create( policy = ProctoredExamReviewPolicy.objects.create(
set_by_user_id=self.user.id, set_by_user_id=self.user.id,
proctored_exam_id=exam_id, proctored_exam_id=exam_id,
...@@ -169,10 +181,17 @@ class SoftwareSecureTests(TestCase): ...@@ -169,10 +181,17 @@ class SoftwareSecureTests(TestCase):
self.assertEqual(policy.review_policy, context['review_policy']) self.assertEqual(policy.review_policy, context['review_policy'])
# call into real implementation # call into real implementation
result = get_backend_provider(emphemeral=True)._get_payload(exam, context) # pylint: disable=protected-access result = get_backend_provider(emphemeral=True)._get_payload(exam, context)
# assert that this is in the 'reviewerNotes' field that is passed to SoftwareSecure # assert that this is in the 'reviewerNotes' field that is passed to SoftwareSecure
self.assertEqual(result['reviewerNotes'], context['review_policy']) expected = context['review_policy']
if review_policy_exception:
expected = '{base}; {exception}'.format(
base=expected,
exception=review_policy_exception
)
self.assertEqual(result['reviewerNotes'], expected)
return result return result
with HTTMock(mock_response_content): with HTTMock(mock_response_content):
...@@ -180,7 +199,7 @@ class SoftwareSecureTests(TestCase): ...@@ -180,7 +199,7 @@ class SoftwareSecureTests(TestCase):
# so that we can assert that we are called with the review policy # so that we can assert that we are called with the review policy
# as well as asserting that _get_payload includes that review policy # as well as asserting that _get_payload includes that review policy
# that was passed in # that was passed in
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock): # pylint: disable=protected-access with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock):
attempt_id = create_exam_attempt( attempt_id = create_exam_attempt(
exam_id, exam_id,
self.user.id, self.user.id,
......
...@@ -579,6 +579,15 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -579,6 +579,15 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
Information about allowing a student additional time on exam. Information about allowing a student additional time on exam.
""" """
# DONT EDIT THE KEYS - THE FIRST VALUE OF THE TUPLE - AS ARE THEY ARE STORED IN THE DATABASE
# THE SECOND ELEMENT OF THE TUPLE IS A DISPLAY STRING AND CAN BE EDITED
ADDITIONAL_TIME_GRANTED = ('additional_time_granted', _('Additional Time (minutes)'))
REVIEW_POLICY_EXCEPTION = ('review_policy_exception', _('Review Policy Exception'))
all_allowances = [
ADDITIONAL_TIME_GRANTED + REVIEW_POLICY_EXCEPTION
]
objects = ProctoredExamStudentAllowanceManager() objects = ProctoredExamStudentAllowanceManager()
user = models.ForeignKey(User) user = models.ForeignKey(User)
...@@ -625,22 +634,55 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -625,22 +634,55 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
""" """
Add or (Update) an allowance for a user within a given exam Add or (Update) an allowance for a user within a given exam
""" """
users = User.objects.filter(username=user_info) user_id = None
if not users.exists():
users = User.objects.filter(email=user_info) # see if key is a tuple, if it is, then the first element is the key
if isinstance(key, tuple) and len(key) > 0:
key = key[0]
# were we passed a PK?
if isinstance(user_info, (int, long)):
user_id = user_info
else:
# we got a string, so try to resolve it
users = User.objects.filter(username=user_info)
if not users.exists():
users = User.objects.filter(email=user_info)
if not users.exists(): if not users.exists():
err_msg = ( err_msg = (
'Cannot find user against {user_info}' 'Cannot find user against {user_info}'
).format(user_info=user_info) ).format(user_info=user_info)
raise UserNotFoundException(err_msg) raise UserNotFoundException(err_msg)
user_id = users[0].id
try: try:
student_allowance = cls.objects.get(proctored_exam_id=exam_id, user_id=users[0].id, key=key) student_allowance = cls.objects.get(proctored_exam_id=exam_id, user_id=user_id, key=key)
student_allowance.value = value student_allowance.value = value
student_allowance.save() student_allowance.save()
except cls.DoesNotExist: # pylint: disable=no-member except cls.DoesNotExist: # pylint: disable=no-member
cls.objects.create(proctored_exam_id=exam_id, user_id=users[0].id, key=key, value=value) cls.objects.create(proctored_exam_id=exam_id, user_id=user_id, key=key, value=value)
@classmethod
def get_additional_time_granted(cls, exam_id, user_id):
"""
Helper method to get the additional time granted
"""
allowance = cls.get_allowance_for_user(exam_id, user_id, cls.ADDITIONAL_TIME_GRANTED[0])
if allowance:
return int(allowance.value)
return None
@classmethod
def get_review_policy_exception(cls, exam_id, user_id):
"""
Helper method to get the policy exception that reviewers should
follow
"""
allowance = cls.get_allowance_for_user(exam_id, user_id, cls.REVIEW_POLICY_EXCEPTION[0])
return allowance.value if allowance else None
class ProctoredExamStudentAllowanceHistory(TimeStampedModel): class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
......
...@@ -14,6 +14,7 @@ var edx = edx || {}; ...@@ -14,6 +14,7 @@ var edx = edx || {};
this.proctored_exams = options.proctored_exams; this.proctored_exams = options.proctored_exams;
this.proctored_exam_allowance_view = options.proctored_exam_allowance_view; this.proctored_exam_allowance_view = options.proctored_exam_allowance_view;
this.course_id = options.course_id; this.course_id = options.course_id;
this.allowance_types = options.allowance_types;
this.model = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel(); this.model = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel();
_.bindAll(this, "render"); _.bindAll(this, "render");
this.loadTemplateData(); this.loadTemplateData();
...@@ -156,11 +157,9 @@ var edx = edx || {}; ...@@ -156,11 +157,9 @@ var edx = edx || {};
}, },
render: function () { render: function () {
var allowance_types = ['Additional time (minutes)'];
$(this.el).html(this.template({ $(this.el).html(this.template({
proctored_exams: this.proctored_exams, proctored_exams: this.proctored_exams,
allowance_types: allowance_types allowance_types: this.allowance_types
})); }));
this.$form = { this.$form = {
......
...@@ -8,6 +8,12 @@ var edx = edx || {}; ...@@ -8,6 +8,12 @@ var edx = edx || {};
edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView = Backbone.View.extend({ edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView = Backbone.View.extend({
initialize: function () { initialize: function () {
this.allowance_types = [
['additional_time_granted', gettext('Additional Time (minutes)')],
['review_policy_exception', gettext('Review Policy Exception')]
];
this.collection = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceCollection(); this.collection = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceCollection();
this.proctoredExamCollection = new edx.instructor_dashboard.proctoring.ProctoredExamCollection(); this.proctoredExamCollection = new edx.instructor_dashboard.proctoring.ProctoredExamCollection();
/* unfortunately we have to make some assumptions about what is being set up in HTML */ /* unfortunately we have to make some assumptions about what is being set up in HTML */
...@@ -121,6 +127,20 @@ var edx = edx || {}; ...@@ -121,6 +127,20 @@ var edx = edx || {};
}, },
render: function () { render: function () {
if (this.template !== null) { if (this.template !== null) {
var self = this;
this.collection.each(function(item){
var key = item.get('key');
var i
for (i=0; i<self.allowance_types.length; i++) {
if (key === self.allowance_types[i][0]) {
item.set('key_display_name', self.allowance_types[i][1]);
break;
}
}
if (!item.has('key_display_name')) {
item.set('key_display_name', key);
}
});
var html = this.template({proctored_exam_allowances: this.collection.toJSON()}); var html = this.template({proctored_exam_allowances: this.collection.toJSON()});
this.$el.html(html); this.$el.html(html);
} }
...@@ -132,7 +152,8 @@ var edx = edx || {}; ...@@ -132,7 +152,8 @@ var edx = edx || {};
var add_allowance_view = new edx.instructor_dashboard.proctoring.AddAllowanceView({ var add_allowance_view = new edx.instructor_dashboard.proctoring.AddAllowanceView({
course_id: self.course_id, course_id: self.course_id,
proctored_exams: self.proctoredExamCollection.toJSON(), proctored_exams: self.proctoredExamCollection.toJSON(),
proctored_exam_allowance_view: self proctored_exam_allowance_view: self,
allowance_types: self.allowance_types
}); });
} }
}); });
......
...@@ -9,7 +9,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -9,7 +9,7 @@ describe('ProctoredExamAddAllowanceView', function () {
created: "2015-08-10T09:15:45Z", created: "2015-08-10T09:15:45Z",
id: 1, id: 1,
modified: "2015-08-10T09:15:45Z", modified: "2015-08-10T09:15:45Z",
key: "Additional time (minutes)", key: "additional_time_granted",
value: "1", value: "1",
proctored_exam: { proctored_exam: {
content_id: "i4x://edX/DemoX/sequential/9f5e9b018a244ea38e5d157e0019e60c", content_id: "i4x://edX/DemoX/sequential/9f5e9b018a244ea38e5d157e0019e60c",
...@@ -59,8 +59,8 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -59,8 +59,8 @@ describe('ProctoredExamAddAllowanceView', function () {
'<label>Allowance Type</label>' + '<label>Allowance Type</label>' +
'</td><td><select id="allowance_type">' + '</td><td><select id="allowance_type">' +
'<% _.each(allowance_types, function(allowance_type){ %>' + '<% _.each(allowance_types, function(allowance_type){ %>' +
'<option value="<%= allowance_type %>">' + '<option value="<%= allowance_type[0] %>">' +
'<%- interpolate(gettext(" %(allowance_type)s "), { allowance_type: allowance_type }, true) %>' + '<%= allowance_type[1] %>' +
'</option>' + '</option>' +
'<% }); %>' + '<% }); %>' +
'</select></td></tr><tr><td>' + '</select></td></tr><tr><td>' +
...@@ -101,7 +101,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -101,7 +101,7 @@ describe('ProctoredExamAddAllowanceView', function () {
'<td>N/A</td><td>N/A</td>' + '<td>N/A</td><td>N/A</td>' +
'<% } %>' + '<% } %>' +
'<td>' + '<td>' +
'<%- interpolate(gettext(" %(allowance_name)s "), { allowance_name: proctored_exam_allowance.key }, true) %>' + '<%- interpolate(gettext(" %(allowance_name)s "), { allowance_name: proctored_exam_allowance.key_display_name }, true) %>' +
'</td>' + '</td>' +
'<td>' + '<td>' +
'<%= proctored_exam_allowance.value %>' + '<%= proctored_exam_allowance.value %>' +
...@@ -185,7 +185,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -185,7 +185,7 @@ describe('ProctoredExamAddAllowanceView', function () {
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1@test.com'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1@test.com');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Additional time (minutes)'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Additional Time (minutes)');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Test Exam'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Test Exam');
// add the proctored exam allowance // add the proctored exam allowance
...@@ -213,7 +213,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -213,7 +213,7 @@ describe('ProctoredExamAddAllowanceView', function () {
//select the form values //select the form values
$('#proctored_exam').val('Test Exam'); $('#proctored_exam').val('Test Exam');
$('#allowance_type').val('Additional time (minutes)'); $('#allowance_type').val('additional_time_granted');
$('#allowance_value').val('1'); $('#allowance_value').val('1');
$("#user_info").val('testuser1'); $("#user_info").val('testuser1');
...@@ -227,7 +227,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -227,7 +227,7 @@ describe('ProctoredExamAddAllowanceView', function () {
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('testuser1'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('testuser1');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('testuser1@test.com'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('testuser1@test.com');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('Additional time (minutes)'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('Additional Time (minutes)');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('Test Exam'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).toContain('Test Exam');
}); });
it("should send error when adding proctored exam allowance", function () { it("should send error when adding proctored exam allowance", function () {
...@@ -254,7 +254,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -254,7 +254,7 @@ describe('ProctoredExamAddAllowanceView', function () {
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1@test.com'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('testuser1@test.com');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Additional time (minutes)'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Additional Time (minutes)');
expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Test Exam'); expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html()).not.toContain('Test Exam');
// add the proctored exam allowance // add the proctored exam allowance
...@@ -282,7 +282,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -282,7 +282,7 @@ describe('ProctoredExamAddAllowanceView', function () {
//select the form values //select the form values
// invalid user_info returns error // invalid user_info returns error
$('#proctored_exam').val('Test Exam'); $('#proctored_exam').val('Test Exam');
$('#allowance_type').val('Additional time (minutes)'); $('#allowance_type').val('additional_time_granted');
$('#allowance_value').val('2'); $('#allowance_value').val('2');
$("#user_info").val('testuser112321'); $("#user_info").val('testuser112321');
...@@ -299,7 +299,7 @@ describe('ProctoredExamAddAllowanceView', function () { ...@@ -299,7 +299,7 @@ describe('ProctoredExamAddAllowanceView', function () {
//select the form values //select the form values
// empty value returns error // empty value returns error
$('#proctored_exam').val('Test Exam'); $('#proctored_exam').val('Test Exam');
$('#allowance_type').val('Additional time (minutes)'); $('#allowance_type').val('Additional Time (minutes)');
$('#allowance_value').val(''); $('#allowance_value').val('');
$("#user_info").val('testuser1'); $("#user_info").val('testuser1');
......
...@@ -59,7 +59,7 @@ describe('ProctoredExamAllowanceView', function () { ...@@ -59,7 +59,7 @@ describe('ProctoredExamAllowanceView', function () {
'<td>N/A</td><td>N/A</td>' + '<td>N/A</td><td>N/A</td>' +
'<% } %>' + '<% } %>' +
'<td>' + '<td>' +
'<%- interpolate(gettext(" %(allowance_name)s "), { allowance_name: proctored_exam_allowance.key }, true) %>' + '<%- interpolate(gettext(" %(allowance_name)s "), { allowance_name: proctored_exam_allowance.key_display_name }, true) %>' +
'</td>' + '</td>' +
'<td>' + '<td>' +
'<%= proctored_exam_allowance.value %>' + '<%= proctored_exam_allowance.value %>' +
......
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
<td> <td>
<select id="allowance_type"> <select id="allowance_type">
<% _.each(allowance_types, function(allowance_type){ %> <% _.each(allowance_types, function(allowance_type){ %>
<option value="<%= allowance_type %>"> <option value="<%= allowance_type[0] %>">
<%- interpolate(gettext(' %(allowance_type)s '), { allowance_type: allowance_type }, true) %> <%= allowance_type[1] %>
</option> </option>
<% }); %> <% }); %>
</select> </select>
......
...@@ -23,23 +23,24 @@ ...@@ -23,23 +23,24 @@
<% _.each(proctored_exam_allowances, function(proctored_exam_allowance){ %> <% _.each(proctored_exam_allowances, function(proctored_exam_allowance){ %>
<tr class="allowance-items"> <tr class="allowance-items">
<td> <td>
<%- interpolate(gettext(' %(exam_display_name)s '), { exam_display_name: proctored_exam_allowance.proctored_exam.exam_name }, true) %> <%- proctored_exam_allowance.proctored_exam.exam_name %>
</td> </td>
<% if (proctored_exam_allowance.user){ %> <% if (proctored_exam_allowance.user){ %>
<td> <td>
<%- interpolate(gettext(' %(username)s '), { username: proctored_exam_allowance.user.username }, true) %> <%= proctored_exam_allowance.user.username %>
</td> </td>
<td> <td>
<%- interpolate(gettext(' %(email)s '), { email: proctored_exam_allowance.user.email }, true) %> <%= proctored_exam_allowance.user.email %>
</td> </td>
<% }else{ %> <% }else{ %>
<td>N/A</td> <td>N/A</td>
<td>N/A</td> <td>N/A</td>
<% } %> <% } %>
<td> <td>
<%- interpolate(gettext(' %(allowance_name)s '), { allowance_name: proctored_exam_allowance.key }, true) %> <%= proctored_exam_allowance.key_display_name %>
</td> </td>
<td><%= proctored_exam_allowance.value %></td> <td>
<%- proctored_exam_allowance.value %></td>
<td> <td>
<a data-exam-id="<%= proctored_exam_allowance.proctored_exam.id %>" <a data-exam-id="<%= proctored_exam_allowance.proctored_exam.id %>"
data-key-name="<%= proctored_exam_allowance.key %>" data-key-name="<%= proctored_exam_allowance.key %>"
......
...@@ -400,7 +400,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -400,7 +400,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
add_allowance_for_user( add_allowance_for_user(
self.proctored_exam_id, self.proctored_exam_id,
self.user.username, self.user.username,
"Additional time (minutes)", ProctoredExamStudentAllowance.ADDITIONAL_TIME_GRANTED,
str(allowed_extra_time) str(allowed_extra_time)
) )
attempt_id = create_exam_attempt(self.proctored_exam_id, self.user_id) attempt_id = create_exam_attempt(self.proctored_exam_id, self.user_id)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment