diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0d7fe2c2d96..86d12adc485 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,7 @@ Unreleased
* Changed behavior of practice exam reset to create a new exam attempt instead
of rolling back state of the current attempt.
+* Added new proctoring info panel to expose onboarding exam status to learners
[2.4.9] - 2020-11-17
~~~~~~~~~~~~~~~~~~~~
diff --git a/edx_proctoring/settings/common.py b/edx_proctoring/settings/common.py
index 5963e5596d5..84fbac2612a 100644
--- a/edx_proctoring/settings/common.py
+++ b/edx_proctoring/settings/common.py
@@ -17,6 +17,7 @@ def plugin_settings(settings):
'proctoring/js/models/proctored_exam_allowance_model.js',
'proctoring/js/models/proctored_exam_attempt_model.js',
'proctoring/js/models/proctored_exam_model.js',
+ 'proctoring/js/models/learner_onboarding_model.js',
'proctoring/js/collections/proctored_exam_allowance_collection.js',
'proctoring/js/collections/proctored_exam_attempt_collection.js',
'proctoring/js/collections/proctored_exam_collection.js',
@@ -25,6 +26,7 @@ def plugin_settings(settings):
'proctoring/js/views/proctored_exam_allowance_view.js',
'proctoring/js/views/proctored_exam_attempt_view.js',
'proctoring/js/views/proctored_exam_view.js',
+ 'proctoring/js/views/proctored_exam_info.js',
'proctoring/js/views/proctored_exam_instructor_launch.js',
'proctoring/js/proctored_app.js',
'proctoring/js/exam_action_handler.js'
diff --git a/edx_proctoring/static/proctoring/js/models/learner_onboarding_model.js b/edx_proctoring/static/proctoring/js/models/learner_onboarding_model.js
new file mode 100644
index 00000000000..b1d3d60c0b6
--- /dev/null
+++ b/edx_proctoring/static/proctoring/js/models/learner_onboarding_model.js
@@ -0,0 +1,9 @@
+(function(Backbone) {
+ 'use strict';
+
+ var LearnerOnboardingModel = Backbone.Model.extend({
+ url: '/api/edx_proctoring/v1/user_onboarding/status/'
+ });
+
+ this.LearnerOnboardingModel = LearnerOnboardingModel;
+}).call(this, Backbone);
diff --git a/edx_proctoring/static/proctoring/js/proctored_app.js b/edx_proctoring/static/proctoring/js/proctored_app.js
index 7dbe2ccc414..767d95c59fb 100644
--- a/edx_proctoring/static/proctoring/js/proctored_app.js
+++ b/edx_proctoring/static/proctoring/js/proctored_app.js
@@ -1,4 +1,4 @@
-/* globals ProctoredExamModel:false */
+/* globals ProctoredExamModel:false LearnerOnboardingModel:false */
$(function() {
'use strict';
@@ -7,5 +7,10 @@ $(function() {
proctored_template: '#proctored-exam-status-tpl',
model: new ProctoredExamModel()
});
+ var proctoredExamInfoView = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
proctoredExamView.render();
+ proctoredExamInfoView.render();
});
diff --git a/edx_proctoring/static/proctoring/js/views/proctored_exam_info.js b/edx_proctoring/static/proctoring/js/views/proctored_exam_info.js
new file mode 100644
index 00000000000..48a44460e85
--- /dev/null
+++ b/edx_proctoring/static/proctoring/js/views/proctored_exam_info.js
@@ -0,0 +1,154 @@
+(function(Backbone, $) {
+ 'use strict';
+
+ var examStatusReadableFormat, notStartedText, startedText, submittedText;
+
+ edx.courseware = edx.courseware || {};
+ edx.courseware.proctored_exam = edx.courseware.proctored_exam || {};
+
+ notStartedText = {
+ status: gettext('Not Started'),
+ message: gettext('You have not started your onboarding exam.')
+ };
+ startedText = {
+ status: gettext('Started'),
+ message: gettext('You have started your onboarding exam.')
+ };
+ submittedText = {
+ status: gettext('Submitted'),
+ message: gettext('You have submitted your onboarding exam.')
+ };
+
+ examStatusReadableFormat = {
+ created: notStartedText,
+ download_software_clicked: notStartedText,
+ ready_to_start: notStartedText,
+ started: startedText,
+ ready_to_submit: startedText,
+ second_review_required: submittedText,
+ submitted: submittedText,
+ verified: {
+ status: gettext('Verified'),
+ message: gettext('You can now take proctored exams in this course.')
+ },
+ rejected: {
+ status: gettext('Rejected'),
+ message: gettext('Your onboarding exam has been rejected. Please retry onboarding.')
+ },
+ error: {
+ status: gettext('Error'),
+ message: gettext('An error has occurred during your onboarding exam. Please retry onboarding.')
+ }
+ };
+
+ edx.courseware.proctored_exam.ProctoredExamInfo = Backbone.View.extend({
+ initialize: function() {
+ this.course_id = this.$el.data('course-id');
+ this.model.url = this.model.url + '?course_id=' + encodeURIComponent(this.course_id);
+ this.template_url = '/static/proctoring/templates/proctored-exam-info.underscore';
+ this.status = '';
+
+ this.loadTemplateData();
+ },
+
+ updateCss: function() {
+ var $el = $(this.el);
+ var color = '#b20610';
+ if (this.status === 'verified') {
+ color = '#008100';
+ } else if (['submitted', 'second_review_required'].includes(this.status)) {
+ color = '#0d4e6c';
+ }
+
+ $el.find('.proctoring-info').css({
+ padding: '10px',
+ border: '1px solid #e7e7e7',
+ 'border-top': '5px solid ' + color,
+ 'margin-bottom': '15px'
+ });
+
+ $el.find('.onboarding-status').css({
+ 'font-weight': 'bold',
+ 'margin-bottom': '15px'
+ });
+
+ $el.find('.onboarding-status-message').css({
+ 'margin-bottom': '15px'
+ });
+
+ $el.find('.action').css({
+ display: 'block',
+ 'font-weight': '600',
+ 'text-align': 'center',
+ 'text-decoration': 'none',
+ padding: '15px 20px',
+ border: 'none'
+ });
+
+ $el.find('.action-onboarding').css({
+ color: '#ffffff',
+ background: '#98050e',
+ 'margin-bottom': '15px'
+ });
+
+ $el.find('.action-info-link').css({
+ border: '1px solid #0d4e6c'
+ });
+ },
+
+ getExamAttemptText: function(status) {
+ if (status in examStatusReadableFormat) {
+ return examStatusReadableFormat[status];
+ } else {
+ return {status: status || 'Not Started', message: ''};
+ }
+ },
+
+ shouldShowExamLink: function(status) {
+ // show the exam link if the user should retry onboarding, or if they haven't submitted the exam
+ var NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified'];
+ return !NO_SHOW_STATES.includes(status);
+ },
+
+ render: function() {
+ var statusText = {};
+ var data = this.model.toJSON();
+ if (this.template) {
+ this.status = data.onboarding_status;
+ statusText = this.getExamAttemptText(data.onboarding_status);
+ data = {
+ onboardingStatus: statusText.status,
+ onboardingMessage: statusText.message,
+ showOnboardingReminder: data.onboarding_status !== 'verified',
+ showOnboardingExamLink: this.shouldShowExamLink(data.onboarding_status),
+ onboardingLink: data.onboarding_link
+ };
+
+ $(this.el).html(this.template(data));
+ }
+ },
+
+ loadTemplateData: function() {
+ var self = this;
+ // only load data/render if course_id is defined
+ if (self.course_id) {
+ $.ajax({url: self.template_url, dataType: 'html'})
+ .done(function(templateData) {
+ self.template = _.template(templateData);
+ self.hydrate();
+ });
+ }
+ },
+
+ hydrate: function() {
+ var self = this;
+ self.model.fetch({
+ success: function() {
+ self.render();
+ self.updateCss();
+ }
+ });
+ }
+ });
+ this.edx.courseware.proctored_exam.ProctoredExamInfo = edx.courseware.proctored_exam.ProctoredExamInfo;
+}).call(this, Backbone, $, _, gettext);
diff --git a/edx_proctoring/static/proctoring/spec/proctored_exam_info_spec.js b/edx_proctoring/static/proctoring/spec/proctored_exam_info_spec.js
new file mode 100644
index 00000000000..f8823aafa83
--- /dev/null
+++ b/edx_proctoring/static/proctoring/spec/proctored_exam_info_spec.js
@@ -0,0 +1,309 @@
+/* global LearnerOnboardingModel:false */
+describe('ProctoredExamInfo', function() {
+ 'use strict';
+
+ var html = '';
+
+ var errorGettingOnboardingProfile = {
+ detail: 'There is no onboarding exam related to this course id.'
+ };
+
+ function expectedProctoredExamInfoJson(status) {
+ return (
+ {
+ onboarding_status: status,
+ onboarding_link: 'onboarding_link'
+ }
+ );
+ }
+
+ beforeEach(function() {
+ html = '
' +
+ '
<%= gettext("This course contains proctored exams") %>
' +
+ '<% if (onboardingStatus) { %>' +
+ '
' +
+ '<%= gettext("Current Onboarding Status:") %> ' +
+ '<%= onboardingStatus %>' +
+ '
' +
+ '
' +
+ '<%= onboardingMessage %>' +
+ '
' +
+ '<%} %>' +
+ '
' +
+ '<% if (showOnboardingReminder) { %>' +
+ '
' +
+ '<%= gettext("You must complete the onboarding process prior to taking any proctored exam.") %>
' +
+ '
' +
+ '<%= gettext("Onboarding profile review, including identity verification, can take 2+ business days.") %>' +
+ '
' +
+ '<%} %>' +
+ '
' +
+ '<% if (showOnboardingExamLink) { %>' +
+ '
' +
+ '<%= gettext("Complete Onboarding") %>' +
+ '<%} %>' +
+ '
' +
+ '<%= gettext("Review instructions and system requirements for proctored exams") %>' +
+ '
';
+ this.server = sinon.fakeServer.create();
+ this.server.autoRespond = true;
+ setFixtures('');
+
+ // load the underscore template response before calling the proctored exam allowance view.
+ this.server.respondWith('GET', '/static/proctoring/templates/proctored-exam-info.underscore',
+ [
+ 200,
+ {'Content-Type': 'text/html'},
+ html
+ ]
+ );
+ });
+
+ afterEach(function() {
+ this.server.restore();
+ });
+
+ it('should not render proctoring info panel when template is not defined', function() {
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.proctored_exam_info.render();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info-panel').html())
+ .toHaveLength(0);
+ });
+
+ it('should not render proctoring info panel if no course id is provided', function() {
+ setFixtures('');
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=',
+ [
+ 400,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(errorGettingOnboardingProfile)
+ ]
+ );
+
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info-panel').html())
+ .toHaveLength(0);
+ });
+
+ it('should not render proctoring info panel for exam with 404 response', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 404,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(errorGettingOnboardingProfile)
+ ]
+ );
+
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info-panel').html())
+ .toHaveLength(0);
+ });
+
+ it('should render proctoring info panel correctly for exam with other status', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson('other'))
+ ]
+ );
+
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(178, 6, 16)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('other');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status-message').text())
+ .toHaveLength(0);
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .toContain('Complete Onboarding');
+ });
+
+ it('should render proctoring info panel correctly for exam with empty string status', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson(''))
+ ]
+ );
+
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(178, 6, 16)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('Not Started');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status-message').text())
+ .toHaveLength(0);
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .toContain('Complete Onboarding');
+ });
+
+ it('should render proctoring info panel correctly for created exam', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson('created'))
+ ]
+ );
+
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(178, 6, 16)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('Not Started');
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .toContain('Complete Onboarding');
+ });
+
+ it('should render proctoring info panel correctly for started exam', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson('started'))
+ ]
+ );
+
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(178, 6, 16)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('Started');
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .toContain('Complete Onboarding');
+ });
+
+ it('should render proctoring info panel correctly for second_review_required exam', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson('second_review_required'))
+ ]
+ );
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(13, 78, 108)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('Submitted');
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .not.toContain('Complete Onboarding');
+ });
+
+ it('should render proctoring info panel correctly for verified exam', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson('verified'))
+ ]
+ );
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(0, 129, 0)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('Verified');
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .not.toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .not.toContain('Complete Onboarding');
+ });
+
+ it('should render proctoring info panel correctly for rejected exam', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status/?course_id=test_course_id',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredExamInfoJson('rejected'))
+ ]
+ );
+ this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
+ el: $('.proctoring-info-panel'),
+ model: new LearnerOnboardingModel()
+ });
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_info.$el.find('.proctoring-info').css('border-top'))
+ .toEqual('5px solid rgb(178, 6, 16)');
+ expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
+ .toContain('Rejected');
+ expect(this.proctored_exam_info.$el.find('.onboarding-reminder').html())
+ .toContain('You must complete the onboarding process');
+ expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
+ .toContain('Complete Onboarding');
+ });
+});
diff --git a/edx_proctoring/static/proctoring/templates/proctored-exam-info.underscore b/edx_proctoring/static/proctoring/templates/proctored-exam-info.underscore
new file mode 100644
index 00000000000..234bf00ba9b
--- /dev/null
+++ b/edx_proctoring/static/proctoring/templates/proctored-exam-info.underscore
@@ -0,0 +1,23 @@
+
+
<%= gettext("This course contains proctored exams") %>
+ <% if (onboardingStatus) { %>
+
+ <%= gettext("Current Onboarding Status:") %> <%= onboardingStatus %>
+
+
+ <%= onboardingMessage %>
+
+ <%} %>
+
+ <% if (showOnboardingReminder) { %>
+
<%= gettext("You must complete the onboarding process prior to taking any proctored exam.") %>
+
+ <%= gettext("Onboarding profile review, including identity verification, can take 2+ business days.") %>
+
+ <%} %>
+
+ <% if (showOnboardingExamLink) { %>
+
<%= gettext("Complete Onboarding") %>
+ <%} %>
+
<%= gettext("Review instructions and system requirements for proctored exams") %>
+