Commit 7ee41eaf by Andy Armstrong

Refactor staff preview menu logic out of template

parent 9cb660a7
**/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
......@@ -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.
......
......@@ -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}
]
};
......
......@@ -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},
......
<nav class="wrapper-preview-menu" aria-label="Course View">
<div class="preview-menu">
<ol class="preview-actions">
<li class="action-preview">
<form action="#" class="action-preview-form" method="post">
<label for="action-preview-select" class="action-preview-label">View this course as:</label>
<select class="action-preview-select" id="action-preview-select" name="select">
<option value="staff" selected>Staff</option>
<option value="student">Student</option>
<option value="specific student">Specific student</option>
<option value="group-a" data-group-id="group-a">Student in Group A</option>
<option value="group-b" data-group-id="group-b">Student in Group B</option>
</select>
<div class="action-preview-username-container">
<label for="action-preview-username" class="action-preview-label">Username or email:</label>
<input type="text" class="action-preview-username" id="action-preview-username">
</div>
<button type="submit" class="sr" name="submit" value="submit">Set preview mode</button>
</form>
</li>
</ol>
</div>
</nav>
(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
};
} ())
}())
;(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);
(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);
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');
});
});
}
);
......@@ -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"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="courseware/${template_name}.underscore" />
</script>
% endfor
<div class="proctored_exam_status"></div>
<%static:js group='proctoring'/>
% for template_name in ["proctored-exam-status"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="courseware/${template_name}.underscore" />
</script>
% endfor
<div class="proctored_exam_status"></div>
% endif
% if show_preview_menu:
<nav class="wrapper-preview-menu" aria-label="${_('Course View')}">
......@@ -65,102 +67,46 @@ include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
</li>
</ol>
% if specific_student_selected:
<div class="preview-specific-student-notice">
<p>
${_("You are now viewing the course as <i>{user_name}</i>.").format(user_name=masquerade_user_name)}
</p>
</div>
<div class="preview-specific-student-notice">
<p>
${Text(_("You are now viewing the course as {i_start}{user_name}{i_end}.")).format(
user_name=masquerade_user_name,
i_start=HTML(u'<i>'),
i_end=HTML(u'</i>'),
)}
</p>
</div>
% endif
</div>
</nav>
% endif
% if disable_tabs is UNDEFINED or not disable_tabs:
<nav class="${active_page} wrapper-course-material" aria-label="${_('Course Material')}">
<div class="course-material">
<%
tab_list = get_course_tab_list(request, course)
tabs_tmpl = static.get_template_path('/courseware/tabs.html')
%>
<ol class="course-tabs">
<%include file="${tabs_tmpl}" args="tab_list=tab_list,active_page=active_page,default_tab=default_tab,tab_image=tab_image" />
<%block name="extratabs" />
</ol>
</div>
</nav>
<nav class="${active_page} wrapper-course-material" aria-label="${_('Course Material')}">
<div class="course-material">
<%
tab_list = get_course_tab_list(request, course)
tabs_tmpl = static.get_template_path('/courseware/tabs.html')
%>
<ol class="course-tabs">
<%include file="${tabs_tmpl}" args="tab_list=tab_list,active_page=active_page,default_tab=default_tab,tab_image=tab_image" />
<%block name="extratabs" />
</ol>
</div>
</nav>
%endif
% if show_preview_menu:
<script type="text/javascript">
(function() {
var selectElement = $('.action-preview-select');
var userNameElement = $('#action-preview-username');
var userNameContainer = $('.action-preview-username-container')
% if disable_student_access:
selectElement.attr("disabled", true);
selectElement.attr("title", "${_("Course is not yet visible to students.")}");
% endif
% if specific_student_selected:
userNameContainer.css('display', 'inline-block');
userNameElement.val('${masquerade_user_name}');
% endif
selectElement.change(function() {
var selectedOption;
if (selectElement.attr("disabled")) {
return alert("${_("You cannot view the course as a student or beta tester before the course release date.")}");
}
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: ${cohorted_user_partition.id if cohorted_user_partition else 'null'},
group_id: selectedOption.data('group-id'),
user_name: selectedOption.val() === 'specific student' ? userNameElement.val() : null
};
$.ajax({
url: '/courses/${course.id}/masquerade',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(data),
success: function(result) {
if (result.success) {
location.reload();
} else {
alert(result.error);
}
},
error: function() {
alert('Error: cannot connect to server');
}
});
<%
preview_options = {
"courseId": course.id,
"disableStudentAccess": disable_student_access if disable_student_access is not UNDEFINED else False,
"specificStudentSelected": specific_student_selected,
"cohortedUserPartitionId": cohorted_user_partition.id if cohorted_user_partition else None,
"masqueradeUsername" : masquerade_user_name if masquerade_user_name is not UNDEFINED else None,
}
}());
</script>
%>
<%static:require_module module_name="lms/js/preview/preview_factory" class_name="PreviewFactory">
PreviewFactory(${preview_options | n, dump_js_escaped_json});
</%static:require_module>
% endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment