Commit 3781546a by Daniel Friedman

Merge pull request #443 from edx/dan-f/la-enrollment-track-filter

LA: Add enrollment track filter
parents 9c186cd3 afe731cd
...@@ -15,31 +15,42 @@ define(function (require) { ...@@ -15,31 +15,42 @@ define(function (require) {
LearnersRootView = Marionette.LayoutView.extend({ LearnersRootView = Marionette.LayoutView.extend({
template: _.template(rootTemplate), template: _.template(rootTemplate),
regions: { regions: {
error: '.learners-error-region', error: '.learners-error-region',
header: '.learners-header-region', header: '.learners-header-region',
main: '.learners-main-region' main: '.learners-main-region'
}, },
childEvents: { childEvents: {
appError: 'onAppError', appError: 'onAppError',
clearError: 'onClearError' clearError: 'onClearError',
setFocusToTop: 'onSetFocusToTop'
}, },
initialize: function (options) { initialize: function (options) {
this.options = options || {}; this.options = options || {};
}, },
onRender: function () { onRender: function () {
this.showChildView('header', new HeaderView({ this.showChildView('header', new HeaderView({
model: this.options.pageModel model: this.options.pageModel
})); }));
}, },
onAppError: function (childView, errorMessage) { onAppError: function (childView, errorMessage) {
this.showChildView('error', new AlertView({ this.showChildView('error', new AlertView({
alertType: 'error', alertType: 'error',
title: errorMessage title: errorMessage
})); }));
}, },
onClearError: function () { onClearError: function () {
this.getRegion('error').empty(); this.getRegion('error').empty();
},
onSetFocusToTop: function () {
this.$('#learner-app-focusable').focus();
} }
}); });
......
...@@ -40,5 +40,12 @@ define(function (require) { ...@@ -40,5 +40,12 @@ define(function (require) {
this.rootView.triggerMethod('clearError', 'This is the error copy'); this.rootView.triggerMethod('clearError', 'This is the error copy');
expect(this.rootView.$('.learners-error-region')).not.toHaveText('This is the error copy'); expect(this.rootView.$('.learners-error-region')).not.toHaveText('This is the error copy');
}); });
it('sets focus on setFocusToTop events', function () {
var childView = new Marionette.View();
this.rootView.showChildView('main', childView);
childView.triggerMethod('setFocusToTop');
expect(this.rootView.$('#learner-app-focusable')).toBeFocused();
});
}); });
}); });
define(function (require) { define(function (require) {
'use strict'; 'use strict';
var PagingCollection = require('uitk/js/pagination/paging-collection'), var PagingCollection = require('uitk/pagination/paging-collection'),
LearnerModel = require('learners/common/models/learner'), LearnerModel = require('learners/common/models/learner'),
LearnerUtils = require('learners/common/utils'), LearnerUtils = require('learners/common/utils'),
......
<% if (!_.isEmpty(cohorts)) { %>
<hr>
<label for="cohort-filter">
<%- selectDisplayName %>
</label>
<select id="cohort-filter" class="form-control">
<% _.each(cohorts, function (cohort) { %>
<option value="<%- cohort.cohortName %>" <% if (cohort.selected) { %> selected <% } %>>
<%- cohort.displayName %>
</option>
<% }); %>
</select>
<% } %>
<div class="learners-search-container"></div> <div class="learners-search-container"></div>
<div class="learners-cohort-filter-container"></div> <div class="learners-cohort-filter-container"></div>
<div class="learners-enrollment-track-filter-container"></div>
<% if (!_.isEmpty(filterValues)) { %>
<% var filterId = 'filter-' + filterKey; %>
<hr>
<label for="<%- filterId %>">
<%- selectDisplayName %>
</label>
<select id="<%- filterId %>" class="form-control">
<% _.each(filterValues, function (filterValue) { %>
<option value="<%- filterValue.name %>" <% if (filterValue.selected) { %> selected <% } %>>
<%- filterValue.displayName %>
</option>
<% }); %>
</select>
<% } %>
<div class="col-md-4 col-md-push-8"> <div class="row">
<div class="learners-table-controls"></div> <div class="col-md-4 col-md-push-8">
</div> <div class="learners-table-controls"></div>
<div class="col-md-8 col-md-pull-4"> </div>
<div class="learners-results"></div> <div class="col-md-8 col-md-pull-4">
<div class="learners-results"></div>
</div>
</div> </div>
<div class="section-data-table row"> <div class="section-data-table">
<div class="learners-table table-responsive"></div> <div class="learners-table table-responsive"></div>
<div class="learners-paging-footer"></div> <div class="learners-paging-footer"></div>
</div> </div>
/**
* A component to filter the roster view by cohort.
*/
define(function (require) {
'use strict';
var $ = require('jquery'),
_ = require('underscore'),
Marionette = require('marionette'),
Utils = require('utils/utils'),
cohortFilterTemplate = require('text!learners/roster/templates/cohort-filter.underscore'),
CohortFilter;
CohortFilter = Marionette.ItemView.extend({
events: {
'change #cohort-filter': 'onSelectCohort'
},
className: 'learners-cohort-filter',
template: _.template(cohortFilterTemplate),
initialize: function (options) {
this.options = options || {};
_.bind(this.onSelectCohort, this);
},
templateHelpers: function () {
// 'cohorts' is an array of objects, each having a 'cohortName' key
// and a 'displayName' key. 'cohortName' is the canonical name for
// the cohort, while 'displayName' is the user-facing representation
// of the cohort.
var catchAllCohortName,
cohorts,
selectedCohort;
cohorts = _.chain(this.options.courseMetadata.get('cohorts'))
.pairs()
.map(function (cohortPair) {
var cohortName = cohortPair[0],
numLearners = cohortPair[1];
return {
cohortName: cohortName,
displayName: _.template(ngettext(
// jshint ignore:start
// Translators: 'cohortName' is the name of the cohort and 'numLearners' is the number of learners in that cohort. The resulting phrase displays a cohort and the number of students belonging to it. For example: "Cohort Awesome (1,234 learners)".
'<%= cohortName %> (<%= numLearners %> learner)',
// Translators: 'cohortName' is the name of the cohort and 'numLearners' is the number of learners in that cohort. The resulting phrase displays a cohort and the number of students belonging to it.
'<%= cohortName %> (<%= numLearners %> learners)',
// jshint ignore:end
numLearners
))({
cohortName: cohortName,
numLearners: Utils.localizeNumber(numLearners, 0)
})
};
})
.value();
if (cohorts.length) {
// There can never be a cohort with no name, due to
// validation in the LMS, therefore it's safe to use the
// empty string as a property in this object. The API
// interprets this as "all students, regardless of
// cohort".
catchAllCohortName = '';
cohorts.unshift({
cohortName: catchAllCohortName,
// Translators: "All" refers to viewing all the learners in a course.
displayName: gettext('All')
});
// Assumes that you can only filter by one cohort at a time.
selectedCohort = _.chain(cohorts)
.pluck('cohortName')
.intersection(this.options.collection.getActiveFilterFields())
.first()
.value() || catchAllCohortName;
_.findWhere(cohorts, {cohortName: selectedCohort}).selected = true;
}
return {
cohorts: cohorts,
// Translators: "Cohort Groups" refers to groups of students within a course.
selectDisplayName: gettext('Cohort Groups')
};
},
onSelectCohort: function (event) {
// Sends a request to the server for the learner list filtered by
// cohort then resets focus.
event.preventDefault();
this.collection.setFilterField('cohort', $(event.currentTarget).find('option:selected').val());
this.collection.refresh();
$('#learner-app-focusable').focus();
}
});
return CohortFilter;
});
...@@ -7,30 +7,42 @@ define(function (require) { ...@@ -7,30 +7,42 @@ define(function (require) {
var _ = require('underscore'), var _ = require('underscore'),
Marionette = require('marionette'), Marionette = require('marionette'),
CohortFilter = require('learners/roster/views/cohort-filter'), Filter = require('learners/roster/views/filter'),
LearnerSearch = require('learners/roster/views/search'), LearnerSearch = require('learners/roster/views/search'),
rosterControlsTemplate = require('text!learners/roster/templates/roster-controls.underscore'), rosterControlsTemplate = require('text!learners/roster/templates/controls.underscore'),
RosterControlsView; RosterControlsView;
RosterControlsView = Marionette.LayoutView.extend({ RosterControlsView = Marionette.LayoutView.extend({
template: _.template(rosterControlsTemplate), template: _.template(rosterControlsTemplate),
regions: { regions: {
search: '.learners-search-container', search: '.learners-search-container',
cohortFilter: '.learners-cohort-filter-container' cohortFilter: '.learners-cohort-filter-container',
enrollmentTrackFilter: '.learners-enrollment-track-filter-container'
}, },
initialize: function (options) { initialize: function (options) {
this.options = options || {}; this.options = options || {};
}, },
onBeforeShow: function () { onBeforeShow: function () {
this.showChildView('search', new LearnerSearch({ this.showChildView('search', new LearnerSearch({
collection: this.options.collection, collection: this.options.collection,
name: 'text_search', name: 'text_search',
placeholder: gettext('Find a learner') placeholder: gettext('Find a learner')
})); }));
this.showChildView('cohortFilter', new CohortFilter({ this.showChildView('cohortFilter', new Filter({
collection: this.options.collection,
filterKey: 'cohort',
filterValues: this.options.courseMetadata.get('cohorts'),
selectDisplayName: gettext('Cohort Groups')
}));
this.showChildView('enrollmentTrackFilter', new Filter({
collection: this.options.collection, collection: this.options.collection,
courseMetadata: this.options.courseMetadata filterKey: 'enrollment_mode',
filterValues: this.options.courseMetadata.get('enrollment_modes'),
selectDisplayName: gettext('Enrollment Tracks')
})); }));
} }
}); });
......
/**
* A view which renders a select box in order to filter a Learners Collection.
*
* It takes a collection, a display name, a filter field, and a set of possible
* filter values.
*/
define(function (require) {
'use strict';
var $ = require('jquery'),
_ = require('underscore'),
Marionette = require('marionette'),
Utils = require('utils/utils'),
filterTemplate = require('text!learners/roster/templates/filter.underscore'),
Filter;
Filter = Marionette.ItemView.extend({
events: function () {
var changeFilterEvent = 'change #filter-' + this.options.filterKey,
eventsHash = {};
eventsHash[changeFilterEvent] = 'onSelectFilter';
return eventsHash;
},
className: 'learners-filter',
template: _.template(filterTemplate),
/**
* Initialize a filter.
*
* @param options an options object which must include the following
* key/values:
* - collection (LearnersCollection): the learners collection to
* filter.
* - filterKey (string): the field to be filtered by on the learner
* collection.
* - filterValues (Object): the set of valid values that the
* filterKey can take on, represented as a mapping from
* filter values to the number of learners matching the applied
* filter.
* - selectDisplayName (string): a *translated* string that will
* appear as the label for this filter.
*/
initialize: function (options) {
this.options = options || {};
_.bind(this.onSelectFilter, this);
},
templateHelpers: function () {
// 'filterValues' is an array of objects, each having a 'name' key
// and a 'displayName' key. 'name' is the name of the filter value
// (e.g. the cohort name when filtering by cohort), while
// 'displayName' is the user-facing representation of the filter
// which combines the filter with the number of users belonging to
// it.
var catchAllFilterValue,
filterValues,
selectedFilterValue;
filterValues = _.chain(this.options.filterValues)
.pairs()
.map(function (filterPair) {
var name = filterPair[0],
numLearners = filterPair[1];
return {
name: name,
displayName: _.template(
// jshint ignore:start
// Translators: 'name' here refers to the name of the filter, while 'numLearners' refers to the number of learners belonging to that filter.
gettext('<%= name %> (<%= numLearners %>)')
// jshint ignore:end
)({
name: name,
numLearners: Utils.localizeNumber(numLearners, 0)
})
};
})
.sortBy('name')
.value();
if (filterValues.length) {
// It is assumed that there can never be a filter with an empty
// name, therefore it's safe to use the empty string as a
// property in this object. The API interprets this as "all
// students, unfiltered".
catchAllFilterValue = '';
filterValues.unshift({
name: catchAllFilterValue,
// Translators: "All" refers to viewing all the learners in a course.
displayName: gettext('All')
});
// Assumes that you can only filter by one filterKey at a time.
selectedFilterValue = _.chain(filterValues)
.pluck('name')
.intersection(this.options.collection.getActiveFilterFields())
.first()
.value() || catchAllFilterValue;
_.findWhere(filterValues, {name: selectedFilterValue}).selected = true;
}
return {
filterKey: this.options.filterKey,
filterValues: filterValues,
selectDisplayName: this.options.selectDisplayName
};
},
onSelectFilter: function (event) {
// Sends a request to the server for the filtered learner list.
event.preventDefault();
this.collection.setFilterField(
this.options.filterKey,
$(event.currentTarget).find('option:selected').val()
);
this.collection.refresh();
this.triggerMethod('setFocusToTop');
}
});
return Filter;
});
...@@ -78,7 +78,7 @@ define(function (require) { ...@@ -78,7 +78,7 @@ define(function (require) {
onBeforeShow: function () { onBeforeShow: function () {
var options = this.options; var options = this.options;
this.showChildView('table', new Backgrid.Grid({ this.showChildView('table', new Backgrid.Grid({
className: 'table table-striped', // Use bootstrap styling className: 'table table-striped dataTable', // Combine bootstrap and datatables styling
collection: this.options.collection, collection: this.options.collection,
columns: _.map(this.options.collection.sortableFields, function (val, key) { columns: _.map(this.options.collection.sortableFields, function (val, key) {
var column = { var column = {
......
...@@ -37,9 +37,8 @@ require.config({ ...@@ -37,9 +37,8 @@ require.config({
'cldr-data': 'bower_components/cldr-data', 'cldr-data': 'bower_components/cldr-data',
globalize: 'bower_components/globalize/dist/globalize', globalize: 'bower_components/globalize/dist/globalize',
globalization: 'js/utils/globalization', globalization: 'js/utils/globalization',
disclosure: 'bower_components/edx-ui-toolkit/src/js/disclosure/disclosure-view',
marionette: 'bower_components/marionette/lib/core/backbone.marionette.min', marionette: 'bower_components/marionette/lib/core/backbone.marionette.min',
uitk: 'bower_components/edx-ui-toolkit/src', uitk: 'bower_components/edx-ui-toolkit/src/js',
// URI and its dependencies // URI and its dependencies
URI: 'bower_components/uri.js/src/URI', URI: 'bower_components/uri.js/src/URI',
IPv6: 'bower_components/uri.js/src/IPv6', IPv6: 'bower_components/uri.js/src/IPv6',
......
...@@ -5,58 +5,68 @@ ...@@ -5,58 +5,68 @@
require(['vendor/domReady!', 'load/init-page'], function(doc, page) { require(['vendor/domReady!', 'load/init-page'], function(doc, page) {
'use strict'; 'use strict';
require(['disclosure', 'underscore', 'views/data-table-view', 'views/iframe-view', 'views/stacked-timeline-view'], require([
function (DisclosureView, _, DataTableView, IFrameView, StackedTimelineView) { 'uitk/disclosure/disclosure-view',
'underscore',
var courseModel = page.models.courseModel, 'views/data-table-view',
timelineSettings = [ 'views/iframe-view',
{ 'views/stacked-timeline-view'
key: 'num_users', ], function (
title: gettext('Unique Viewers'), DisclosureView,
className: 'text-right', _,
type: 'number', DataTableView,
color: 'rgb(61,162,229)' IFrameView,
}, StackedTimelineView
{ ) {
key: 'num_replays',
title: gettext('Replays'), var courseModel = page.models.courseModel,
className: 'text-right', timelineSettings = [
type: 'number', {
color: 'rgb(18,46,204)' key: 'num_users',
} title: gettext('Unique Viewers'),
], className: 'text-right',
tableColumns = [ type: 'number',
{key: 'start_time', title: gettext('Time'), type: 'time'} color: 'rgb(61,162,229)'
]; },
{
tableColumns = tableColumns.concat(timelineSettings); key: 'num_replays',
title: gettext('Replays'),
new DisclosureView({ className: 'text-right',
el: '.module-preview-disclosure' type: 'number',
}); color: 'rgb(18,46,204)'
}
// loading the iframe blocks content, so load it after the rest of the page loads ],
new IFrameView({ tableColumns = [
el: '#module-preview', {key: 'start_time', title: gettext('Time'), type: 'time'}
loadingSelector: '#module-loading' ];
});
tableColumns = tableColumns.concat(timelineSettings);
new StackedTimelineView({
el: '#chart-view', new DisclosureView({
model: courseModel, el: '.module-preview-disclosure'
modelAttribute: 'videoTimeline', });
trends: timelineSettings,
x: { key: 'start_time', title: 'Time' }, // loading the iframe blocks content, so load it after the rest of the page loads
y: { key: 'num_users' } new IFrameView({
}); el: '#module-preview',
loadingSelector: '#module-loading'
new DataTableView({ });
el: '[data-role=data-table]',
model: courseModel, new StackedTimelineView({
modelAttribute: 'videoTimeline', el: '#chart-view',
columns: tableColumns model: courseModel,
}); modelAttribute: 'videoTimeline',
trends: timelineSettings,
} x: { key: 'start_time', title: 'Time' },
); y: { key: 'num_users' }
});
new DataTableView({
el: '[data-role=data-table]',
model: courseModel,
modelAttribute: 'videoTimeline',
columns: tableColumns
});
});
}); });
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