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) {
LearnersRootView = Marionette.LayoutView.extend({
template: _.template(rootTemplate),
regions: {
error: '.learners-error-region',
header: '.learners-header-region',
main: '.learners-main-region'
},
childEvents: {
appError: 'onAppError',
clearError: 'onClearError'
clearError: 'onClearError',
setFocusToTop: 'onSetFocusToTop'
},
initialize: function (options) {
this.options = options || {};
},
onRender: function () {
this.showChildView('header', new HeaderView({
model: this.options.pageModel
}));
},
onAppError: function (childView, errorMessage) {
this.showChildView('error', new AlertView({
alertType: 'error',
title: errorMessage
}));
},
onClearError: function () {
this.getRegion('error').empty();
},
onSetFocusToTop: function () {
this.$('#learner-app-focusable').focus();
}
});
......
......@@ -40,5 +40,12 @@ define(function (require) {
this.rootView.triggerMethod('clearError', '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) {
'use strict';
var PagingCollection = require('uitk/js/pagination/paging-collection'),
var PagingCollection = require('uitk/pagination/paging-collection'),
LearnerModel = require('learners/common/models/learner'),
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-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="learners-table-controls"></div>
</div>
<div class="col-md-8 col-md-pull-4">
<div class="learners-results"></div>
<div class="row">
<div class="col-md-4 col-md-push-8">
<div class="learners-table-controls"></div>
</div>
<div class="col-md-8 col-md-pull-4">
<div class="learners-results"></div>
</div>
</div>
<div class="section-data-table row">
<div class="section-data-table">
<div class="learners-table table-responsive"></div>
<div class="learners-paging-footer"></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) {
var _ = require('underscore'),
Marionette = require('marionette'),
CohortFilter = require('learners/roster/views/cohort-filter'),
Filter = require('learners/roster/views/filter'),
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 = Marionette.LayoutView.extend({
template: _.template(rosterControlsTemplate),
regions: {
search: '.learners-search-container',
cohortFilter: '.learners-cohort-filter-container'
cohortFilter: '.learners-cohort-filter-container',
enrollmentTrackFilter: '.learners-enrollment-track-filter-container'
},
initialize: function (options) {
this.options = options || {};
},
onBeforeShow: function () {
this.showChildView('search', new LearnerSearch({
collection: this.options.collection,
name: 'text_search',
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,
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) {
onBeforeShow: function () {
var options = this.options;
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,
columns: _.map(this.options.collection.sortableFields, function (val, key) {
var column = {
......
......@@ -37,9 +37,8 @@ require.config({
'cldr-data': 'bower_components/cldr-data',
globalize: 'bower_components/globalize/dist/globalize',
globalization: 'js/utils/globalization',
disclosure: 'bower_components/edx-ui-toolkit/src/js/disclosure/disclosure-view',
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: 'bower_components/uri.js/src/URI',
IPv6: 'bower_components/uri.js/src/IPv6',
......
......@@ -5,58 +5,68 @@
require(['vendor/domReady!', 'load/init-page'], function(doc, page) {
'use strict';
require(['disclosure', 'underscore', 'views/data-table-view', 'views/iframe-view', 'views/stacked-timeline-view'],
function (DisclosureView, _, DataTableView, IFrameView, StackedTimelineView) {
var courseModel = page.models.courseModel,
timelineSettings = [
{
key: 'num_users',
title: gettext('Unique Viewers'),
className: 'text-right',
type: 'number',
color: 'rgb(61,162,229)'
},
{
key: 'num_replays',
title: gettext('Replays'),
className: 'text-right',
type: 'number',
color: 'rgb(18,46,204)'
}
],
tableColumns = [
{key: 'start_time', title: gettext('Time'), type: 'time'}
];
tableColumns = tableColumns.concat(timelineSettings);
new DisclosureView({
el: '.module-preview-disclosure'
});
// loading the iframe blocks content, so load it after the rest of the page loads
new IFrameView({
el: '#module-preview',
loadingSelector: '#module-loading'
});
new StackedTimelineView({
el: '#chart-view',
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
});
}
);
require([
'uitk/disclosure/disclosure-view',
'underscore',
'views/data-table-view',
'views/iframe-view',
'views/stacked-timeline-view'
], function (
DisclosureView,
_,
DataTableView,
IFrameView,
StackedTimelineView
) {
var courseModel = page.models.courseModel,
timelineSettings = [
{
key: 'num_users',
title: gettext('Unique Viewers'),
className: 'text-right',
type: 'number',
color: 'rgb(61,162,229)'
},
{
key: 'num_replays',
title: gettext('Replays'),
className: 'text-right',
type: 'number',
color: 'rgb(18,46,204)'
}
],
tableColumns = [
{key: 'start_time', title: gettext('Time'), type: 'time'}
];
tableColumns = tableColumns.concat(timelineSettings);
new DisclosureView({
el: '.module-preview-disclosure'
});
// loading the iframe blocks content, so load it after the rest of the page loads
new IFrameView({
el: '#module-preview',
loadingSelector: '#module-loading'
});
new StackedTimelineView({
el: '#chart-view',
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