Commit 2120af6a by Dennis Jen

WIP: adding filters to course list

* Refactored filter into checkbox and drop down filters
parent d531db73
<% if (!_.isEmpty(filterValues)) { %>
<% var filterId = 'filter-' + filterKey; %>
<hr>
<div id="<%- filterId %>">
<% _.each(filterValues, function (filterValue) { %>
<% if (filterValue.name === 'inactive') { %>
<input id="<%- filterValue.name %>" type="checkbox" value="<%- filterValue.name %>" <% if (isChecked) { %> checked <% } %>>
<label for="<%- filterValue.name %>"> <%- selectDisplayName %> </label><br>
<% } %>
<% }); %>
</div>
<% } %>
<% if (!_.isEmpty(filterValues)) { %>
<% var filterId = 'filter-' + filterKey; %>
<hr>
<% if (!(filterKey === 'ignore_segments')) { %>
<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>
<% } %>
<% if (filterKey === 'ignore_segments') { %>
<div id="<%- filterId %>">
<% _.each(filterValues, function (filterValue) { %>
<% if (filterValue.name === 'inactive') { %>
<input id="<%- filterValue.name %>" type="checkbox" value="<%- filterValue.name %>" <% if (hideInactive) { %> checked <% } %>>
<label for="<%- filterValue.name %>"> <%- selectDisplayName %> </label><br>
<% } %>
<% }); %>
</div>
<% } %>
<% 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>
<% } %>
/**
/**
* Extends the filter view for checkbox filters.
*/
define(function(require) {
'use strict';
var $ = require('jquery'),
_ = require('underscore'),
filterTemplate = require('text!components/filter/templates/checkbox-filter.underscore'),
Filter = require('components/filter/views/filter');
return Filter.extend({
template: _.template(filterTemplate),
/**
* Returns the default template options along with the checkbox state.
*/
templateHelpers: function() {
var templateOptions = Filter.prototype.templateHelpers.call(this);
if (this.options.filterKey in this.options.collection.getActiveFilterFields()) {
templateOptions.isChecked = true;
} else {
templateOptions.isChecked = false;
}
return templateOptions;
},
onFilter: function(event) {
var $inputs = $(event.currentTarget).find('input:checkbox:checked'),
filterKey = $(event.currentTarget).attr('id').slice(7), // chop off "filter-" prefix
appliedFilters = [],
filterValue = '';
if ($inputs.length) {
_.each($inputs, _.bind(function(input) {
appliedFilters.push($(input).attr('id'));
}, this));
filterValue = appliedFilters.join(',');
this.collection.setFilterField(filterKey, filterValue);
} else {
this.collection.unsetFilterField(filterKey);
}
this.filterUpdated(filterValue);
}
});
});
/**
* Extends the filter view for drop down filters.
*/
define(function(require) {
'use strict';
var $ = require('jquery'),
_ = require('underscore'),
filterTemplate = require('text!components/filter/templates/drop-down-filter.underscore'),
Filter = require('components/filter/views/filter');
return Filter.extend({
template: _.template(filterTemplate),
/**
* Returns the default template options along with an "All" option and the
* current selection state.
*/
templateHelpers: function() {
var templateOptions = Filter.prototype.templateHelpers.call(this),
filterValues = templateOptions.filterValues,
selectedFilterValue;
// update filter values with an "All" option and current selection
if (filterValues.length > 0) {
filterValues.unshift({
name: this.catchAllFilterValue,
// Translators: "All" refers to viewing all items (e.g. all courses).
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() || this.catchAllFilterValue;
_.findWhere(filterValues, {name: selectedFilterValue}).selected = true;
}
return templateOptions;
},
onFilter: function(event) {
var selectedOption = $(event.currentTarget).find('option:selected'),
selectedFilterValue = selectedOption.val(),
filterWasUnset = selectedFilterValue === this.catchAllFilterValue;
event.preventDefault();
if (filterWasUnset) {
this.collection.unsetFilterField(this.options.filterKey);
} else {
this.collection.setFilterField(this.options.filterKey, selectedFilterValue);
}
this.filterUpdated(this.options.filterKey);
}
});
return Filter;
});
/**
* A view which renders a select box in order to filter a Learners Collection.
* A view which renders a select box in order to filter a ListCollection.
*
* It takes a collection, a display name, a filter field, and a set of possible
* filter values.
......@@ -12,7 +12,6 @@ define(function(require) {
Marionette = require('marionette'),
Utils = require('utils/utils'),
filterTemplate = require('text!learners/roster/templates/filter.underscore'),
Filter;
......@@ -27,34 +26,44 @@ define(function(require) {
// 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
// learners, unfiltered".
// items, unfiltered".
catchAllFilterValue: '',
className: 'learners-filter',
className: function() {
return this.appClass + '-filter';
},
template: _.template(filterTemplate),
/**
* Updates the filter set on the collection and calls filterUpdated().
*/
onFilter: function() {
throw 'onFilter must be implemented'
},
/**
* Initialize a filter.
*
* @param options an options object which must include the following
* key/values:
* - collection (LearnersCollection): the learners collection to
* - collection (ListCollection): the collection to
* filter.
* - filterKey (string): the field to be filtered by on the learner
* collection.
* - filterKey (string): the field to be filtered by on the 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.
* filter values to the number of items matching the applied filter.
* - selectDisplayName (string): a *translated* string that will
* appear as the label for this filter.
* - trackingModel (Object): tracking model for broadcasting filter
* events.
*/
initialize: function(options) {
this.options = options || {};
_.bind(this.onSelectFilter, this);
this.options = _.defaults({}, options, {
trackSubject: 'list',
appClass: ''
});
this.options = _.defaults(this.options, {
trackFilterEventName: ['edx', 'bi', this.options.trackSubject, 'filtered'].join('.')
});
this.listenTo(this.options.collection, 'sync', this.render);
},
......@@ -65,108 +74,46 @@ define(function(require) {
// 'displayName' is the user-facing representation of the filter
// which combines the filter with the number of users belonging to
// it.
var hideInactive = false,
filterValues,
selectedFilterValue;
var filterValues;
filterValues = _.chain(this.options.filterValues)
.pairs()
.map(function(filterPair) {
var name = filterPair[0],
numLearners = filterPair[1];
count = filterPair[1];
return {
name: name,
displayName: _.template(
// eslint-disable-next-line max-len
// 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 %>)')
// Translators: 'name' here refers to the name of the filter, while 'count' refers to the number of items belonging to that filter.
gettext('<%= name %> (<%= count %>)')
)({
name: name,
numLearners: Utils.localizeNumber(numLearners, 0)
count: Utils.localizeNumber(count, 0)
})
};
})
.sortBy('name')
.value();
if (filterValues.length && this.options.filterInput === 'select') {
filterValues.unshift({
name: this.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() || this.catchAllFilterValue;
_.findWhere(filterValues, {name: selectedFilterValue}).selected = true;
}
if (this.options.filterKey === 'ignore_segments') {
// Translators: inactive meaning that these learners have not interacted with the course recently.
this.options.selectDisplayName = gettext('Hide Inactive Learners');
}
if ('ignore_segments' in this.options.collection.getActiveFilterFields()) {
hideInactive = true;
}
return {
filterKey: this.options.filterKey,
filterValues: filterValues,
hideInactive: hideInactive,
selectDisplayName: this.options.selectDisplayName
};
},
onCheckboxFilter: function(event) {
var $inputs = $(event.currentTarget).find('input:checkbox:checked'),
filterKey = $(event.currentTarget).attr('id').slice(7), // chop off "filter-" prefix
appliedFilters = [],
filterValue = '';
if ($inputs.length) {
_.each($inputs, _.bind(function(input) {
appliedFilters.push($(input).attr('id'));
}, this));
filterValue = appliedFilters.join(',');
this.collection.setFilterField(filterKey, filterValue);
} else {
this.collection.unsetFilterField(filterKey);
}
/**
* Refreshes collection, sets focus, and triggers tracking event.
*/
filterUpdated: function(filterValue) {
this.collection.refresh();
$('#learner-app-focusable').focus();
this.options.trackingModel.trigger('segment:track', 'edx.bi.roster.filtered', {
$('#' + this.appClass + '-focusable').focus();
this.options.trackingModel.trigger('segment:track', this.options.trackFilterEventName, {
category: filterValue
});
},
onSelectFilter: function(event) {
// Sends a request to the server for the filtered learner list.
var selectedOption = $(event.currentTarget).find('option:selected'),
selectedFilterValue = selectedOption.val(),
filterWasUnset = selectedFilterValue === this.catchAllFilterValue;
event.preventDefault();
if (this.options.filterKey === 'segments') {
selectedFilterValue = 'inactive';
}
if (filterWasUnset) {
this.collection.unsetFilterField(this.options.filterKey);
} else {
this.collection.setFilterField(this.options.filterKey, selectedFilterValue);
}
this.collection.refresh();
$('#learner-app-focusable').focus();
this.options.trackingModel.trigger('segment:track', 'edx.bi.roster.filtered', {
category: this.options.filterKey
});
},
onFilter: function(event) {
if ($(event.currentTarget).find('option').length) {
this.onSelectFilter(event);
} else if ($(event.currentTarget).find('input:checkbox').length) {
this.onCheckboxFilter(event);
}
}
});
return Filter;
......
......@@ -51,7 +51,9 @@ define(function(require) {
class: CourseListControlsView,
options: {
collection: this.options.collection,
trackingModel: this.options.trackingModel
trackingModel: this.options.trackingModel,
trackSubject: this.options.trackSubject,
appClass: this.options.appClass
}
},
{
......
......@@ -7,7 +7,8 @@ define(function(require) {
var _ = require('underscore'),
ParentView = require('components/generic-list/common/views/parent-view'),
LearnerFilter = require('learners/roster/views/filter'),
CheckboxFilter = require('components/filter/views/checkbox-filter'),
DropDownFilter = require('components/filter/views/drop-down-filter'),
LearnerSearch = require('learners/roster/views/search'),
rosterControlsTemplate = require('text!learners/roster/templates/controls.underscore'),
......@@ -24,8 +25,16 @@ define(function(require) {
},
initialize: function(options) {
var defaultFilterOptions;
this.options = options || {};
defaultFilterOptions = {
collection: this.options.collection,
trackingModel: this.options.trackingModel,
trackSubject: this.options.trackSubject,
appClass: this.options.appClass
};
this.childViews = [
{
region: 'search',
......@@ -39,41 +48,32 @@ define(function(require) {
},
{
region: 'cohortFilter',
class: LearnerFilter,
options: {
collection: this.options.collection,
class: DropDownFilter,
options: _({
filterKey: 'cohort',
filterValues: this.options.courseMetadata.get('cohorts'),
filterInput: 'select',
selectDisplayName: gettext('Cohort Groups'),
trackingModel: this.options.trackingModel
}
}).extend(defaultFilterOptions)
},
{
region: 'enrollmentTrackFilter',
class: LearnerFilter,
options: {
collection: this.options.collection,
class: DropDownFilter,
options: _({
filterKey: 'enrollment_mode',
filterValues: this.options.courseMetadata.get('enrollment_modes'),
filterInput: 'select',
selectDisplayName: gettext('Enrollment Tracks'),
trackingModel: this.options.trackingModel
}
selectDisplayName: gettext('Enrollment Tracks')
}).extend(defaultFilterOptions)
},
{
region: 'activeFilter',
class: LearnerFilter,
options: {
collection: this.options.collection,
class: CheckboxFilter,
options: _({
filterKey: 'ignore_segments',
filterValues: this.options.courseMetadata.get('segments'),
filterInput: 'checkbox',
// Translators: inactive meaning that these learners have not interacted with the course
// recently.
selectDisplayName: gettext('Hide Inactive Learners'),
trackingModel: this.options.trackingModel
}
}).extend(defaultFilterOptions)
}
];
}
......
......@@ -66,7 +66,9 @@ define(function(require) {
options: {
collection: this.options.collection,
courseMetadata: this.options.courseMetadata,
trackingModel: this.options.trackingModel
trackingModel: this.options.trackingModel,
trackSubject: this.options.trackSubject,
appClass: this.options.appClass
}
},
{
......
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