Commit 46c93e5f by Chris Dodge

Add out additional allowance that will be passed to Software Secure reviewers,…

Add out additional allowance that will be passed to Software Secure reviewers, also refactor existing allowance to not use a display string as a key name
parent 19747620
...@@ -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