Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Add course listing filters.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsjen committed Feb 21, 2017
1 parent a82d20c commit 1ab4209
Show file tree
Hide file tree
Showing 52 changed files with 958 additions and 170 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:

env:
# Make sure to update this string on every Insights or Data API release
DATA_API_VERSION: "0.20.1-rc.3"
DATA_API_VERSION: "0.21.0"
DOCKER_COMPOSE_VERSION: "1.9.0"

before_install:
Expand Down
1 change: 1 addition & 0 deletions .travis/docker-compose-travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
ENABLE_OAUTH_TESTS: "False"
ENABLE_ERROR_PAGE_TESTS: "False"
DISPLAY_LEARNER_ANALYTICS: "True"
ENABLE_COURSE_LIST_FILTERS: "True"
depends_on:
- "es"
- "analyticsapi"
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ test_python: test_compress test_python_no_compress
accept:
ifeq ("${DISPLAY_LEARNER_ANALYTICS}", "True")
./manage.py waffle_flag enable_learner_analytics on --create --everyone
endif
ifeq ("${ENABLE_COURSE_LIST_FILTERS}", "True")
./manage.py waffle_switch enable_course_filters on --create
endif
nosetests -v acceptance_tests -e NUM_PROCESSES=1 --exclude-dir=acceptance_tests/course_validation

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ The following switches are available:
| enable_performance_learning_outcome | Enable performance section with learning outcome breakdown (functionality based on tagging questions in Studio) |
| enable_learner_download | Display Download CSV button on Learner List page. |
| enable_problem_response_download | Enable downloadable CSV of problem responses |
| enable_course_filters | Enable filters (e.g. pacing type) on courses page. |

[Waffle](http://waffle.readthedocs.org/en/latest/) flags are used to disable/enable
functionality on request (e.g. turning on beta functionality for superusers). Create a
Expand Down
3 changes: 3 additions & 0 deletions acceptance_tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,6 @@ def str2bool(s):

# Learner analytics
DISPLAY_LEARNER_ANALYTICS = str2bool(os.environ.get('DISPLAY_LEARNER_ANALYTICS', False))

# Learner analytics
ENABLE_COURSE_LIST_FILTERS = str2bool(os.environ.get('ENABLE_COURSE_LIST_FILTERS', False))
61 changes: 60 additions & 1 deletion acceptance_tests/test_course_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from bok_choy.web_app_test import WebAppTest
from selenium.webdriver.common.keys import Keys

from acceptance_tests import TEST_COURSE_ID
from acceptance_tests import (
ENABLE_COURSE_LIST_FILTERS,
TEST_COURSE_ID,
)
from acceptance_tests.mixins import AnalyticsDashboardWebAppTestMixin
from acceptance_tests.pages import CourseIndexPage

Expand All @@ -25,6 +28,8 @@ def test_page(self):
self._test_clear_input()
self._test_clear_active_filter()
self._test_clear_all_filters()
if ENABLE_COURSE_LIST_FILTERS:
self._test_filters()

def _test_course_list(self):
"""
Expand Down Expand Up @@ -161,3 +166,57 @@ def _test_clear_all_filters(self):
clear_all_filters.first.click()

self.check_cleared()

def _test_individual_filters(self):
"""
Tests checking each option under each filter set.
The test course will only be displayed under "Upcoming" or "self_paced" filters.
"""
# maps id of filter in DOM to display name shown in active filters
filters = {
"Archived": "Archived",
"Current": "Current",
"Upcoming": "Upcoming",
"unknown": "Unknown",
"instructor_paced": "Instructor-Paced",
"self_paced": "Self-Paced",
}
course_in_filters = ['Upcoming', 'self_paced']
for filter_id, display_name in filters.items():
self._test_filter(filter_id, display_name,
course_in_filter=(True if filter_id in course_in_filters else False))

def _test_multiple_filters(self, filter_sequence):
"""
Tests checking multiple filter options together and whether the course is shown after each filter application.
filter_sequence should be a list of tuples where each element, by index, is:
0. the filter id to apply
1. the filter display name
2. boolean for whether the test course is shown in the list after the filter is applied.
"""
for index, filter in enumerate(filter_sequence):
id = filter[0]
name = filter[1]
course_shown = filter[2]
first_filter = index == 0
self._test_filter(id, name, course_in_filter=course_shown, clear_existing_filters=first_filter)

def _test_filters(self):
self._test_individual_filters()

# Filters ORed within a set
self._test_multiple_filters([
('Archived', 'Archived', False),
('Upcoming', 'Upcoming', True),
('Current', 'Current', True),
('unknown', 'Unknown', True),
])

# Filters ANDed between sets
self._test_multiple_filters([
('Upcoming', 'Upcoming', True),
('self_paced', 'Self-Paced', False),
('instructor_paced', 'Instructor-Paced', False),
])
2 changes: 1 addition & 1 deletion analytics_dashboard/core/templates/core/landing.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{% block body %}
<body class="landing">
<div id="wrap-content-push-footer">
<a href="#content" class="skip-link">{% trans 'Skip to content' %}</a>
<a id="skip" href="#content" class="skip-link">{% trans 'Skip to content' %}</a>
{% include "_skip_link.html" with content_id="content" %}
<header class="navbar navbar-default" id="nav">
<div class="grid-container grid-manual">
Expand Down
3 changes: 3 additions & 0 deletions analytics_dashboard/courses/views/course_summaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _

from waffle import switch_is_active

from courses import permissions
from courses.views import (
CourseAPIMixin,
Expand Down Expand Up @@ -49,6 +51,7 @@ def get_context_data(self, **kwargs):

data = {
'course_list_json': summaries,
'enable_course_filters': switch_is_active('enable_course_filters')
}
context['js_data']['course'] = data
context['page_data'] = self.get_page_data(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ define(function(require) {
trackSubject: 'tracking-subject',
appClass: 'app',
filterKey: 'trees',
filterValues: {dogwood: 1000},
selectDisplayName: 'Dogwood is a tree'
filterValues: [{
name: 'dogwood',
displayName: 'Dogwood is a tree'
}],
sectionDisplayName: 'Checkbox of Trees'
});
checkboxFilter.render();
});
Expand All @@ -30,6 +33,7 @@ define(function(require) {
expect(fixture).toContainElement('#filter-trees');
expect(fixture).toContainElement('input#dogwood');
expect($('#filter-trees label')).toContainText('Dogwood is a tree');
expect($('.filters-label')).toContainText('Checkbox of Trees');
});

it('updates focus', function() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@ define(function(require) {
trackSubject: 'tracking',
appClass: 'app',
filterKey: 'flowers',
filterValues: {tulips: 1000},
selectDisplayName: 'Drop Down Title'
filterValues: [{
name: 'tulips',
displayName: 'Flower: tulips',
count: 1000
}],
sectionDisplayName: 'Drop Down Title'
});
dropDownFilter.render();
});

it('renders a drop down', function() {
var items = dropDownFilter.$el.find('option');
expect(fixture).toContainElement('#filter-flowers');
expect(dropDownFilter.$el.find('label')).toContainText('Drop Down Title');
expect(items.length).toEqual(2);
expect(items[0]).toContainText('All');
expect(items[1]).toContainText('tulips (1,000)');
expect(items[1]).toContainText('Flower: tulips (1,000)');
});

it('updates focus', function() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<% if (!_.isEmpty(filterValues)) { %>
<% var filterId = 'filter-' + filterKey; %>
<hr>
<% if (!_.isEmpty(sectionDisplayName)) { %>
<div class="filters-label"><%- sectionDisplayName %></div>
<% } %>
<div id="<%- filterId %>">
<% _.each(filterValues, function (filterValue) { %>
<input id="<%- filterValue.name %>" type="checkbox" value="<%- filterValue.name %>" <% if (isChecked) { %> checked <% } %>>
<label for="<%- filterValue.name %>"> <%- selectDisplayName %> </label><br>
<input id="<%- filterValue.name %>" type="checkbox" value="<%- filterValue.name %>" <% if (filterValue.isChecked) { %> checked <% } %>>
<label for="<%- filterValue.name %>"> <%- filterValue.displayName %> </label><br>
<% }); %>
</div>
<% } %>
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<% if (!_.isEmpty(filterValues)) { %>
<% var filterId = 'filter-' + filterKey; %>
<hr>
<label for="<%- filterId %>">
<%- selectDisplayName %>
<%- sectionDisplayName %>
</label>
<select id="<%- filterId %>" class="form-control">
<% _.each(filterValues, function (filterValue) { %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ define(function(require) {
* 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;
}
var collection = this.options.collection,
templateOptions = Filter.prototype.templateHelpers.call(this);

_(templateOptions.filterValues).each(function(templateOption) {
var filterValues = collection.getFilterFieldValue(this.options.filterKey);
_(templateOption).extend({
isChecked: !_(filterValues).isNull() && filterValues.indexOf(templateOption.name) > -1
});
}, this);

return templateOptions;
},

Expand Down
37 changes: 22 additions & 15 deletions analytics_dashboard/static/apps/components/filter/views/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ define(function(require) {
trackFilterEventName: ['edx', 'bi', this.options.trackSubject, 'filtered'].join('.')
});
this.listenTo(this.options.collection, 'sync', this.render);
this.listenTo(this.options.collection, 'backgrid:filtersCleared', this.render);
},

templateHelpers: function() {
Expand All @@ -76,29 +77,35 @@ define(function(require) {
var filterValues;

filterValues = _.chain(this.options.filterValues)
.pairs()
.map(function(filterPair) {
var name = filterPair[0],
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 'count' refers to the number of items belonging to that filter.
gettext('<%= name %> (<%= count %>)')
)({
name: name,
count: Utils.localizeNumber(count, 0)
})
.map(function(options) {
var templateOptions = {
name: options.name
};

if (_(options).has('count')) {
_(templateOptions).extend({
displayName: _.template(
// eslint-disable-next-line max-len
// 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: options.displayName,
count: Utils.localizeNumber(options.count, 0)
})
});
} else {
templateOptions.displayName = options.displayName;
}

return templateOptions;
})
.sortBy('name')
.value();

return {
filterKey: this.options.filterKey,
filterValues: filterValues,
selectDisplayName: this.options.selectDisplayName
sectionDisplayName: this.options.sectionDisplayName
};
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ define(function(require) {

this.url = options.url;
this.downloadUrl = options.downloadUrl;

// a map of the filterKey to filterValue to display name
this.filterNameToDisplay = options.filterNameToDisplay;
},

fetch: function(options) {
Expand Down Expand Up @@ -111,17 +114,41 @@ define(function(require) {
}
},

clearFilter: function(filterKey) {
this.unsetFilterField(filterKey);
if (this.mode === 'server') {
this.refresh();
clearFilter: function(filterKey, filterValue) {
var removedFilter = {},
currentValues = this.getFilterFieldValue(filterKey);

if (filterKey === PagingCollection.DefaultSearchKey || currentValues.split(',').length === 1) {
this.unsetFilterField(filterKey);
} else {
// there are multiple values associated with this key, so just remove the one
this.setFilterField(filterKey,
_(currentValues.split(',')).without(filterValue).join(','));
}

this.refresh();
removedFilter[filterKey] = filterValue;
this.trigger('backgrid:filtersCleared', removedFilter);
},

clearAllFilters: function() {
var originalFilters = this.getActiveFilterFields(true);
this.unsetAllFilterFields();
if (this.mode === 'server') {
this.refresh();
this.refresh();
this.trigger('backgrid:filtersCleared', originalFilters);
},

/**
* Returns the display named used for the filter values. Default is to return
* the filterValue. Provide filterNameToDisplay as an option to enable this.
*/
getFilterValueDisplayName: function(filterKey, filterValue) {
var filterNameToDisplay = this.filterNameToDisplay;
if (filterNameToDisplay && _(filterNameToDisplay).has(filterKey) &&
_(filterNameToDisplay[filterKey]).has(filterValue)) {
return filterNameToDisplay[filterKey][filterValue];
} else {
return filterValue.charAt(0).toUpperCase() + filterValue.slice(1);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<% if (hasActiveFilters) { %>
<span id="active-filters-title"><%- activeFiltersTitle %></span>
<ul class="active-filters list-inline" aria-describedby="active-filters-title">
<% _.mapObject(activeFilters, function (filterVal, filterKey) { %>
<li class="filter filter-<%- filterKey %>">
<button class="action-clear-filter btn btn-default" data-filter-key="<%- filterKey %>">
<%- filterVal.displayName %> &nbsp; <span class="fa fa-times" aria-hidden="true"></span>
<% _.each(activeFilters, function (filter) { %>
<li class="filter filter-<%- filter.filterKey %>">
<button class="action-clear-filter btn btn-default" data-filter-key="<%- filter.filterKey %>" data-filter-name="<%- filter.name %>">
<%- filter.displayName %> &nbsp; <span class="fa fa-times" aria-hidden="true"></span>
<span class="sr-only"><%- removeFilterMessage %></span>
</button>
</li>
Expand Down
Loading

0 comments on commit 1ab4209

Please sign in to comment.