Merge pull request #8962 from edx/cdodge/proctoring-studio
Integrate timed and proctored exam authoring into Studio
This commit is contained in:
@@ -17,6 +17,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
id: 'mock-course',
|
||||
display_name: 'Mock Course',
|
||||
category: 'course',
|
||||
enable_proctored_exams: true,
|
||||
studio_url: '/course/slashes:MockCourse',
|
||||
is_container: true,
|
||||
has_changes: false,
|
||||
@@ -214,7 +215,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
'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'
|
||||
'staff-lock-editor', 'timed-examination-preference-editor'
|
||||
]);
|
||||
appendSetFixtures(mockOutlinePage);
|
||||
mockCourseJSON = createMockCourseJSON({}, [
|
||||
@@ -582,7 +583,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
});
|
||||
|
||||
describe("Subsection", function() {
|
||||
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson;
|
||||
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson, setModalTimedExaminationPreferenceValues;
|
||||
|
||||
getDisplayNameWrapper = function() {
|
||||
return getItemHeaders('subsection').find('.wrapper-xblock-field');
|
||||
@@ -595,6 +596,16 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
$("#staff_lock").prop('checked', is_locked);
|
||||
};
|
||||
|
||||
setModalTimedExaminationPreferenceValues = function(
|
||||
is_timed_examination,
|
||||
time_limit,
|
||||
is_exam_proctoring_enabled
|
||||
){
|
||||
$("#id_time_limit").val(time_limit);
|
||||
$("#id_exam_proctoring").prop('checked', is_exam_proctoring_enabled);
|
||||
$("#id_timed_examination").prop('checked', is_timed_examination);
|
||||
};
|
||||
|
||||
// Contains hard-coded dates because dates are presented in different formats.
|
||||
mockServerValuesJson = createMockSectionJSON({
|
||||
release_date: 'Jan 01, 2970 at 05:00 UTC'
|
||||
@@ -607,7 +618,10 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
format: "Lab",
|
||||
due: "2014-07-10T00:00:00Z",
|
||||
has_explicit_staff_lock: true,
|
||||
staff_only_message: true
|
||||
staff_only_message: true,
|
||||
"is_time_limited": true,
|
||||
"is_proctored_enabled": true,
|
||||
"default_time_limit_minutes": 150
|
||||
}, [
|
||||
createMockVerticalJSON({
|
||||
has_changes: true,
|
||||
@@ -682,6 +696,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
|
||||
setModalTimedExaminationPreferenceValues(true, "02:30", true);
|
||||
$(".wrapper-modal-window .action-save").click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
|
||||
"graderType":"Lab",
|
||||
@@ -689,7 +704,10 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
"metadata":{
|
||||
"visible_to_staff_only": true,
|
||||
"start":"2014-07-09T00:00:00.000Z",
|
||||
"due":"2014-07-10T00:00:00.000Z"
|
||||
"due":"2014-07-10T00:00:00.000Z",
|
||||
"is_time_limited": true,
|
||||
"is_proctored_enabled": true,
|
||||
"default_time_limit_minutes": 150
|
||||
}
|
||||
});
|
||||
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
|
||||
@@ -720,6 +738,27 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut
|
||||
expect($("#due_date").val()).toBe('7/10/2014');
|
||||
expect($("#grading_type").val()).toBe('Lab');
|
||||
expect($("#staff_lock").is(":checked")).toBe(true);
|
||||
expect($("#id_timed_examination").is(":checked")).toBe(true);
|
||||
expect($("#id_exam_proctoring").is(":checked")).toBe(true);
|
||||
expect($("#id_time_limit").val()).toBe("02:30");
|
||||
});
|
||||
|
||||
it('can be edited and enable/disable proctoring fields, when time_limit checkbox value changes', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
|
||||
setModalTimedExaminationPreferenceValues(true, "02:30", true);
|
||||
var target = $('#id_timed_examination');
|
||||
target.attr("checked","checked");
|
||||
target.click();
|
||||
expect($('#id_exam_proctoring')).toHaveAttr('disabled','disabled');
|
||||
expect($('#id_time_limit')).toHaveAttr('disabled','disabled');
|
||||
target.removeAttr("checked");
|
||||
target.click();
|
||||
expect($('#id_exam_proctoring')).not.toHaveAttr('disabled','disabled');
|
||||
expect($('#id_time_limit')).not.toHaveAttr('disabled','disabled');
|
||||
expect($('#id_time_limit').val()).toBe('00:30');
|
||||
expect($('#id_exam_proctoring')).not.toHaveAttr('checked');
|
||||
});
|
||||
|
||||
it('release date, due date, grading type, and staff lock can be cleared.', function() {
|
||||
|
||||
@@ -140,9 +140,17 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
},
|
||||
|
||||
editXBlock: function() {
|
||||
var enable_proctored_exams = false;
|
||||
if (this.model.get('category') === 'sequential' &&
|
||||
this.parentView.parentView.model.has('enable_proctored_exams')) {
|
||||
|
||||
enable_proctored_exams = this.parentView.parentView.model.get('enable_proctored_exams');
|
||||
}
|
||||
|
||||
var modal = CourseOutlineModalsFactory.getModal('edit', this.model, {
|
||||
onSave: this.refresh.bind(this),
|
||||
parentInfo: this.parentInfo,
|
||||
enable_proctored_exams: enable_proctored_exams,
|
||||
xblockType: XBlockViewUtils.getXBlockType(
|
||||
this.model.get('category'), this.parentView.model, true
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
) {
|
||||
'use strict';
|
||||
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
|
||||
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, StaffLockEditor;
|
||||
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, StaffLockEditor, TimedExaminationPreferenceEditor;
|
||||
|
||||
CourseOutlineXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
@@ -257,7 +257,94 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
};
|
||||
}
|
||||
});
|
||||
TimedExaminationPreferenceEditor = AbstractEditor.extend({
|
||||
templateName: 'timed-examination-preference-editor',
|
||||
className: 'edit-settings-timed-examination',
|
||||
|
||||
events : {
|
||||
'change #id_timed_examination': 'timedExamination',
|
||||
'focusout #id_time_limit': 'timeLimitFocusout'
|
||||
},
|
||||
timeLimitFocusout: function(event) {
|
||||
var selectedTimeLimit = $(event.currentTarget).val();
|
||||
if (!this.isValidTimeLimit(selectedTimeLimit)) {
|
||||
$(event.currentTarget).val("00:30");
|
||||
}
|
||||
},
|
||||
timedExamination: function (event) {
|
||||
event.preventDefault();
|
||||
if (!$(event.currentTarget).is(':checked')) {
|
||||
this.$('#id_exam_proctoring').attr('checked', false);
|
||||
this.$('#id_time_limit').val('00:30');
|
||||
this.$('#id_exam_proctoring').attr('disabled','disabled');
|
||||
this.$('#id_time_limit').attr('disabled', 'disabled');
|
||||
}
|
||||
else {
|
||||
this.$('#id_exam_proctoring').removeAttr('disabled');
|
||||
this.$('#id_time_limit').removeAttr('disabled');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.$('input.time').timepicker({
|
||||
'timeFormat' : 'H:i',
|
||||
'forceRoundTime': false
|
||||
});
|
||||
this.setExamTime(this.model.get('default_time_limit_minutes'));
|
||||
this.setExamTmePreference(this.model.get('is_time_limited'));
|
||||
this.setExamProctoring(this.model.get('is_proctored_enabled'));
|
||||
},
|
||||
setExamProctoring: function(value) {
|
||||
this.$('#id_exam_proctoring').prop('checked', value);
|
||||
},
|
||||
setExamTime: function(value) {
|
||||
var time = this.convertTimeLimitMinutesToString(value);
|
||||
this.$('#id_time_limit').val(time);
|
||||
},
|
||||
setExamTmePreference: function (value) {
|
||||
this.$('#id_timed_examination').prop('checked', value);
|
||||
if (!this.$('#id_timed_examination').is(':checked')) {
|
||||
this.$('#id_exam_proctoring').attr('disabled','disabled');
|
||||
this.$('#id_time_limit').attr('disabled', 'disabled');
|
||||
}
|
||||
},
|
||||
isExamTimeEnabled: function () {
|
||||
return this.$('#id_timed_examination').is(':checked');
|
||||
},
|
||||
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);
|
||||
},
|
||||
getExamTimeLimit: function () {
|
||||
return this.$('#id_time_limit').val();
|
||||
},
|
||||
convertTimeLimitMinutesToString: function (timeLimitMinutes) {
|
||||
var hoursStr = "" + Math.floor(timeLimitMinutes / 60);
|
||||
var actualMinutesStr = "" + (timeLimitMinutes % 60);
|
||||
hoursStr = "00".substring(0, 2 - hoursStr.length) + hoursStr;
|
||||
actualMinutesStr = "00".substring(0, 2 - actualMinutesStr.length) + actualMinutesStr;
|
||||
return hoursStr + ":" + actualMinutesStr;
|
||||
},
|
||||
convertTimeLimitToMinutes: function (time_limit) {
|
||||
var time = time_limit.split(':');
|
||||
var total_time = (parseInt(time[0]) * 60) + parseInt(time[1]);
|
||||
return total_time;
|
||||
},
|
||||
isExamProctoringEnabled: function () {
|
||||
return this.$('#id_exam_proctoring').is(':checked');
|
||||
},
|
||||
getRequestData: function () {
|
||||
var time_limit = this.getExamTimeLimit();
|
||||
return {
|
||||
metadata: {
|
||||
'is_time_limited': this.isExamTimeEnabled(),
|
||||
'is_proctored_enabled': this.isExamProctoringEnabled(),
|
||||
'default_time_limit_minutes': this.convertTimeLimitToMinutes(time_limit)
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
GradingEditor = AbstractEditor.extend({
|
||||
templateName: 'grading-editor',
|
||||
className: 'edit-settings-grading',
|
||||
@@ -358,11 +445,19 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
if (xblockInfo.isChapter()) {
|
||||
editors = [ReleaseDateEditor, StaffLockEditor];
|
||||
} else if (xblockInfo.isSequential()) {
|
||||
editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, StaffLockEditor];
|
||||
editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
|
||||
|
||||
// since timed/proctored exams are optional
|
||||
// we want it before the StaffLockEditor
|
||||
// to keep it closer to the GradingEditor
|
||||
if (options.enable_proctored_exams) {
|
||||
editors.push(TimedExaminationPreferenceEditor);
|
||||
}
|
||||
|
||||
editors.push(StaffLockEditor);
|
||||
} else if (xblockInfo.isVertical()) {
|
||||
editors = [StaffLockEditor];
|
||||
}
|
||||
|
||||
return new SettingsXBlockModal($.extend({
|
||||
editors: editors,
|
||||
model: xblockInfo
|
||||
|
||||
@@ -519,9 +519,14 @@
|
||||
.wrapper-modal-window-bulkpublish-subsection,
|
||||
.wrapper-modal-window-bulkpublish-unit,
|
||||
.course-outline-modal {
|
||||
|
||||
.exam-time-list-fields {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
.list-fields {
|
||||
|
||||
.field-message {
|
||||
color: $gray;
|
||||
font-size: ($baseline/2);
|
||||
}
|
||||
.field {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
@@ -625,7 +630,42 @@
|
||||
}
|
||||
|
||||
// UI: staff lock section
|
||||
.edit-staff-lock {
|
||||
.edit-staff-lock, .edit-settings-timed-examination {
|
||||
|
||||
.checkbox-cosmetic .input-checkbox {
|
||||
@extend %cont-text-sr;
|
||||
|
||||
// CASE: unchecked
|
||||
~ .tip-warning {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// CASE: checked
|
||||
&:checked {
|
||||
|
||||
~ .tip-warning {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// needed to override poorly scoped margin-bottom on any label element in a view (from _forms.scss)
|
||||
.checkbox-cosmetic .label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: timed and proctored exam section
|
||||
.edit-settings-timed-examination {
|
||||
|
||||
// give a little space between the sections
|
||||
padding-bottom: 10px;
|
||||
|
||||
// indent this group a bit to make it seem like
|
||||
// it is one group, under a header
|
||||
.modal-section-content {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.checkbox-cosmetic .input-checkbox {
|
||||
@extend %cont-text-sr;
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// reset to remove jquery-ui float
|
||||
// reset to remove jquery-ui float
|
||||
a.link-tab {
|
||||
float: none;
|
||||
}
|
||||
@@ -546,20 +546,24 @@ $outline-indent-width: $baseline;
|
||||
> .subsection-status .status-grading {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
> .subsection-status .status-timed-proctored-exam {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// status - grading
|
||||
.status-grading {
|
||||
.status-grading, .status-timed-proctored-exam {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.status-grading-value {
|
||||
.status-grading-value, .status-proctored-exam-value {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-grading-date {
|
||||
.status-grading-date, .status-due-date {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: ($baseline/4);
|
||||
|
||||
Reference in New Issue
Block a user