From 7ee41eafb3271af944d6377fb72a172b3d5baa2d Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Fri, 27 May 2016 17:14:07 -0400 Subject: [PATCH] Refactor staff preview menu logic out of template --- .jshintignore | 4 +- .jshintrc | 2 +- lms/static/karma_lms.conf.js | 5 +- lms/static/karma_lms_coffee.conf.js | 2 +- .../lms/fixtures/preview/course_preview.html | 23 +++ lms/static/lms/js/build.js | 25 ++-- lms/static/lms/js/preview/preview_factory.js | 78 ++++++++++ lms/static/{ => lms}/js/spec/main.js | 31 ++-- .../js/spec/main_requirejs_coffee.js | 0 .../js/spec/preview/preview_factory_spec.js | 122 ++++++++++++++++ .../courseware/course_navigation.html | 138 ++++++------------ 11 files changed, 301 insertions(+), 129 deletions(-) create mode 100644 lms/static/lms/fixtures/preview/course_preview.html create mode 100644 lms/static/lms/js/preview/preview_factory.js rename lms/static/{ => lms}/js/spec/main.js (97%) rename lms/static/{ => lms}/js/spec/main_requirejs_coffee.js (100%) create mode 100644 lms/static/lms/js/spec/preview/preview_factory_spec.js diff --git a/.jshintignore b/.jshintignore index a9339cc37a..26e4c15e1e 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,5 +1,7 @@ **/vendor -node_modules cms/static/js/i18n/**/*.js lms/static/js/i18n/**/*.js +lms/static/lms/js/build.js +lms/static/lms/js/spec/main.js +node_modules venv diff --git a/.jshintrc b/.jshintrc index c4f41b728f..42c7dbc103 100644 --- a/.jshintrc +++ b/.jshintrc @@ -16,7 +16,7 @@ "immed" : true, // Prohibits the use of immediate function invocations without wrapping them in parentheses. // "indent" : 4, // Enforces specific tab width for your code. Has no effect when "white" option is not used. "latedef" : "nofunc", // Prohibits the use of a variable before it was defined. Setting this option to "nofunc" will allow function declarations to be ignored. - "newcap" : true, // Requires you to capitalize names of constructor functions. + "newcap" : false, // Requires you to capitalize names of constructor functions. "noarg" : true, // Prohibits the use of arguments.caller and arguments.callee. "noempty" : true, // Warns when you have an empty block in your code. "nonbsp" : true, // Warns about "non-breaking whitespace" characters. diff --git a/lms/static/karma_lms.conf.js b/lms/static/karma_lms.conf.js index c0a17a5305..a4f5f3c29d 100644 --- a/lms/static/karma_lms.conf.js +++ b/lms/static/karma_lms.conf.js @@ -29,12 +29,14 @@ var options = { sourceFiles: [ {pattern: 'coffee/src/**/!(*spec).js'}, {pattern: 'js/**/!(*spec|djangojs).js'}, + {pattern: 'lms/js/**/!(*spec).js'}, {pattern: 'support/js/**/!(*spec).js'}, {pattern: 'teams/js/**/!(*spec).js'} ], specFiles: [ {pattern: 'js/spec/**/*spec.js'}, + {pattern: 'lms/js/spec/**/*spec.js'}, {pattern: 'support/js/spec/**/*spec.js'}, {pattern: 'teams/js/spec/**/*spec.js'}, {pattern: 'xmodule_js/common_static/coffee/spec/**/*.js'} @@ -42,13 +44,14 @@ var options = { fixtureFiles: [ {pattern: 'js/fixtures/**/*.html'}, + {pattern: 'lms/fixtures/**/*.html'}, {pattern: 'support/templates/**/*.*'}, {pattern: 'teams/templates/**/*.*'}, {pattern: 'templates/**/*.*'} ], runFiles: [ - {pattern: 'js/spec/main.js', included: true} + {pattern: 'lms/js/spec/main.js', included: true} ] }; diff --git a/lms/static/karma_lms_coffee.conf.js b/lms/static/karma_lms_coffee.conf.js index 6050f154df..6541c2e498 100644 --- a/lms/static/karma_lms_coffee.conf.js +++ b/lms/static/karma_lms_coffee.conf.js @@ -16,7 +16,7 @@ var options = { // Avoid adding files to this list. Use RequireJS. libraryFilesToInclude: [ {pattern: 'xmodule_js/common_static/js/vendor/requirejs/require.js', included: true}, - {pattern: 'js/spec/main_requirejs_coffee.js', included: true}, + {pattern: 'lms/js/spec/main_requirejs_coffee.js', included: true}, {pattern: 'js/RequireJS-namespace-undefine.js', included: true}, {pattern: 'xmodule_js/common_static/coffee/src/ajax_prefix.js', included: true}, diff --git a/lms/static/lms/fixtures/preview/course_preview.html b/lms/static/lms/fixtures/preview/course_preview.html new file mode 100644 index 0000000000..7e005ff244 --- /dev/null +++ b/lms/static/lms/fixtures/preview/course_preview.html @@ -0,0 +1,23 @@ + diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index c75ec18585..7579e00a7e 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -1,9 +1,9 @@ -(function () { +(function() { 'use strict'; - var getModulesList = function (modules) { - return modules.map(function (moduleName) { - return { name: moduleName }; + var getModulesList = function(modules) { + return modules.map(function(moduleName) { + return {name: moduleName}; }); }; @@ -11,19 +11,23 @@ process.env.REQUIRE_BUILD_PROFILE_OPTIMIZE : 'uglify2'; return { - namespace: "RequireJS", + namespace: 'RequireJS', /** * List the modules that will be optimized. All their immediate and deep * dependencies will be included in the module's file when the build is * done. */ modules: getModulesList([ + 'js/api_admin/catalog_preview_factory', + 'js/courseware/courseware_factory', 'js/discovery/discovery_factory', 'js/edxnotes/views/notes_visibility_factory', 'js/edxnotes/views/page_factory', 'js/financial-assistance/financial_assistance_form_factory', 'js/groups/views/cohorts_dashboard_factory', 'js/header_factory', + 'js/learner_dashboard/program_details_factory', + 'js/learner_dashboard/program_list_factory', 'js/search/course/course_search_factory', 'js/search/dashboard/dashboard_search_factory', 'js/student_account/logistration_factory', @@ -31,13 +35,10 @@ 'js/student_account/views/finish_auth_factory', 'js/student_profile/views/learner_profile_factory', 'js/views/message_banner', - 'teams/js/teams_tab_factory', + 'lms/js/preview/preview_factory', 'support/js/certificates_factory', 'support/js/enrollment_factory', - 'js/courseware/courseware_factory', - 'js/learner_dashboard/program_details_factory', - 'js/learner_dashboard/program_list_factory', - 'js/api_admin/catalog_preview_factory' + 'teams/js/teams_tab_factory' ]), /** @@ -91,7 +92,7 @@ /** * Stub out requireJS text in the optimized file, but leave available for non-optimized development use. */ - stubModules: ["text"], + stubModules: ['text'], /** * If shim config is used in the app during runtime, duplicate the config @@ -161,4 +162,4 @@ */ logLevel: 1 }; -} ()) +}()) diff --git a/lms/static/lms/js/preview/preview_factory.js b/lms/static/lms/js/preview/preview_factory.js new file mode 100644 index 0000000000..9a5b37e43e --- /dev/null +++ b/lms/static/lms/js/preview/preview_factory.js @@ -0,0 +1,78 @@ +;(function(define) { + 'use strict'; + + define(['jquery', 'common/js/components/utils/view_utils'], + function($, ViewUtils) { + return function(options) { + + var $selectElement = $('.action-preview-select'), + $userNameElement = $('.action-preview-username'), + $userNameContainer = $('.action-preview-username-container'); + + if (options.disableStudentAccess) { + $selectElement.attr('disabled', true); + $selectElement.attr('title', gettext('Course is not yet visible to students.')); + } + + if (options.specificStudentSelected) { + $userNameContainer.css('display', 'inline-block'); + $userNameElement.val(options.masqueradeUsername); + } + + $selectElement.change(function() { + var selectedOption; + if ($selectElement.attr('disabled')) { + return alert(gettext('You cannot view the course as a student or beta tester before the course release date.')); // jshint ignore:line + } + selectedOption = $selectElement.find('option:selected'); + if (selectedOption.val() === 'specific student') { + $userNameContainer.css('display', 'inline-block'); + } else { + $userNameContainer.hide(); + masquerade(selectedOption); + } + }); + + $userNameElement.keypress(function(event) { + if (event.keyCode === 13) { + // Avoid submitting the form on enter, since the submit action isn't implemented. + // Instead, blur the element to trigger a change event in case the value was edited, + // which in turn will trigger an AJAX request to update the masquerading data. + $userNameElement.blur(); + return false; + } + return true; + }); + + $userNameElement.change(function() { + masquerade($selectElement.find('option:selected')); + }); + + function masquerade(selectedOption) { + var data = { + role: selectedOption.val() === 'staff' ? 'staff' : 'student', + user_partition_id: options.cohortedUserPartitionId, + group_id: selectedOption.data('group-id'), + user_name: selectedOption.val() === 'specific student' ? $userNameElement.val() : null + }; + $.ajax({ + url: '/courses/' + options.courseId + '/masquerade', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(data), + success: function(result) { + if (result.success) { + ViewUtils.reload(); + } else { + alert(result.error); + } + }, + error: function() { + alert('Error: cannot connect to server'); + } + }); + } + }; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/main.js b/lms/static/lms/js/spec/main.js similarity index 97% rename from lms/static/js/spec/main.js rename to lms/static/lms/js/spec/main.js index c582c9ffe4..62c17fc499 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -1,4 +1,4 @@ -(function(requirejs, define) { +(function(requirejs) { 'use strict'; // TODO: how can we share the vast majority of this config that is in common with CMS? @@ -120,7 +120,7 @@ 'date': { exports: 'Date' }, - "jquery-migrate": ['jquery'], + 'jquery-migrate': ['jquery'], 'jquery.ui': { deps: ['jquery'], exports: 'jQuery.ui' @@ -204,8 +204,8 @@ deps: ['backbone'], exports: 'Backbone.PageableCollection' }, - "backbone-super": { - deps: ["backbone"] + 'backbone-super': { + deps: ['backbone'] }, 'paging-collection': { deps: ['jquery', 'underscore', 'backbone.paginator'] @@ -292,13 +292,13 @@ }, 'coffee/src/instructor_dashboard/util': { exports: 'coffee/src/instructor_dashboard/util', - deps: ['jquery', 'gettext'], + deps: ['jquery', 'underscore', 'slick.core', 'slick.grid'], init: function() { // Set global variables that the util code is expecting to be defined require([ 'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/string-utils' - ], function (HtmlUtils, StringUtils) { + ], function(HtmlUtils, StringUtils) { window.edx = edx || {}; window.edx.HtmlUtils = HtmlUtils; window.edx.StringUtils = StringUtils; @@ -309,10 +309,6 @@ exports: 'coffee/src/instructor_dashboard/student_admin', deps: ['jquery', 'underscore', 'coffee/src/instructor_dashboard/util', 'string_utils'] }, - 'coffee/src/instructor_dashboard/util': { - exports: 'coffee/src/instructor_dashboard/util', - deps: ['jquery', 'underscore', 'slick.core', 'slick.grid'] - }, 'js/instructor_dashboard/certificates': { exports: 'js/instructor_dashboard/certificates', deps: ['jquery', 'gettext', 'underscore'] @@ -369,11 +365,11 @@ }, 'js/verify_student/models/verification_model': { exports: 'edx.verify_student.VerificationModel', - deps: [ 'jquery', 'underscore', 'backbone', 'jquery.cookie' ] + deps: ['jquery', 'underscore', 'backbone', 'jquery.cookie'] }, 'js/verify_student/views/error_view': { exports: 'edx.verify_student.ErrorView', - deps: [ 'jquery', 'underscore', 'backbone' ] + deps: ['jquery', 'underscore', 'backbone'] }, 'js/verify_student/views/webcam_photo_view': { exports: 'edx.verify_student.WebcamPhotoView', @@ -387,11 +383,11 @@ }, 'js/verify_student/views/image_input_view': { exports: 'edx.verify_student.ImageInputView', - deps: [ 'jquery', 'underscore', 'backbone', 'gettext' ] + deps: ['jquery', 'underscore', 'backbone', 'gettext'] }, 'js/verify_student/views/step_view': { exports: 'edx.verify_student.StepView', - deps: [ 'jquery', 'underscore', 'underscore.string', 'backbone', 'gettext' ], + deps: ['jquery', 'underscore', 'underscore.string', 'backbone', 'gettext'], init: function() { // Set global variables that the payment code is expecting to be defined require([ @@ -538,7 +534,7 @@ exports: 'DiscussionUtil', init: function() { // Set global variables that the discussion code is expecting to be defined - require(['backbone', 'URI'], function (Backbone, URI) { + require(['backbone', 'URI'], function(Backbone, URI) { window.Backbone = Backbone; window.URI = URI; }); @@ -686,6 +682,7 @@ }); var testFiles = [ + 'lms/js/spec/preview/preview_factory_spec.js', 'js/spec/api_admin/catalog_preview_spec.js', 'js/spec/courseware/bookmark_button_view_spec.js', 'js/spec/courseware/bookmarks_list_view_spec.js', @@ -821,8 +818,8 @@ // Jasmine has a global stack for creating a tree of specs. We need to load // spec files one by one, otherwise some end up getting nested under others. - window.requireSerial(specHelpers.concat(testFiles), function () { + window.requireSerial(specHelpers.concat(testFiles), function() { // start test run, once Require.js is done window.__karma__.start(); }); -}).call(this, requirejs, define); +}).call(this, requirejs); diff --git a/lms/static/js/spec/main_requirejs_coffee.js b/lms/static/lms/js/spec/main_requirejs_coffee.js similarity index 100% rename from lms/static/js/spec/main_requirejs_coffee.js rename to lms/static/lms/js/spec/main_requirejs_coffee.js diff --git a/lms/static/lms/js/spec/preview/preview_factory_spec.js b/lms/static/lms/js/spec/preview/preview_factory_spec.js new file mode 100644 index 0000000000..43f1dfd385 --- /dev/null +++ b/lms/static/lms/js/spec/preview/preview_factory_spec.js @@ -0,0 +1,122 @@ +define( + [ + 'common/js/spec_helpers/ajax_helpers', + 'common/js/components/utils/view_utils', + 'lms/js/preview/preview_factory' + ], + function(AjaxHelpers, ViewUtils, PreviewFactory) { + 'use strict'; + + describe('Preview Factory', function() { + var showPreview, + previewActionSelect, + usernameInput; + + showPreview = function(options) { + PreviewFactory(options); + }; + + previewActionSelect = function() { + return $('.action-preview-select'); + }; + + usernameInput = function() { + return $('.action-preview-username'); + }; + + beforeEach(function() { + loadFixtures('lms/fixtures/preview/course_preview.html'); + }); + + it('can render preview for a staff user', function() { + showPreview({ + courseId: 'test_course' + }); + expect(previewActionSelect().val()).toBe('staff'); + }); + + it('can disable course access for a student', function() { + var select; + showPreview({ + courseId: 'test_course', + disableStudentAccess: true + }); + select = previewActionSelect(); + expect(select.attr('disabled')).toBe('disabled'); + expect(select.attr('title')).toBe('Course is not yet visible to students.'); + }); + + it('can switch to view as a student', function() { + var requests = AjaxHelpers.requests(this), + reloadSpy = spyOn(ViewUtils, 'reload'); + showPreview({ + courseId: 'test_course' + }); + previewActionSelect().find('option[value="student"]').prop('selected', 'selected').change(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', '/courses/test_course/masquerade', + { + role: 'student', + user_name: null + } + ); + AjaxHelpers.respondWithJson(requests, { + success: true + }); + expect(reloadSpy).toHaveBeenCalled(); + }); + + it('can switch to view as a content group', function() { + var requests = AjaxHelpers.requests(this), + reloadSpy = spyOn(ViewUtils, 'reload'); + showPreview({ + cohortedUserPartitionId: 'test_partition_id', + courseId: 'test_course' + }); + previewActionSelect().find('option[value="group-b"]').prop('selected', 'selected').change(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', '/courses/test_course/masquerade', + { + role: 'student', + user_name: null, + user_partition_id: 'test_partition_id', + group_id: 'group-b' + } + ); + AjaxHelpers.respondWithJson(requests, { + success: true + }); + expect(reloadSpy).toHaveBeenCalled(); + }); + + it('can switch to masquerade as a specific student', function() { + var requests = AjaxHelpers.requests(this), + reloadSpy = spyOn(ViewUtils, 'reload'); + showPreview({ + courseId: 'test_course' + }); + previewActionSelect().find('option[value="specific student"]').prop('selected', 'selected').change(); + usernameInput().val('test_user').change(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', '/courses/test_course/masquerade', + { + role: 'student', + user_name: 'test_user' + } + ); + AjaxHelpers.respondWithJson(requests, { + success: true + }); + expect(reloadSpy).toHaveBeenCalled(); + }); + + it('shows the correct information when masquerading as a specific student', function() { + showPreview({ + specificStudentSelected: true, + masqueradeUsername: 'test_user' + }); + expect(usernameInput().val()).toBe('test_user'); + }); + }); + } +); diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 22526b5ccd..9b3a5afae3 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -6,9 +6,11 @@ from courseware.tabs import get_course_tab_list from django.core.urlresolvers import reverse from django.conf import settings from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition +from openedx.core.djangolib.js_utils import dump_js_escaped_json +from openedx.core.djangolib.markup import HTML, Text from student.models import CourseEnrollment %> -<%page args="active_page=None" /> +<%page args="active_page=None" expression_filter="h" /> <% if active_page is None and active_page_context is not UNDEFINED: @@ -29,13 +31,13 @@ include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and %> % if include_special_exams: - <%static:js group='proctoring'/> - % for template_name in ["proctored-exam-status"]: - - % endfor -
+ <%static:js group='proctoring'/> + % for template_name in ["proctored-exam-status"]: + + % endfor +
% endif % if show_preview_menu: % endif % if disable_tabs is UNDEFINED or not disable_tabs: - + %endif % if show_preview_menu: - + %> + <%static:require_module module_name="lms/js/preview/preview_factory" class_name="PreviewFactory"> + PreviewFactory(${preview_options | n, dump_js_escaped_json}); + % endif