From 6cf5516a84ed3f8d065eae1ea384e30c4e382f70 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 24 Jul 2015 20:22:38 -0400 Subject: [PATCH] Integration of edx_proctoring into the LMS --- cms/djangoapps/contentstore/proctoring.py | 4 +- cms/djangoapps/contentstore/views/item.py | 3 +- cms/envs/aws.py | 1 - cms/envs/bok_choy.env.json | 3 +- cms/envs/bok_choy.py | 2 + cms/envs/common.py | 9 +- .../spec/views/pages/course_outline_spec.js | 3 + .../js/views/modals/course_outline_modals.js | 21 +- ...d-examination-preference-editor.underscore | 11 + common/lib/xmodule/xmodule/seq_module.py | 86 ++++- .../test/acceptance/pages/lms/courseware.py | 30 ++ .../pages/lms/instructor_dashboard.py | 92 +++++ .../test/acceptance/pages/studio/overview.py | 54 +++ .../tests/lms/test_lms_courseware.py | 169 ++++++++- .../lms/test_lms_instructor_dashboard.py | 180 +++++++++- lms/djangoapps/courseware/module_render.py | 77 +++- .../courseware/tests/test_module_render.py | 335 ++++++++++++++++++ lms/djangoapps/instructor/services.py | 71 ++++ lms/djangoapps/instructor/tests/test_api.py | 30 ++ .../instructor/tests/test_proctoring.py | 44 +++ .../instructor/tests/test_services.py | 116 ++++++ lms/djangoapps/instructor/views/api.py | 37 ++ lms/djangoapps/instructor/views/api_urls.py | 4 + .../instructor/views/instructor_dashboard.py | 19 + lms/djangoapps/instructor_analytics/basic.py | 21 ++ .../instructor_analytics/tests/test_basic.py | 47 ++- lms/djangoapps/instructor_task/api.py | 15 + lms/djangoapps/instructor_task/tasks.py | 12 + .../instructor_task/tasks_helper.py | 35 +- lms/envs/acceptance.py | 4 +- lms/envs/aws.py | 6 +- lms/envs/bok_choy.env.json | 3 +- lms/envs/bok_choy.py | 2 + lms/envs/common.py | 23 +- lms/startup.py | 11 + .../instructor_dashboard/data_download.coffee | 20 ++ .../instructor_dashboard.coffee | 10 + .../js/instructor_dashboard/proctoring.js | 22 ++ .../sass/course/courseware/_courseware.scss | 286 ++++++++++++--- .../sass/course/courseware/_sidebar.scss | 13 + .../sass/course/instructor/_instructor_2.scss | 186 +++++++++- .../course/layout/_courseware_preview.scss | 44 +++ lms/templates/courseware/accordion.html | 30 +- .../courseware/course_navigation.html | 12 + lms/templates/courseware/courseware.html | 24 +- .../proctored-exam-controls.underscore | 5 + .../proctored-exam-status.underscore | 24 ++ .../instructor_dashboard_2/data_download.html | 4 + .../instructor_dashboard_2.html | 2 + .../instructor_dashboard_2/proctoring.html | 18 + lms/templates/seq_module.html | 2 - lms/templates/wiki/base.html | 1 + lms/urls.py | 5 + openedx/core/djangoapps/credit/services.py | 132 +++++++ openedx/core/djangoapps/credit/tasks.py | 5 +- .../djangoapps/credit/tests/test_services.py | 185 ++++++++++ .../djangoapps/credit/tests/test_tasks.py | 26 +- requirements/edx/github.txt | 3 +- test_root/db/test_edx.db-journal | 0 59 files changed, 2541 insertions(+), 98 deletions(-) create mode 100644 lms/djangoapps/instructor/services.py create mode 100644 lms/djangoapps/instructor/tests/test_proctoring.py create mode 100644 lms/djangoapps/instructor/tests/test_services.py create mode 100644 lms/static/js/instructor_dashboard/proctoring.js create mode 100644 lms/templates/courseware/proctored-exam-controls.underscore create mode 100644 lms/templates/courseware/proctored-exam-status.underscore create mode 100644 lms/templates/instructor/instructor_dashboard_2/proctoring.html create mode 100644 openedx/core/djangoapps/credit/services.py create mode 100644 openedx/core/djangoapps/credit/tests/test_services.py delete mode 100644 test_root/db/test_edx.db-journal diff --git a/cms/djangoapps/contentstore/proctoring.py b/cms/djangoapps/contentstore/proctoring.py index 06be401a28..2db79e3b2f 100644 --- a/cms/djangoapps/contentstore/proctoring.py +++ b/cms/djangoapps/contentstore/proctoring.py @@ -28,7 +28,7 @@ def register_proctored_exams(course_key): This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the edx-proctoring subsystem. Likewise, if formerly registered exams are unmarked, then those - registred exams are marked as inactive + registered exams are marked as inactive """ if not settings.FEATURES.get('ENABLE_PROCTORED_EXAMS'): @@ -76,6 +76,7 @@ def register_proctored_exams(course_key): exam_name=timed_exam.display_name, time_limit_mins=timed_exam.default_time_limit_minutes, is_proctored=timed_exam.is_proctored_enabled, + is_practice_exam=timed_exam.is_practice_exam, is_active=True ) msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id']) @@ -87,6 +88,7 @@ def register_proctored_exams(course_key): exam_name=timed_exam.display_name, time_limit_mins=timed_exam.default_time_limit_minutes, is_proctored=timed_exam.is_proctored_enabled, + is_practice_exam=timed_exam.is_practice_exam, is_active=True ) msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 4029951784..04bf559e65 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -862,7 +862,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F xblock_info.update({ "is_proctored_enabled": xblock.is_proctored_enabled, "is_time_limited": xblock.is_time_limited, - "default_time_limit_minutes": xblock.default_time_limit_minutes + "default_time_limit_minutes": xblock.default_time_limit_minutes, + "is_practice_exam": xblock.is_practice_exam }) # Entrance exam subsection should be hidden. in_entrance_exam is inherited metadata, all children will have it. diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 4c239d521f..fdf9681363 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -352,7 +352,6 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) - ################# PROCTORING CONFIGURATION ################## PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER) diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json index d4a648394e..981d4deca9 100644 --- a/cms/envs/bok_choy.env.json +++ b/cms/envs/bok_choy.env.json @@ -77,7 +77,8 @@ "SUBDOMAIN_BRANDING": false, "SUBDOMAIN_COURSE_LISTINGS": false, "ALLOW_ALL_ADVANCED_COMPONENTS": true, - "ENABLE_CONTENT_LIBRARIES": true + "ENABLE_CONTENT_LIBRARIES": true, + "ENABLE_PROCTORED_EXAMS": true }, "FEEDBACK_SUBMISSION_EMAIL": "", "GITHUB_REPO_ROOT": "** OVERRIDDEN **", diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 90187a7a44..83a8fdc570 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -101,6 +101,8 @@ FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True +FEATURES['ENABLE_PROCTORED_EXAMS'] = True + # Point the URL used to test YouTube availability to our stub YouTube server YOUTUBE_PORT = 9080 YOUTUBE['API'] = "http://127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) diff --git a/cms/envs/common.py b/cms/envs/common.py index 3214e7231a..44be9626ad 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -173,6 +173,9 @@ FEATURES = { # Show video bumper in Studio 'ENABLE_VIDEO_BUMPER': False, + # Timed Proctored Exams + 'ENABLE_PROCTORED_EXAMS': False, + # How many seconds to show the bumper again, default is 7 days: 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, @@ -775,6 +778,9 @@ INSTALLED_APPS = ( 'openedx.core.djangoapps.credit', 'xblock_django', + + # edX Proctoring + 'edx_proctoring', ) @@ -1040,11 +1046,10 @@ CREDIT_PROVIDER_TIMESTAMP_EXPIRATION = 15 * 60 DEPRECATED_BLOCK_TYPES = ['peergrading', 'combinedopenended'] - #### PROCTORING CONFIGURATION DEFAULTS PROCTORING_BACKEND_PROVIDER = { - 'class': 'edx_proctoring.backends.NullBackendProvider', + 'class': 'edx_proctoring.backends.null.NullBackendProvider', 'options': {}, } PROCTORING_SETTINGS = {} diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index 0e9104d9d2..1bbaaf228b 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -620,6 +620,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut has_explicit_staff_lock: true, staff_only_message: true, "is_time_limited": true, + "is_practice_exam": false, "is_proctored_enabled": true, "default_time_limit_minutes": 150 }, [ @@ -706,6 +707,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut "start":"2014-07-09T00:00:00.000Z", "due":"2014-07-10T00:00:00.000Z", "is_time_limited": true, + "is_practice_exam": false, "is_proctored_enabled": true, "default_time_limit_minutes": 150 } @@ -740,6 +742,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_ut expect($("#staff_lock").is(":checked")).toBe(true); expect($("#id_timed_examination").is(":checked")).toBe(true); expect($("#id_exam_proctoring").is(":checked")).toBe(true); + expect($("#is_practice_exam").is(":checked")).toBe(false); expect($("#id_time_limit").val()).toBe("02:30"); }); diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js index 9758fd7f0d..2fa152dd7a 100644 --- a/cms/static/js/views/modals/course_outline_modals.js +++ b/cms/static/js/views/modals/course_outline_modals.js @@ -275,11 +275,17 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', event.preventDefault(); if (!$(event.currentTarget).is(':checked')) { this.$('#id_exam_proctoring').attr('checked', false); - this.$('#id_time_limit').val('00:30'); + this.$('#id_time_limit').val('00:00'); this.$('#id_exam_proctoring').attr('disabled','disabled'); this.$('#id_time_limit').attr('disabled', 'disabled'); + this.$('#id_practice_exam').attr('checked', false); + this.$('#id_practice_exam').attr('disabled','disabled'); } else { + if (!this.isValidTimeLimit(this.$('#id_time_limit').val())) { + this.$('#id_time_limit').val('00:30'); + } + this.$('#id_practice_exam').removeAttr('disabled'); this.$('#id_exam_proctoring').removeAttr('disabled'); this.$('#id_time_limit').removeAttr('disabled'); } @@ -289,11 +295,17 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', AbstractEditor.prototype.afterRender.call(this); this.$('input.time').timepicker({ 'timeFormat' : 'H:i', + 'minTime': '00:30', + 'maxTime': '05:00', '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')); + this.setPracticeExam(this.model.get('is_practice_exam')); + }, + setPracticeExam: function(value) { + this.$('#id_practice_exam').prop('checked', value); }, setExamProctoring: function(value) { this.$('#id_exam_proctoring').prop('checked', value); @@ -307,14 +319,18 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', if (!this.$('#id_timed_examination').is(':checked')) { this.$('#id_exam_proctoring').attr('disabled','disabled'); this.$('#id_time_limit').attr('disabled', 'disabled'); + this.$('#id_practice_exam').attr('disabled', 'disabled'); } }, isExamTimeEnabled: function () { return this.$('#id_timed_examination').is(':checked'); }, + isPracticeExam: function () { + return this.$('#id_practice_exam').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); + return pattern.test(time_limit) && time_limit !== "00:00"; }, getExamTimeLimit: function () { return this.$('#id_time_limit').val(); @@ -338,6 +354,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', var time_limit = this.getExamTimeLimit(); return { metadata: { + 'is_practice_exam': this.isPracticeExam(), 'is_time_limited': this.isExamTimeEnabled(), 'is_proctored_enabled': this.isExamProctoringEnabled(), 'default_time_limit_minutes': this.convertTimeLimitToMinutes(time_limit) diff --git a/cms/templates/js/timed-examination-preference-editor.underscore b/cms/templates/js/timed-examination-preference-editor.underscore index 027156cb0f..cba0399b8c 100644 --- a/cms/templates/js/timed-examination-preference-editor.underscore +++ b/cms/templates/js/timed-examination-preference-editor.underscore @@ -24,6 +24,17 @@

<%- gettext('Students see warnings when 20% and 5% of the allotted time remains. In certain cases, students can be granted allowances that give them extra time to complete the exam.') %>

+