PHX-161
- added the new field review_rules for software secure - added a new tab name "Additional Settings" for the proctored/timed exams
This commit is contained in:
committed by
Douglas Hall
parent
9e5eda0ab6
commit
788cece45e
@@ -15,9 +15,13 @@ from edx_proctoring.api import (
|
||||
update_exam,
|
||||
create_exam,
|
||||
get_all_exams_for_course,
|
||||
update_review_policy,
|
||||
create_exam_review_policy,
|
||||
remove_review_policy,
|
||||
)
|
||||
from edx_proctoring.exceptions import (
|
||||
ProctoredExamNotFoundException
|
||||
ProctoredExamNotFoundException,
|
||||
ProctoredExamReviewPolicyNotFoundException
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -72,7 +76,7 @@ def register_special_exams(course_key):
|
||||
try:
|
||||
exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location))
|
||||
# update case, make sure everything is synced
|
||||
update_exam(
|
||||
exam_id = update_exam(
|
||||
exam_id=exam['id'],
|
||||
exam_name=timed_exam.display_name,
|
||||
time_limit_mins=timed_exam.default_time_limit_minutes,
|
||||
@@ -83,6 +87,7 @@ def register_special_exams(course_key):
|
||||
)
|
||||
msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
|
||||
log.info(msg)
|
||||
|
||||
except ProctoredExamNotFoundException:
|
||||
exam_id = create_exam(
|
||||
course_id=unicode(course_key),
|
||||
@@ -97,6 +102,30 @@ def register_special_exams(course_key):
|
||||
msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id)
|
||||
log.info(msg)
|
||||
|
||||
# only create/update exam policy for the proctored exams
|
||||
if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam:
|
||||
try:
|
||||
update_review_policy(
|
||||
exam_id=exam_id,
|
||||
set_by_user_id=timed_exam.edited_by,
|
||||
review_policy=timed_exam.exam_review_rules
|
||||
)
|
||||
except ProctoredExamReviewPolicyNotFoundException:
|
||||
if timed_exam.exam_review_rules: # won't save an empty rule.
|
||||
create_exam_review_policy(
|
||||
exam_id=exam_id,
|
||||
set_by_user_id=timed_exam.edited_by,
|
||||
review_policy=timed_exam.exam_review_rules
|
||||
)
|
||||
msg = 'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id)
|
||||
log.info(msg)
|
||||
else:
|
||||
try:
|
||||
# remove any associated review policy
|
||||
remove_review_policy(exam_id=exam_id)
|
||||
except ProctoredExamReviewPolicyNotFoundException:
|
||||
pass
|
||||
|
||||
# then see which exams we have in edx-proctoring that are not in
|
||||
# our current list. That means the the user has disabled it
|
||||
exams = get_all_exams_for_course(course_key)
|
||||
|
||||
@@ -11,7 +11,10 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from contentstore.signals import listen_for_course_publish
|
||||
|
||||
from edx_proctoring.api import get_all_exams_for_course
|
||||
from edx_proctoring.api import (
|
||||
get_all_exams_for_course,
|
||||
get_review_policy_by_exam_id
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -44,21 +47,28 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
self.assertEqual(len(exams), 1)
|
||||
|
||||
exam = exams[0]
|
||||
|
||||
if exam['is_proctored'] and not exam['is_practice_exam']:
|
||||
# get the review policy object
|
||||
exam_review_policy = get_review_policy_by_exam_id(exam['id'])
|
||||
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
|
||||
|
||||
self.assertEqual(exam['course_id'], unicode(self.course.id))
|
||||
self.assertEqual(exam['content_id'], unicode(sequence.location))
|
||||
self.assertEqual(exam['exam_name'], sequence.display_name)
|
||||
self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes)
|
||||
self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam)
|
||||
self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam)
|
||||
self.assertEqual(exam['is_active'], expected_active)
|
||||
|
||||
@ddt.data(
|
||||
(True, 10, True, True, False),
|
||||
(True, 10, False, True, False),
|
||||
(True, 10, True, True, True),
|
||||
(True, 10, True, False, True, False),
|
||||
(True, 10, False, False, True, False),
|
||||
(True, 10, True, True, True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes,
|
||||
is_proctored_exam, expected_active, republish):
|
||||
is_proctored_exam, is_practice_exam, expected_active, republish):
|
||||
"""
|
||||
Happy path testing to see that when a course is published which contains
|
||||
a proctored exam, it will also put an entry into the exam tables
|
||||
@@ -73,7 +83,9 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
is_time_limited=is_time_limited,
|
||||
default_time_limit_minutes=default_time_limit_minutes,
|
||||
is_proctored_exam=is_proctored_exam,
|
||||
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1)
|
||||
is_practice_exam=is_practice_exam,
|
||||
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
|
||||
exam_review_rules="allow_use_of_paper"
|
||||
)
|
||||
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
@@ -205,7 +217,8 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
graded=True,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=10,
|
||||
is_proctored_exam=True
|
||||
is_proctored_exam=True,
|
||||
exam_review_rules="allow_use_of_paper"
|
||||
)
|
||||
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
|
||||
@@ -869,6 +869,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
"is_proctored_exam": xblock.is_proctored_exam,
|
||||
"is_practice_exam": xblock.is_practice_exam,
|
||||
"is_time_limited": xblock.is_time_limited,
|
||||
"exam_review_rules": xblock.exam_review_rules,
|
||||
"default_time_limit_minutes": xblock.default_time_limit_minutes
|
||||
})
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class CourseMetadata(object):
|
||||
'is_proctored_enabled',
|
||||
'is_time_limited',
|
||||
'is_practice_exam',
|
||||
'exam_review_rules',
|
||||
'self_paced'
|
||||
]
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
'course-outline', 'xblock-string-field-editor', 'modal-button',
|
||||
'basic-modal', 'course-outline-modal', 'release-date-editor',
|
||||
'due-date-editor', 'grading-editor', 'publish-editor',
|
||||
'staff-lock-editor', 'timed-examination-preference-editor'
|
||||
'staff-lock-editor', 'settings-tab-section', 'timed-examination-preference-editor'
|
||||
]);
|
||||
appendSetFixtures(mockOutlinePage);
|
||||
mockCourseJSON = createMockCourseJSON({}, [
|
||||
@@ -580,7 +580,8 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
|
||||
describe("Subsection", function() {
|
||||
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson,
|
||||
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam;
|
||||
selectDisableSpecialExams, selectGeneralSettings, selectAdvancedSettings,
|
||||
selectTimedExam, selectProctoredExam, selectPracticeExam;
|
||||
|
||||
getDisplayNameWrapper = function() {
|
||||
return getItemHeaders('subsection').find('.wrapper-xblock-field');
|
||||
@@ -597,6 +598,14 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
this.$("#id_not_timed").prop('checked', true).trigger('change');
|
||||
};
|
||||
|
||||
selectGeneralSettings = function() {
|
||||
this.$(".modal-section .general-settings-button").click();
|
||||
};
|
||||
|
||||
selectAdvancedSettings = function() {
|
||||
this.$(".modal-section .advanced-settings-button").click();
|
||||
};
|
||||
|
||||
selectTimedExam = function(time_limit) {
|
||||
this.$("#id_timed_exam").prop('checked', true).trigger('change');
|
||||
this.$("#id_time_limit").val(time_limit);
|
||||
@@ -701,6 +710,22 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
collapseItemsAndVerifyState('subsection');
|
||||
expandItemsAndVerifyState('subsection');
|
||||
});
|
||||
|
||||
it('can show general settings', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectGeneralSettings();
|
||||
expect($('.modal-section .general-settings-button')).toHaveClass('active');
|
||||
expect($('.modal-section .advanced-settings-button')).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
it('can show advanced settings', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectAdvancedSettings();
|
||||
expect($('.modal-section .general-settings-button')).not.toHaveClass('active');
|
||||
expect($('.modal-section .advanced-settings-button')).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('can be edited', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
@@ -715,6 +740,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
"visible_to_staff_only": true,
|
||||
"start":"2014-07-09T00:00:00.000Z",
|
||||
"due":"2014-07-10T00:00:00.000Z",
|
||||
"exam_review_rules": "",
|
||||
"is_time_limited": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored_enabled": true,
|
||||
|
||||
@@ -84,7 +84,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
getContext: function () {
|
||||
return $.extend({
|
||||
xblockInfo: this.model,
|
||||
introductionMessage: this.getIntroductionMessage()
|
||||
introductionMessage: this.getIntroductionMessage(),
|
||||
enable_proctored_exams: this.options.enable_proctored_exams,
|
||||
enable_timed_exams: this.options.enable_timed_exams
|
||||
});
|
||||
},
|
||||
|
||||
@@ -114,6 +116,78 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
gettext('Change the settings for %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
},
|
||||
|
||||
initializeEditors: function () {
|
||||
var special_exams_editors = this.options.special_exam_editors;
|
||||
if (typeof special_exams_editors !== 'undefined' && special_exams_editors.length > 0) {
|
||||
var tabs_html = this.loadTemplate('settings-tab-section');
|
||||
this.$('.modal-section').html(tabs_html);
|
||||
this.options.editors = _.map(this.options.editors, function (Editor) {
|
||||
return new Editor({
|
||||
parentElement: this.$('.modal-section .general-settings'),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType,
|
||||
enable_proctored_exams: this.options.enable_proctored_exams,
|
||||
enable_timed_exams: this.options.enable_timed_exams
|
||||
});
|
||||
}, this);
|
||||
|
||||
this.options.special_exam_editors = _.map(special_exams_editors, function (Editor) {
|
||||
return new Editor({
|
||||
parentElement: this.$('.modal-section .advanced-settings'),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType,
|
||||
enable_proctored_exams: this.options.enable_proctored_exams,
|
||||
enable_timed_exams: this.options.enable_timed_exams
|
||||
});
|
||||
}, this);
|
||||
this.hideAdvancedSettings();
|
||||
} else {
|
||||
CourseOutlineXBlockModal.prototype.initializeEditors.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .action-save': 'save',
|
||||
'click .general-settings-button': 'showGeneralSettings',
|
||||
'click .advanced-settings-button': 'showAdvancedSettings'
|
||||
},
|
||||
|
||||
/**
|
||||
* Return request data.
|
||||
* @return {Object}
|
||||
*/
|
||||
getRequestData: function () {
|
||||
var combined_editors = this.options.editors.concat(this.options.special_exam_editors);
|
||||
var requestData = _.map(combined_editors, function (editor) {
|
||||
return editor.getRequestData();
|
||||
});
|
||||
return $.extend.apply(this, [true, {}].concat(requestData));
|
||||
},
|
||||
|
||||
hideAdvancedSettings: function() {
|
||||
this.$('.modal-section .general-settings-button').addClass('active');
|
||||
this.$('.modal-section .advanced-settings-button').removeClass('active');
|
||||
this.$('.modal-section .general-settings').show();
|
||||
this.$('.modal-section .advanced-settings').hide();
|
||||
|
||||
},
|
||||
|
||||
hideGeneralSettings: function() {
|
||||
this.$('.modal-section .general-settings-button').removeClass('active');
|
||||
this.$('.modal-section .advanced-settings-button').addClass('active');
|
||||
this.$('.modal-section .general-settings').hide();
|
||||
this.$('.modal-section .advanced-settings').show();
|
||||
},
|
||||
showGeneralSettings: function (event) {
|
||||
event.preventDefault();
|
||||
this.hideAdvancedSettings();
|
||||
},
|
||||
|
||||
showAdvancedSettings: function (event) {
|
||||
event.preventDefault();
|
||||
this.hideGeneralSettings();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -267,20 +341,40 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
className: 'edit-settings-timed-examination',
|
||||
events : {
|
||||
'change #id_not_timed': 'notTimedExam',
|
||||
'change #id_timed_exam': 'showTimeLimit',
|
||||
'change #id_practice_exam': 'showTimeLimit',
|
||||
'change #id_proctored_exam': 'showTimeLimit',
|
||||
'change #id_timed_exam': 'setTimedExam',
|
||||
'change #id_practice_exam': 'setPracticeExam',
|
||||
'change #id_proctored_exam': 'setProctoredExam',
|
||||
'focusout #id_time_limit': 'timeLimitFocusout'
|
||||
},
|
||||
notTimedExam: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#id_time_limit_div').hide();
|
||||
this.$('.exam-review-rules-list-fields').hide();
|
||||
this.$('#id_time_limit').val('00:00');
|
||||
},
|
||||
showTimeLimit: function (event) {
|
||||
event.preventDefault();
|
||||
selectSpecialExam: function (showRulesField) {
|
||||
this.$('#id_time_limit_div').show();
|
||||
this.$('#id_time_limit').val("00:30");
|
||||
if (!this.isValidTimeLimit(this.$('#id_time_limit').val())) {
|
||||
this.$('#id_time_limit').val('00:30');
|
||||
}
|
||||
if (showRulesField) {
|
||||
this.$('.exam-review-rules-list-fields').show();
|
||||
}
|
||||
else {
|
||||
this.$('.exam-review-rules-list-fields').hide();
|
||||
}
|
||||
},
|
||||
setTimedExam: function (event) {
|
||||
event.preventDefault();
|
||||
this.selectSpecialExam(false);
|
||||
},
|
||||
setPracticeExam: function (event) {
|
||||
event.preventDefault();
|
||||
this.selectSpecialExam(false);
|
||||
},
|
||||
setProctoredExam: function (event) {
|
||||
event.preventDefault();
|
||||
this.selectSpecialExam(true);
|
||||
},
|
||||
timeLimitFocusout: function(event) {
|
||||
event.preventDefault();
|
||||
@@ -301,6 +395,8 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
this.setExamType(this.model.get('is_time_limited'), this.model.get('is_proctored_exam'),
|
||||
this.model.get('is_practice_exam'));
|
||||
this.setExamTime(this.model.get('default_time_limit_minutes'));
|
||||
|
||||
this.setReviewRules(this.model.get('exam_review_rules'));
|
||||
},
|
||||
setExamType: function(is_time_limited, is_proctored_exam, is_practice_exam) {
|
||||
if (!is_time_limited) {
|
||||
@@ -309,12 +405,14 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
}
|
||||
|
||||
this.$('#id_time_limit_div').show();
|
||||
this.$('.exam-review-rules-list-fields').hide();
|
||||
|
||||
if (this.options.enable_proctored_exams && is_proctored_exam) {
|
||||
if (is_practice_exam) {
|
||||
this.$('#id_practice_exam').prop('checked', true);
|
||||
} else {
|
||||
this.$('#id_proctored_exam').prop('checked', true);
|
||||
this.$('.exam-review-rules-list-fields').show();
|
||||
}
|
||||
} else {
|
||||
// Since we have an early exit at the top of the method
|
||||
@@ -327,6 +425,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
var time = this.convertTimeLimitMinutesToString(value);
|
||||
this.$('#id_time_limit').val(time);
|
||||
},
|
||||
setReviewRules: function (value) {
|
||||
this.$('#id_exam_review_rules').val(value);
|
||||
},
|
||||
isValidTimeLimit: function(time_limit) {
|
||||
var pattern = new RegExp('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$');
|
||||
return pattern.test(time_limit) && time_limit !== "00:00";
|
||||
@@ -351,6 +452,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
var is_practice_exam;
|
||||
var is_proctored_exam;
|
||||
var time_limit = this.getExamTimeLimit();
|
||||
var exam_review_rules = this.$('#id_exam_review_rules').val();
|
||||
|
||||
if (this.$("#id_not_timed").is(':checked')){
|
||||
is_time_limited = false;
|
||||
@@ -374,6 +476,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
metadata: {
|
||||
'is_practice_exam': is_practice_exam,
|
||||
'is_time_limited': is_time_limited,
|
||||
'exam_review_rules': exam_review_rules,
|
||||
// We have to use the legacy field name
|
||||
// as the Ajax handler directly populates
|
||||
// the xBlocks fields. We will have to
|
||||
@@ -584,6 +687,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
|
||||
getEditModal: function (xblockInfo, options) {
|
||||
var editors = [];
|
||||
var special_exam_editors = [];
|
||||
|
||||
if (xblockInfo.isChapter()) {
|
||||
editors = [ReleaseDateEditor, StaffLockEditor];
|
||||
@@ -592,7 +696,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
|
||||
var enable_special_exams = (options.enable_proctored_exams || options.enable_timed_exams);
|
||||
if (enable_special_exams) {
|
||||
editors.push(TimedExaminationPreferenceEditor);
|
||||
special_exam_editors.push(TimedExaminationPreferenceEditor);
|
||||
}
|
||||
|
||||
editors.push(StaffLockEditor);
|
||||
@@ -610,6 +714,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
}
|
||||
return new SettingsXBlockModal($.extend({
|
||||
editors: editors,
|
||||
special_exam_editors: special_exam_editors,
|
||||
model: xblockInfo
|
||||
}, options));
|
||||
},
|
||||
|
||||
@@ -99,6 +99,37 @@
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.settings-tab {
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
|
||||
li.settings-section {
|
||||
display: inline-block;
|
||||
margin-right: $baseline;
|
||||
|
||||
.general-settings-button,
|
||||
.advanced-settings-button {
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-regular;
|
||||
background-image: none;
|
||||
background-color: $white;
|
||||
color: $mediumGrey;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
text-transform: uppercase;
|
||||
&:hover {
|
||||
background-color: $white;
|
||||
color: $blue;
|
||||
}
|
||||
&.active {
|
||||
border-bottom: 4px solid $blue-d2;
|
||||
color: $offBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-section-title {
|
||||
@@ -528,7 +559,8 @@
|
||||
.wrapper-modal-window-bulkpublish-subsection,
|
||||
.wrapper-modal-window-bulkpublish-unit,
|
||||
.course-outline-modal {
|
||||
.exam-time-list-fields {
|
||||
.exam-time-list-fields,
|
||||
.exam-review-rules-list-fields {
|
||||
margin: 0 0 ($baseline/2) ($baseline/2);
|
||||
}
|
||||
.list-fields {
|
||||
|
||||
@@ -22,7 +22,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
|
||||
<%block name="header_extras">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor']:
|
||||
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'settings-tab-section']:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<%
|
||||
var enable_proctored_exams = enable_proctored_exams;
|
||||
var enable_timed_exams = enable_timed_exams;
|
||||
%>
|
||||
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
|
||||
<div class="message modal-introduction">
|
||||
<p><%= introductionMessage %></p>
|
||||
</div>
|
||||
<% if (!( enable_proctored_exams || enable_timed_exams )) { %>
|
||||
<div class="message modal-introduction">
|
||||
<p><%- introductionMessage %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="modal-section"></div>
|
||||
</div>
|
||||
|
||||
|
||||
10
cms/templates/js/settings-tab-section.underscore
Normal file
10
cms/templates/js/settings-tab-section.underscore
Normal file
@@ -0,0 +1,10 @@
|
||||
<ul class="settings-tab">
|
||||
<li class="settings-section">
|
||||
<button class="general-settings-button" href="#"><%- gettext('General Settings') %></button>
|
||||
</li>
|
||||
<li class="settings-section">
|
||||
<button class="advanced-settings-button" href="#"><%- gettext('Advanced') %></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class='general-settings'></div>
|
||||
<div class='advanced-settings'></div>
|
||||
@@ -1,8 +1,5 @@
|
||||
<form>
|
||||
<h3 class="modal-section-title"><%- gettext('Additional Options:') %></h3>
|
||||
|
||||
<div class="modal-section-content has-actions">
|
||||
|
||||
<div class='exam-time-list-fields'>
|
||||
<ul class="list-fields list-input">
|
||||
<li class="field-radio">
|
||||
@@ -22,7 +19,7 @@
|
||||
<%- gettext('Timed') %>
|
||||
</label>
|
||||
</li>
|
||||
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time on per learner basis through the Instructor Dashboard.') %> </p>
|
||||
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@@ -35,7 +32,7 @@
|
||||
<%- gettext('Proctored') %>
|
||||
</label>
|
||||
</li>
|
||||
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed, and software records video of each learner taking the exam. These videos are then reviewed by a third party.') %> </p>
|
||||
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='exam-time-list-fields'>
|
||||
@@ -46,7 +43,7 @@
|
||||
<%- gettext('Practice Proctored') %>
|
||||
</label>
|
||||
</li>
|
||||
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of the practice exam do not count towards the learner's grade.") %> </p>
|
||||
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner's grade.") %> </p>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -61,5 +58,15 @@
|
||||
<p class='field-message' id='time-limit-description'><%- gettext('Learners see warnings when 20% and 5% of the allotted time remains. You can grant learners extra time to complete the exam through the Instructor Dashboard.') %></p>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='exam-review-rules-list-fields is-hidden'>
|
||||
<ul class="list-fields list-input exam-review-rules">
|
||||
<li class="field field-text field-exam-review-rules">
|
||||
<label for="id_exam_review_rules" class="label"><%- gettext('Review Rules') %> </label>
|
||||
<textarea id="id_exam_review_rules" cols="50" maxlength="255" name="review_rules" aria-describedby="review-rules-description"
|
||||
class="review-rules input input-text" autocomplete="off" />
|
||||
</li>
|
||||
<p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -11,7 +11,7 @@ import warnings
|
||||
|
||||
from lxml import etree
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Integer, Scope, Boolean
|
||||
from xblock.fields import Integer, Scope, Boolean, String
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
from .exceptions import NotFoundError
|
||||
@@ -88,6 +88,15 @@ class ProctoringFields(object):
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
exam_review_rules = String(
|
||||
display_name=_("Software Secure Review Rules"),
|
||||
help=_(
|
||||
"This setting indicates what rules the proctoring team should follow when viewing the videos."
|
||||
),
|
||||
default='',
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
is_practice_exam = Boolean(
|
||||
display_name=_("Is Practice Exam"),
|
||||
help=_(
|
||||
|
||||
@@ -530,6 +530,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
self.q(css=".action-save").first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def select_advanced_settings_tab(self):
|
||||
"""
|
||||
Select the advanced settings tab
|
||||
"""
|
||||
self.q(css=".advanced-settings-button").first.click()
|
||||
self.wait_for_element_presence('#id_not_timed', 'Advanced settings fields not present.')
|
||||
|
||||
def make_exam_proctored(self):
|
||||
"""
|
||||
Makes a Proctored exam.
|
||||
@@ -576,6 +583,12 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
"""
|
||||
return self.q(css="#id_time_limit_div").visible
|
||||
|
||||
def exam_review_rules_field_visible(self):
|
||||
"""
|
||||
Returns whether the review rules field is visible
|
||||
"""
|
||||
return self.q(css=".exam-review-rules-list-fields").visible
|
||||
|
||||
def proctoring_items_are_displayed(self):
|
||||
"""
|
||||
Returns True if all the items are found.
|
||||
|
||||
@@ -186,151 +186,188 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
|
||||
def test_can_create_proctored_exam_in_studio(self):
|
||||
"""
|
||||
Test that Proctored exam settings are visible in Studio.
|
||||
Given that I am a staff member
|
||||
When I visit the course outline page in studio.
|
||||
And open the subsection edit dialog
|
||||
Then I can view all settings related to Proctored and timed exams
|
||||
"""
|
||||
|
||||
# Given that I am a staff member
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
|
||||
# When I visit the course outline page in studio.
|
||||
self.course_outline.visit()
|
||||
|
||||
# And open the subsection edit dialog
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
|
||||
# Then I can view all settings related to Proctored and timed exams
|
||||
self.assertTrue(self.course_outline.proctoring_items_are_displayed())
|
||||
|
||||
def test_proctored_exam_flow(self):
|
||||
"""
|
||||
Test that staff can create a proctored exam.
|
||||
Given that I am a staff member on the exam settings section
|
||||
select advanced settings tab
|
||||
When I Make the exam proctored.
|
||||
And I login as a verified student.
|
||||
And visit the courseware as a verified student.
|
||||
Then I can see an option to take the exam as a proctored exam.
|
||||
"""
|
||||
|
||||
# Given that I am a staff member on the exam settings section
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
|
||||
# When I Make the exam proctored.
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
self.course_outline.make_exam_proctored()
|
||||
|
||||
# And I login as a verified student.
|
||||
LogoutPage(self.browser).visit()
|
||||
self._login_as_a_verified_user()
|
||||
|
||||
# And visit the courseware as a verified student.
|
||||
self.courseware_page.visit()
|
||||
|
||||
# Then I can see an option to take the exam as a proctored exam.
|
||||
self.assertTrue(self.courseware_page.can_start_proctored_exam)
|
||||
|
||||
def test_timed_exam_flow(self):
|
||||
"""
|
||||
Test that staff can create a timed exam.
|
||||
Given that I am a staff member on the exam settings section
|
||||
select advanced settings tab
|
||||
When I Make the exam timed.
|
||||
And I login as a verified student.
|
||||
And visit the courseware as a verified student.
|
||||
And I start the timed exam
|
||||
Then I am taken to the exam with a timer bar showing
|
||||
"""
|
||||
|
||||
# Given that I am a staff member on the exam settings section
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
|
||||
# When I Make the exam timed.
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
self.course_outline.make_exam_timed()
|
||||
|
||||
# And I login as a verified student.
|
||||
LogoutPage(self.browser).visit()
|
||||
self._login_as_a_verified_user()
|
||||
|
||||
# And visit the courseware as a verified student.
|
||||
self.courseware_page.visit()
|
||||
|
||||
# And I start the timed exam
|
||||
self.courseware_page.start_timed_exam()
|
||||
|
||||
# Then I am taken to the exam with a timer bar showing
|
||||
self.assertTrue(self.courseware_page.is_timer_bar_present)
|
||||
|
||||
def test_time_allotted_field_is_not_visible_with_none_exam(self):
|
||||
"""
|
||||
Test that the time allotted text field is not shown if 'none' radio
|
||||
button is selected
|
||||
Given that I am a staff member
|
||||
And I have visited the course outline page in studio.
|
||||
And the subsection edit dialog is open
|
||||
select advanced settings tab
|
||||
When I select the 'None' exams radio button
|
||||
Then the time allotted text field becomes invisible
|
||||
"""
|
||||
|
||||
# Given that I am a staff member
|
||||
# And I have visited the course outline page in studio.
|
||||
# And the subsection edit dialog is open
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
# When I select the 'None' exams radio button
|
||||
self.course_outline.select_none_exam()
|
||||
|
||||
# Then the time allotted text field becomes invisible
|
||||
self.assertFalse(self.course_outline.time_allotted_field_visible())
|
||||
|
||||
def test_time_allotted_field_is_visible_with_timed_exam(self):
|
||||
"""
|
||||
Test that the time allotted text field is shown if timed exam radio
|
||||
button is selected
|
||||
Given that I am a staff member
|
||||
And I have visited the course outline page in studio.
|
||||
And the subsection edit dialog is open
|
||||
select advanced settings tab
|
||||
When I select the timed exams radio button
|
||||
Then the time allotted text field becomes visible
|
||||
"""
|
||||
|
||||
# Given that I am a staff member
|
||||
# And I have visited the course outline page in studio.
|
||||
# And the subsection edit dialog is open
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
# When I select the timed exams radio button
|
||||
self.course_outline.select_timed_exam()
|
||||
|
||||
# Then the time allotted text field becomes visible
|
||||
self.assertTrue(self.course_outline.time_allotted_field_visible())
|
||||
|
||||
def test_time_allotted_field_is_visible_with_proctored_exam(self):
|
||||
"""
|
||||
Test that the time allotted text field is shown if proctored exam radio
|
||||
button is selected
|
||||
Given that I am a staff member
|
||||
And I have visited the course outline page in studio.
|
||||
And the subsection edit dialog is open
|
||||
select advanced settings tab
|
||||
When I select the proctored exams radio button
|
||||
Then the time allotted text field becomes visible
|
||||
"""
|
||||
|
||||
# Given that I am a staff member
|
||||
# And I have visited the course outline page in studio.
|
||||
# And the subsection edit dialog is open
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
# When I select the proctored exams radio button
|
||||
self.course_outline.select_proctored_exam()
|
||||
|
||||
# Then the time allotted text field becomes visible
|
||||
self.assertTrue(self.course_outline.time_allotted_field_visible())
|
||||
|
||||
def test_exam_review_rules_field_is_visible_with_proctored_exam(self):
|
||||
"""
|
||||
Given that I am a staff member
|
||||
And I have visited the course outline page in studio.
|
||||
And the subsection edit dialog is open
|
||||
select advanced settings tab
|
||||
When I select the proctored exams radio button
|
||||
Then the review rules textarea field becomes visible
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
self.course_outline.select_proctored_exam()
|
||||
self.assertTrue(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
def test_exam_review_rules_field_is_not_visible_with_other_than_proctored_exam(self):
|
||||
"""
|
||||
Given that I am a staff member
|
||||
And I have visited the course outline page in studio.
|
||||
And the subsection edit dialog is open
|
||||
select advanced settings tab
|
||||
When I select the timed exams radio button
|
||||
Then the review rules textarea field is not visible
|
||||
When I select the none exam radio button
|
||||
Then the review rules textarea field is not visible
|
||||
When I select the practice exam radio button
|
||||
Then the review rules textarea field is not visible
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
self.course_outline.select_timed_exam()
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_none_exam()
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_practice_exam()
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
def test_time_allotted_field_is_visible_with_practice_exam(self):
|
||||
"""
|
||||
Test that the time allotted text field is shown if practice exam radio
|
||||
button is selected
|
||||
Given that I am a staff member
|
||||
And I have visited the course outline page in studio.
|
||||
And the subsection edit dialog is open
|
||||
select advanced settings tab
|
||||
When I select the practice exams radio button
|
||||
Then the time allotted text field becomes visible
|
||||
"""
|
||||
|
||||
# Given that I am a staff member
|
||||
# And I have visited the course outline page in studio.
|
||||
# And the subsection edit dialog is open
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
# When I select the practice exams radio button
|
||||
self.course_outline.select_practice_exam()
|
||||
|
||||
# Then the time allotted text field becomes visible
|
||||
self.assertTrue(self.course_outline.time_allotted_field_visible())
|
||||
|
||||
|
||||
|
||||
@@ -211,6 +211,10 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
|
||||
|
||||
# open the exam settings to make it a proctored exam.
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
|
||||
# select advanced settings tab
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
self.course_outline.make_exam_proctored()
|
||||
|
||||
# login as a verified student and visit the courseware.
|
||||
@@ -233,6 +237,10 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
|
||||
|
||||
# open the exam settings to make it a proctored exam.
|
||||
self.course_outline.open_exam_settings_dialog()
|
||||
|
||||
# select advanced settings tab
|
||||
self.course_outline.select_advanced_settings_tab()
|
||||
|
||||
self.course_outline.make_exam_timed()
|
||||
|
||||
# login as a verified student and visit the courseware.
|
||||
|
||||
@@ -94,7 +94,7 @@ git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0
|
||||
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
|
||||
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
|
||||
git+https://github.com/edx/edx-organizations.git@release-2015-12-08#egg=edx-organizations==0.2.0
|
||||
git+https://github.com/edx/edx-proctoring.git@0.11.6#egg=edx-proctoring==0.11.6
|
||||
git+https://github.com/edx/edx-proctoring.git@0.12.1#egg=edx-proctoring==0.12.1
|
||||
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.0#egg=xblock-lti-consumer==v1.0.0
|
||||
|
||||
# Third Party XBlocks
|
||||
|
||||
Reference in New Issue
Block a user