Merge pull request #19684 from edx/mroytman/EDUCATOR-3952-practice-proctored-to-onboarding

Change "practice exam" to "onboarding" for courses that support onboarding exams.
This commit is contained in:
Michael Roytman
2019-02-08 11:48:56 -05:00
committed by GitHub
10 changed files with 263 additions and 72 deletions

View File

@@ -76,7 +76,8 @@ def register_special_exams(course_key):
'time_limit_mins': timed_exam.default_time_limit_minutes,
'due_date': timed_exam.due,
'is_proctored': timed_exam.is_proctored_exam,
'is_practice_exam': timed_exam.is_practice_exam,
# backends that support onboarding exams will treat onboarding exams as practice
'is_practice_exam': timed_exam.is_practice_exam or timed_exam.is_onboarding_exam,
'is_active': True,
'hide_after_due': timed_exam.hide_after_due,
'backend': course.proctoring_provider,
@@ -106,7 +107,7 @@ def register_special_exams(course_key):
}
# only create/update exam policy for the proctored exams
if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam:
if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam and not timed_exam.is_onboarding_exam:
try:
update_review_policy(**exam_review_policy_metadata)
except ProctoredExamReviewPolicyNotFoundException:

View File

@@ -61,10 +61,43 @@ class TestProctoredExams(ModuleStoreTestCase):
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_practice_exam'], sequence.is_practice_exam or sequence.is_onboarding_exam)
self.assertEqual(exam['is_active'], expected_active)
self.assertEqual(exam['backend'], self.course.proctoring_provider)
@ddt.data(
(False, True),
(True, False),
)
@ddt.unpack
def test_onboarding_exam_is_practice_exam(self, is_practice_exam, is_onboarding_exam):
"""
Check that an onboarding exam is treated as a practice exam when
communicating with the edx-proctoring subsystem.
"""
default_time_limit_minutes = 10
is_proctored_exam = True
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
sequence = ItemFactory.create(
parent=chapter,
category='sequential',
display_name='Test Proctored Exam',
graded=True,
is_time_limited=True,
default_time_limit_minutes=default_time_limit_minutes,
is_proctored_exam=is_proctored_exam,
is_practice_exam=is_practice_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
exam_review_rules="allow_use_of_paper",
hide_after_due=True,
is_onboarding_exam=is_onboarding_exam,
)
listen_for_course_publish(self, self.course.id)
self._verify_exam_data(sequence, True)
@ddt.data(
(True, False, True, False, False),
(False, False, True, False, False),
@@ -93,6 +126,7 @@ class TestProctoredExams(ModuleStoreTestCase):
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
exam_review_rules="allow_use_of_paper",
hide_after_due=hide_after_due,
is_onboarding_exam=False,
)
listen_for_course_publish(self, self.course.id)
@@ -125,6 +159,7 @@ class TestProctoredExams(ModuleStoreTestCase):
default_time_limit_minutes=10,
is_proctored_exam=True,
hide_after_due=False,
is_onboarding_exam=False,
)
listen_for_course_publish(self, self.course.id)

View File

@@ -68,7 +68,7 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.services import ConfigurationService, SettingsService
from xmodule.tabs import CourseTabList
from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT, PREVIEW_VIEWS, STUDENT_VIEW, STUDIO_VIEW
from edx_proctoring.api import get_exam_configuration_dashboard_url
from edx_proctoring.api import get_exam_configuration_dashboard_url, does_backend_support_onboarding
__all__ = [
'orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler', 'xblock_container_handler'
@@ -1222,6 +1222,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
})
elif xblock.category == 'sequential':
rules_url = settings.PROCTORING_SETTINGS.get('LINK_URLS', {}).get('online_proctoring_rules', "")
supports_onboarding = does_backend_support_onboarding(course.proctoring_provider)
proctoring_exam_configuration_link = None
if xblock.is_proctored_exam:
@@ -1232,10 +1233,12 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'is_proctored_exam': xblock.is_proctored_exam,
'online_proctoring_rules': rules_url,
'is_practice_exam': xblock.is_practice_exam,
'is_onboarding_exam': xblock.is_onboarding_exam,
'is_time_limited': xblock.is_time_limited,
'exam_review_rules': xblock.exam_review_rules,
'default_time_limit_minutes': xblock.default_time_limit_minutes,
'proctoring_exam_configuration_link': proctoring_exam_configuration_link,
'supports_onboarding': supports_onboarding,
})
# Update with gating info

View File

@@ -2786,8 +2786,10 @@ class TestXBlockInfo(ItemTest):
self.assertIsNone(xblock_info.get('child_info', None))
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
@patch('contentstore.views.item.does_backend_support_onboarding')
@patch('contentstore.views.item.get_exam_configuration_dashboard_url')
def test_proctored_exam_xblock_info(self, get_exam_configuration_dashboard_url_patch):
def test_proctored_exam_xblock_info(self, get_exam_configuration_dashboard_url_patch,
does_backend_support_onboarding_patch):
self.course.enable_proctored_exams = True
self.course.save()
self.store.update_item(self.course, self.user.id)
@@ -2805,11 +2807,12 @@ class TestXBlockInfo(ItemTest):
parent_location=self.chapter.location, category='sequential',
display_name="Test Lesson 1", user_id=self.user.id,
is_proctored_exam=True, is_time_limited=True,
default_time_limit_minutes=100
default_time_limit_minutes=100, is_onboarding_exam=False
)
sequential = modulestore().get_item(sequential.location)
get_exam_configuration_dashboard_url_patch.return_value = 'test_url'
does_backend_support_onboarding_patch.return_value = True
xblock_info = create_xblock_info(
sequential,
include_child_info=True,
@@ -2820,6 +2823,8 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['is_time_limited'], True)
self.assertEqual(xblock_info['default_time_limit_minutes'], 100)
self.assertEqual(xblock_info['proctoring_exam_configuration_link'], 'test_url')
self.assertEqual(xblock_info['supports_onboarding'], True)
self.assertEqual(xblock_info['is_onboarding_exam'], False)
get_exam_configuration_dashboard_url_patch.assert_called_with(self.course.id, xblock_info['id'])

View File

@@ -65,6 +65,7 @@ class CourseMetadata(object):
'chrome',
'default_tab',
'highlights_enabled_for_messaging',
'is_onboarding_exam',
]
@classmethod

View File

@@ -14,6 +14,7 @@ describe('CourseOutlinePage', function() {
selectVisibilitySettings, selectAdvancedSettings, createMockCourseJSON, createMockSectionJSON,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
selectOnboardingExam,
mockOutlinePage = readFixtures('templates/mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('templates/mock/mock-course-rerun-notification.underscore');
@@ -1025,6 +1026,12 @@ describe('CourseOutlinePage', function() {
$('.field-time-limit input').trigger('focusout');
};
selectOnboardingExam = function(time_limit) {
$('input.onboarding_exam').prop('checked', true).trigger('change');
$('.field-time-limit input').val(time_limit);
$('.field-time-limit input').trigger('focusout');
};
selectPrerequisite = function() {
$('#is_prereq').prop('checked', true).trigger('change');
};
@@ -1070,7 +1077,8 @@ describe('CourseOutlinePage', function() {
is_time_limited: false,
exam_review_rules: '',
is_proctored_enabled: false,
default_time_limit_minutes: null
default_time_limit_minutes: null,
is_onboarding_exam: false
}
};
@@ -1274,7 +1282,8 @@ describe('CourseOutlinePage', function() {
is_practice_exam: false,
is_proctored_enabled: false,
default_time_limit_minutes: 150,
hide_after_due: true
hide_after_due: true,
is_onboarding_exam: false,
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
@@ -1339,6 +1348,96 @@ describe('CourseOutlinePage', function() {
$('.wrapper-modal-window .action-save').click();
});
it('can select the onboarding exam when a course supports onboarding', function () {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: true,
is_proctored_exam: true,
default_time_limit_minutes: 150,
supports_onboarding: true,
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectOnboardingExam('00:30');
// time limit should be visible, review rules should be hidden
checkOptionFieldVisibility(true, false);
$('.wrapper-modal-window .action-save').click();
});
it('does not show practice exam option when course supports onboarding', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: true,
is_proctored_exam: true,
default_time_limit_minutes: 150,
supports_onboarding: true,
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.practice_exam')).not.toExist();
expect($('input.onboarding_exam')).toExist();
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('does not show onboarding exam option when course does not support onboarding', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: true,
is_proctored_exam: true,
default_time_limit_minutes: 150,
supports_onboarding: false,
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.practice_exam')).toExist();
expect($('input.onboarding_exam')).not.toExist();
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can select the timed exam', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
@@ -1525,6 +1624,36 @@ describe('CourseOutlinePage', function() {
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can show a saved onboarding exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: true,
default_time_limit_minutes: 150,
supports_onboarding: true,
is_onboarding_exam: true
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.onboarding_exam').is(':checked')).toBe(true);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('does not show proctored settings if proctored exams not enabled', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({

View File

@@ -70,8 +70,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
},
save: function(event) {
var requestData;
event.preventDefault();
var requestData = this.getRequestData();
requestData = this.getRequestData();
if (!_.isEqual(requestData, {metadata: {}})) {
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
success: this.options.onSave
@@ -122,10 +124,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
},
initializeEditors: function() {
var tabsTemplate;
var tabs = this.options.tabs;
if (tabs && tabs.length > 0) {
if (tabs.length > 1) {
var tabsTemplate = this.loadTemplate('settings-modal-tabs');
tabsTemplate = this.loadTemplate('settings-modal-tabs');
HtmlUtils.setHtml(this.$('.modal-section'), HtmlUtils.HTML(tabsTemplate({tabs: tabs})));
_.each(this.options.tabs, function(tab) {
this.options.editors.push.apply(
@@ -411,9 +414,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
className: 'edit-settings-timed-examination',
events: {
'change input.no_special_exam': 'notTimedExam',
'change input.timed_exam': 'setTimedExam',
'change input.practice_exam': 'setPracticeExam',
'change input.timed_exam': 'setSpecialExamWithoutRules',
'change input.practice_exam': 'setSpecialExamWithoutRules',
'change input.proctored_exam': 'setProctoredExam',
'change input.onboarding_exam': 'setSpecialExamWithoutRules',
'focusout .field-time-limit input': 'timeLimitFocusout'
},
notTimedExam: function(event) {
@@ -433,11 +437,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.$('.field-exam-review-rules').hide();
}
},
setTimedExam: function(event) {
event.preventDefault();
this.selectSpecialExam(false);
},
setPracticeExam: function(event) {
setSpecialExamWithoutRules: function(event) {
event.preventDefault();
this.selectSpecialExam(false);
},
@@ -446,8 +446,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.selectSpecialExam(true);
},
timeLimitFocusout: function(event) {
var selectedTimeLimit;
event.preventDefault();
var selectedTimeLimit = $(event.currentTarget).val();
selectedTimeLimit = $(event.currentTarget).val();
if (!this.isValidTimeLimit(selectedTimeLimit)) {
$(event.currentTarget).val('00:30');
}
@@ -462,24 +464,26 @@ 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.model.get('is_practice_exam'), this.model.get('is_onboarding_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) {
setExamType: function(isTimeLimited, isProctoredExam, isPracticeExam, isOnboardingExam) {
this.$('.field-time-limit').hide();
this.$('.field-exam-review-rules').hide();
if (!is_time_limited) {
if (!isTimeLimited) {
this.$('input.no_special_exam').prop('checked', true);
return;
}
this.$('.field-time-limit').show();
if (this.options.enable_proctored_exams && is_proctored_exam) {
if (is_practice_exam) {
if (this.options.enable_proctored_exams && isProctoredExam) {
if (isOnboardingExam) {
this.$('input.onboarding_exam').prop('checked', true);
} else if (isPracticeExam) {
this.$('input.practice_exam').prop('checked', true);
} else {
this.$('input.proctored_exam').prop('checked', true);
@@ -499,9 +503,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
setReviewRules: function(value) {
this.$('.field-exam-review-rules textarea').val(value);
},
isValidTimeLimit: function(time_limit) {
isValidTimeLimit: function(timeLimit) {
var pattern = new RegExp('^\\d{1,2}:[0-5][0-9]$');
return pattern.test(time_limit) && time_limit !== '00:00';
return pattern.test(timeLimit) && timeLimit !== '00:00';
},
getExamTimeLimit: function() {
return this.$('.field-time-limit input').val();
@@ -513,48 +517,32 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
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;
convertTimeLimitToMinutes: function(timeLimit) {
var time = timeLimit.split(':');
var totalTime = (parseInt(time[0], 10) * 60) + parseInt(time[1], 10);
return totalTime;
},
getRequestData: function() {
var is_time_limited;
var is_practice_exam;
var is_proctored_exam;
var time_limit = this.getExamTimeLimit();
var exam_review_rules = this.$('.field-exam-review-rules textarea').val();
if (this.$('input.no_special_exam').is(':checked')) {
is_time_limited = false;
is_practice_exam = false;
is_proctored_exam = false;
} else if (this.$('input.timed_exam').is(':checked')) {
is_time_limited = true;
is_practice_exam = false;
is_proctored_exam = false;
} else if (this.$('input.proctored_exam').is(':checked')) {
is_time_limited = true;
is_practice_exam = false;
is_proctored_exam = true;
} else if (this.$('input.practice_exam').is(':checked')) {
is_time_limited = true;
is_practice_exam = true;
is_proctored_exam = true;
}
var isNoSpecialExamChecked = this.$('input.no_special_exam').is(':checked');
var isProctoredExamChecked = this.$('input.proctored_exam').is(':checked');
var isPracticeExamChecked = this.$('input.practice_exam').is(':checked');
var isOnboardingExamChecked = this.$('input.onboarding_exam').is(':checked');
var timeLimit = this.getExamTimeLimit();
var examReviewRules = this.$('.field-exam-review-rules textarea').val();
return {
metadata: {
is_practice_exam: is_practice_exam,
is_time_limited: is_time_limited,
exam_review_rules: exam_review_rules,
is_practice_exam: isPracticeExamChecked,
is_time_limited: !isNoSpecialExamChecked,
exam_review_rules: examReviewRules,
// We have to use the legacy field name
// as the Ajax handler directly populates
// the xBlocks fields. We will have to
// update this call site when we migrate
// seq_module.py to use 'is_proctored_exam'
is_proctored_enabled: is_proctored_exam,
default_time_limit_minutes: this.convertTimeLimitToMinutes(time_limit)
is_proctored_enabled: isProctoredExamChecked || isPracticeExamChecked || isOnboardingExamChecked,
default_time_limit_minutes: this.convertTimeLimitToMinutes(timeLimit),
is_onboarding_exam: isOnboardingExamChecked
}
};
}
@@ -569,10 +557,12 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'keyup #prereq_min_score': 'validateScoreAndCompletion'
},
afterRender: function() {
var prereq, prereqMinScore, prereqMinCompletion;
AbstractEditor.prototype.afterRender.call(this);
var prereq = this.model.get('prereq') || '';
var prereqMinScore = this.model.get('prereq_min_score') || '100';
var prereqMinCompletion = this.model.get('prereq_min_completion') || '100';
prereq = this.model.get('prereq') || '';
prereqMinScore = this.model.get('prereq_min_score') || '100';
prereqMinCompletion = this.model.get('prereq_min_completion') || '100';
this.$('#is_prereq').prop('checked', this.model.get('is_prereq'));
this.$('#prereq option[value="' + prereq + '"]').prop('selected', true);
this.$('#prereq_min_score').val(prereqMinScore);
@@ -853,9 +843,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
},
toggleUnlockWarning: function() {
var display;
var warning = this.$('.staff-lock .tip-warning');
if (warning) {
var display;
if (this.currentVisibility() !== 'staff_only') {
display = 'block';
} else {
@@ -868,8 +858,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
},
getRequestData: function() {
var metadata;
if (this.hasChanges()) {
var metadata = {};
metadata = {};
if (this.currentVisibility() === 'staff_only') {
metadata.visible_to_staff_only = true;
metadata.hide_after_due = null;

View File

@@ -84,15 +84,19 @@ if (xblockInfo.get('graded')) {
var is_proctored_exam = xblockInfo.get('is_proctored_exam');
var is_practice_exam = xblockInfo.get('is_practice_exam');
var is_onboarding_exam = xblockInfo.get('is_onboarding_exam');
var exam_value;
if (is_proctored_exam) {
if (is_practice_exam) {
var exam_value = gettext('Practice proctored Exam');
if (is_onboarding_exam) {
exam_value = gettext('Onboarding Exam');
} else if (is_practice_exam) {
exam_value = gettext('Practice proctored Exam');
} else {
var exam_value = gettext('Proctored Exam');
exam_value = gettext('Proctored Exam');
}
} else {
var exam_value = gettext('Timed Exam');
exam_value = gettext('Timed Exam');
}
%>
<% if (parentInfo) { %>

View File

@@ -19,12 +19,23 @@
<%- gettext('Proctored') %>
</label>
<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>
<label class="label">
<input type="radio" name="exam_type" class="input input-radio practice_exam"
aria-describedby="practice-exam-description"/>
<%- gettext('Practice Proctored') %>
</label>
<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>
<% var supports_onboarding = xblockInfo.get('supports_onboarding'); %>
<% if (supports_onboarding) { %>
<label class="label">
<input type="radio" name="exam_type" class="input input-radio onboarding_exam"
aria-describedby="onboarding-exam-description"/>
<%- gettext('Onboarding') %>
</label>
<p class='field-message' id='onboarding-exam-description'> <%- gettext("Use Onboarding to introduce learners to proctoring, verify their identity, and create an onboarding profile. Learners must complete the onboarding profile step prior to taking a proctored exam. Profile reviews take 2+ business days.") %> </p>
<% } else { %>
<label class="label">
<input type="radio" name="exam_type" class="input input-radio practice_exam"
aria-describedby="practice-exam-description"/>
<%- gettext('Practice Proctored') %>
</label>
<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>
<% } %>
<% } %>
</div>
<div class="list-fields list-input exam-options">
@@ -45,6 +56,7 @@
<% var online_proctoring_rules = xblockInfo.get('online_proctoring_rules'); %>
<p class='field-message' id='review-rules-description'>
<% if (online_proctoring_rules) { %>
% // xss-lint: disable=underscore-not-escaped %>
<%= edx.HtmlUtils.interpolateHtml(
gettext('Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam, along with the {linkStart}general proctored exam rules{linkEnd}.'),
{

View File

@@ -125,6 +125,15 @@ class ProctoringFields(object):
scope=Scope.settings,
)
is_onboarding_exam = Boolean(
display_name=_("Is Onboarding Exam"),
help=_(
"This setting indicates whether this exam is an onboarding exam."
),
default=False,
scope=Scope.settings,
)
def _get_course(self):
"""
Return course by course id.