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") %> +