From 382909b7c34580ef982fe1401ae654ed70f5a0ce Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Aug 2015 13:08:25 -0400 Subject: [PATCH] Add sorting controls on Teams topics page. TNL-1936 --- .../collections/paging_collection.js | 2 +- .../js/components/views/paging_header.js | 17 +++- .../js/spec/components/paging_header_spec.js | 21 ++++- .../components/paging-header.underscore | 17 +++- common/test/acceptance/pages/lms/teams.py | 18 +++++ .../test/acceptance/tests/lms/test_teams.py | 77 +++++++++++++++++-- .../teams/static/teams/js/collections/base.js | 2 +- .../teams/static/teams/js/views/topics.js | 12 ++- 8 files changed, 151 insertions(+), 15 deletions(-) diff --git a/common/static/common/js/components/collections/paging_collection.js b/common/static/common/js/components/collections/paging_collection.js index ae93d25029..4ac4bf93b3 100644 --- a/common/static/common/js/components/collections/paging_collection.js +++ b/common/static/common/js/components/collections/paging_collection.js @@ -82,7 +82,7 @@ setPage: function (page) { var oldPage = this.currentPage, self = this; - this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then( + return this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then( function () { self.trigger('page_changed'); }, diff --git a/common/static/common/js/components/views/paging_header.js b/common/static/common/js/components/views/paging_header.js index bd5e741778..8cd01bb9a1 100644 --- a/common/static/common/js/components/views/paging_header.js +++ b/common/static/common/js/components/views/paging_header.js @@ -9,12 +9,16 @@ var PagingHeader = Backbone.View.extend({ initialize: function (options) { this.srInfo = options.srInfo; - this.collections = options.collection; + this.showSortControls = options.showSortControls; this.collection.bind('add', _.bind(this.render, this)); this.collection.bind('remove', _.bind(this.render, this)); this.collection.bind('reset', _.bind(this.render, this)); }, + events: { + 'change #paging-header-select': 'sortCollection' + }, + render: function () { var message, start = _.isUndefined(this.collection.start) ? 0 : this.collection.start, @@ -31,9 +35,18 @@ } this.$el.html(_.template(headerTemplate, { message: message, - srInfo: this.srInfo + srInfo: this.srInfo, + sortableFields: this.collection.sortableFields, + sortOrder: this.sortOrder, + showSortControls: this.showSortControls })); return this; + }, + + sortCollection: function () { + var selected = this.$('#paging-header-select option:selected'); + this.sortOrder = selected.attr('value'); + this.collection.setSortField(this.sortOrder); } }); return PagingHeader; diff --git a/common/static/common/js/spec/components/paging_header_spec.js b/common/static/common/js/spec/components/paging_header_spec.js index 2e664d449e..dee7efcb6e 100644 --- a/common/static/common/js/spec/components/paging_header_spec.js +++ b/common/static/common/js/spec/components/paging_header_spec.js @@ -8,7 +8,7 @@ define([ var pagingHeader, newCollection = function (size, perPage) { var pageSize = 5, - results = _.map(_.range(size), function () { return {}; }); + results = _.map(_.range(size), function (i) { return {foo: i}; }); var collection = new PagingCollection( { count: results.length, @@ -22,6 +22,14 @@ define([ collection.start = 0; collection.totalCount = results.length; return collection; + }, + sortableHeader = function (sortable) { + var collection = newCollection(5, 4); + collection.registerSortableField('foo', 'Display Name'); + return new PagingHeader({ + collection: collection, + showSortControls: _.isUndefined(sortable) ? true : sortable + }); }; it('correctly displays which items are being viewed', function () { @@ -47,5 +55,16 @@ define([ expect(pagingHeader.$el.find('.search-count').text()) .toContain('Showing 1 out of 1 total'); }); + + it('optionally shows sorting controls', function () { + pagingHeader = sortableHeader().render(); + expect(pagingHeader.$el.find('.listing-sort').text()) + .toMatch(/Sorted by\s+Display Name/); + }); + + it('does not show sorting controls if the `showSortControls` option is not passed', function () { + pagingHeader = sortableHeader(false).render(); + expect(pagingHeader.$el.text()).not.toContain('Sorted by'); + }); }); }); diff --git a/common/static/common/templates/components/paging-header.underscore b/common/static/common/templates/components/paging-header.underscore index 04a5d60e44..7484299b72 100644 --- a/common/static/common/templates/components/paging-header.underscore +++ b/common/static/common/templates/components/paging-header.underscore @@ -1,8 +1,21 @@ <% if (!_.isUndefined(srInfo)) { %>

<%- srInfo.text %>

<% } %> -
- +
+ <%= message %> + <% if (showSortControls) { %> + | + + + + + <% } %>
diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index 07833c9eb0..6015f46a53 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -11,6 +11,7 @@ from .fields import FieldsMixin TOPIC_CARD_CSS = 'div.wrapper-card-core' +CARD_TITLE_CSS = 'h3.card-title' MY_TEAMS_BUTTON_CSS = 'a.nav-item[data-index="0"]' BROWSE_BUTTON_CSS = 'a.nav-item[data-index="1"]' TEAMS_LINK_CSS = '.action-view' @@ -121,6 +122,11 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin): """Return a list of the topic cards present on the page.""" return self.q(css=TOPIC_CARD_CSS).results + @property + def topic_names(self): + """Return a list of the topic names present on the page.""" + return self.q(css=CARD_TITLE_CSS).map(lambda e: e.text).results + def browse_teams_for_topic(self, topic_name): """ Show the teams list for `topic_name`. @@ -130,6 +136,13 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin): )[0].click() self.wait_for_ajax() + def sort_topics_by(self, sort_order): + """Sort the list of topics by the given `sort_order`.""" + self.q( + css='#paging-header-select option[value={sort_order}]'.format(sort_order=sort_order) + ).click() + self.wait_for_ajax() + class BrowseTeamsPage(CoursePage, PaginatedUIMixin): """ @@ -388,3 +401,8 @@ class TeamPage(CoursePage, PaginatedUIMixin): def new_post_button_present(self): """ Returns True if New Post button is present else False """ return self.q(css='.discussion-module .new-post-btn').present + + def click_all_topics_breadcrumb(self): + """Navigate to the 'All Topics' page.""" + self.q(css='.breadcrumbs a').results[0].click() + self.wait_for_ajax() diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 1a105c38ef..3e9c36367b 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -2,6 +2,7 @@ Acceptance tests for the teams feature. """ import json +import random import ddt from flaky import flaky @@ -22,6 +23,9 @@ from ...pages.lms.tab_nav import TabNavPage from ...pages.lms.teams import TeamsPage, MyTeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage, TeamPage +TOPICS_PER_PAGE = 12 + + class TeamsTabBase(UniqueCourseTest): """Base class for Teams Tab tests""" def setUp(self): @@ -274,6 +278,7 @@ class MyTeamsTest(TeamsTabBase): @attr('shard_5') +@ddt.ddt class BrowseTopicsTest(TeamsTabBase): """ Tests for the Browse tab of the Teams page. @@ -283,6 +288,66 @@ class BrowseTopicsTest(TeamsTabBase): super(BrowseTopicsTest, self).setUp() self.topics_page = BrowseTopicsPage(self.browser, self.course_id) + @ddt.data(('name', False), ('team_count', True)) + @ddt.unpack + def test_sort_topics(self, sort_order, reverse): + """ + Scenario: the user should be able to sort the list of topics by name or team count + Given I am enrolled in a course with team configuration and topics + When I visit the Teams page + And I browse topics + Then I should see a list of topics for the course + When I choose a sort order + Then I should see the paginated list of topics in that order + """ + topics = self.create_topics(TOPICS_PER_PAGE + 1) + self.set_team_configuration({u"max_team_size": 100, u"topics": topics}) + for i, topic in enumerate(random.sample(topics, len(topics))): + self.create_teams(topic, i) + topic['team_count'] = i + self.topics_page.visit() + self.topics_page.sort_topics_by(sort_order) + topic_names = self.topics_page.topic_names + self.assertEqual(len(topic_names), TOPICS_PER_PAGE) + self.assertEqual( + topic_names, + [t['name'] for t in sorted(topics, key=lambda t: t[sort_order], reverse=reverse)][:TOPICS_PER_PAGE] + ) + + def test_sort_topics_update(self): + """ + Scenario: the list of topics should remain sorted after updates + Given I am enrolled in a course with team configuration and topics + When I visit the Teams page + And I browse topics and choose a sort order + Then I should see the paginated list of topics in that order + When I create a team in one of those topics + And I return to the topics list + Then I should see the topics in the correct sorted order + """ + topics = self.create_topics(3) + self.set_team_configuration({u"max_team_size": 100, u"topics": topics}) + self.topics_page.visit() + self.topics_page.sort_topics_by('team_count') + topic_name = self.topics_page.topic_names[-1] + topic = [t for t in topics if t['name'] == topic_name][0] + self.topics_page.browse_teams_for_topic(topic_name) + browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, topic) + self.assertTrue(browse_teams_page.is_browser_on_page()) + browse_teams_page.click_create_team_link() + create_team_page = CreateTeamPage(self.browser, self.course_id, topic) + create_team_page.value_for_text_field(field_id='name', value='Team Name', press_enter=False) + create_team_page.value_for_textarea_field( + field_id='description', + value='Team description.' + ) + create_team_page.submit_form() + team_page = TeamPage(self.browser, self.course_id) + self.assertTrue(team_page.is_browser_on_page) + team_page.click_all_topics_breadcrumb() + self.assertTrue(self.topics_page.is_browser_on_page()) + self.assertEqual(topic_name, self.topics_page.topic_names[0]) + def test_list_topics(self): """ Scenario: a list of topics should be visible in the "Browse" tab @@ -294,7 +359,7 @@ class BrowseTopicsTest(TeamsTabBase): self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(2)}) self.topics_page.visit() self.assertEqual(len(self.topics_page.topic_cards), 2) - self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-2 out of 2 total') + self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-2 out of 2 total')) self.assertFalse(self.topics_page.pagination_controls_visible()) self.assertFalse(self.topics_page.is_previous_page_button_enabled()) self.assertFalse(self.topics_page.is_next_page_button_enabled()) @@ -309,8 +374,8 @@ class BrowseTopicsTest(TeamsTabBase): """ self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(20)}) self.topics_page.visit() - self.assertEqual(len(self.topics_page.topic_cards), 12) - self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-12 out of 20 total') + self.assertEqual(len(self.topics_page.topic_cards), TOPICS_PER_PAGE) + self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-12 out of 20 total')) self.assertTrue(self.topics_page.pagination_controls_visible()) self.assertFalse(self.topics_page.is_previous_page_button_enabled()) self.assertTrue(self.topics_page.is_next_page_button_enabled()) @@ -360,10 +425,10 @@ class BrowseTopicsTest(TeamsTabBase): self.topics_page.visit() self.topics_page.press_next_page_button() self.assertEqual(len(self.topics_page.topic_cards), 1) - self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 13-13 out of 13 total') + self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 13-13 out of 13 total')) self.topics_page.press_previous_page_button() - self.assertEqual(len(self.topics_page.topic_cards), 12) - self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-12 out of 13 total') + self.assertEqual(len(self.topics_page.topic_cards), TOPICS_PER_PAGE) + self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-12 out of 13 total')) def test_topic_description_truncation(self): """ diff --git a/lms/djangoapps/teams/static/teams/js/collections/base.js b/lms/djangoapps/teams/static/teams/js/collections/base.js index 512ce2f3bc..01410af938 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/base.js +++ b/lms/djangoapps/teams/static/teams/js/collections/base.js @@ -27,7 +27,7 @@ var self = this, deferred = $.Deferred(); if (force || this.isStale) { - this.fetch() + this.setPage(1) .done(function() { self.isStale = false; deferred.resolve(); diff --git a/lms/djangoapps/teams/static/teams/js/views/topics.js b/lms/djangoapps/teams/static/teams/js/views/topics.js index b1b4df19bb..e5ba55c46c 100644 --- a/lms/djangoapps/teams/static/teams/js/views/topics.js +++ b/lms/djangoapps/teams/static/teams/js/views/topics.js @@ -3,8 +3,9 @@ define([ 'gettext', 'teams/js/views/topic_card', + 'common/js/components/views/paging_header', 'common/js/components/views/paginated_view' - ], function (gettext, TopicCardView, PaginatedView) { + ], function (gettext, TopicCardView, PagingHeader, PaginatedView) { var TopicsView = PaginatedView.extend({ type: 'topics', @@ -21,11 +22,18 @@ PaginatedView.prototype.initialize.call(this); }, + createHeaderView: function () { + return new PagingHeader({ + collection: this.options.collection, + srInfo: this.srInfo, + showSortControls: true + }); + }, + render: function() { var self = this; this.collection.refresh() .done(function() { - self.collection.isStale = false; PaginatedView.prototype.render.call(self); }); return this;