diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index 1f3779efaf..351e091b6b 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -20,6 +20,25 @@ TEAMS_HEADER_CSS = '.teams-header' CREATE_TEAM_LINK_CSS = '.create-team' +class TeamCardsMixin(object): + """Provides common operations on the team card component.""" + + @property + def team_cards(self): + """Get all the team cards on the page.""" + return self.q(css='.team-card') + + @property + def team_names(self): + """Return the names of each team on the page.""" + return self.q(css='h3.card-title').map(lambda e: e.text).results + + @property + def team_descriptions(self): + """Return the names of each team on the page.""" + return self.q(css='p.card-description').map(lambda e: e.text).results + + class TeamsPage(CoursePage): """ Teams page/tab. @@ -84,7 +103,7 @@ class TeamsPage(CoursePage): self.q(css='a.nav-item').filter(text=topic)[0].click() -class MyTeamsPage(CoursePage, PaginatedUIMixin): +class MyTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin): """ The 'My Teams' tab of the Teams page. """ @@ -98,11 +117,6 @@ class MyTeamsPage(CoursePage, PaginatedUIMixin): return False return 'is-active' in button_classes[0] - @property - def team_cards(self): - """Get all the team cards on the page.""" - return self.q(css='.team-card') - class BrowseTopicsPage(CoursePage, PaginatedUIMixin): """ @@ -145,7 +159,7 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin): self.wait_for_ajax() -class BrowseTeamsPage(CoursePage, PaginatedUIMixin): +class BrowseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin): """ The paginated UI for browsing teams within a Topic on the Teams page. @@ -179,9 +193,13 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text @property - def team_cards(self): - """Get all the team cards on the page.""" - return self.q(css='.team-card') + def sort_order(self): + """Return the current sort order on the page.""" + return self.q( + css='#paging-header-select option' + ).filter( + lambda e: e.is_selected() + ).results[0].text.strip() def click_create_team_link(self): """ Click on create team link.""" @@ -204,6 +222,13 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): query.first.click() self.wait_for_ajax() + def sort_teams_by(self, sort_order): + """Sort the list of teams 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 CreateOrEditTeamPage(CoursePage, FieldsMixin): """ diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 84f327fb16..0315311254 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -3,7 +3,9 @@ Acceptance tests for the teams feature. """ import json import random +import time +from dateutil.parser import parse import ddt from flaky import flaky from nose.plugins.attrib import attr @@ -38,7 +40,7 @@ class TeamsTabBase(UniqueCourseTest): """Create `num_topics` test topics.""" return [{u"description": i, u"name": i, u"id": i} for i in map(str, xrange(num_topics))] - def create_teams(self, topic, num_teams): + def create_teams(self, topic, num_teams, time_between_creation=0): """Create `num_teams` teams belonging to `topic`.""" teams = [] for i in xrange(num_teams): @@ -55,6 +57,10 @@ class TeamsTabBase(UniqueCourseTest): data=json.dumps(team), headers=self.course_fixture.headers ) + # Sadly, this sleep is necessary in order to ensure that + # sorting by last_activity_at works correctly when running + # in Jenkins. + time.sleep(time_between_creation) teams.append(json.loads(response.text)) return teams @@ -107,15 +113,8 @@ class TeamsTabBase(UniqueCourseTest): self.assertEqual(expected_team['name'], team_card_name) self.assertEqual(expected_team['description'], team_card_description) - team_cards = page.team_cards - team_card_names = [ - team_card.find_element_by_css_selector('.card-title').text - for team_card in team_cards.results - ] - team_card_descriptions = [ - team_card.find_element_by_css_selector('.card-description').text - for team_card in team_cards.results - ] + team_card_names = page.team_names + team_card_descriptions = page.team_descriptions map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions) def verify_my_team_count(self, expected_number_of_teams): @@ -473,6 +472,7 @@ class BrowseTopicsTest(TeamsTabBase): @attr('shard_5') +@ddt.ddt class BrowseTeamsWithinTopicTest(TeamsTabBase): """ Tests for browsing Teams within a Topic on the Teams page. @@ -482,10 +482,25 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): def setUp(self): super(BrowseTeamsWithinTopicTest, self).setUp() self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} - self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) + self.max_team_size = 10 + self.set_team_configuration({ + 'course_id': self.course_id, + 'max_team_size': self.max_team_size, + 'topics': [self.topic] + }) self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) self.topics_page = BrowseTopicsPage(self.browser, self.course_id) + def teams_with_default_sort_order(self, teams): + """Return a list of teams sorted according to the default ordering + (last_activity_at, with a secondary sort by open slots). + """ + return sorted( + sorted(teams, key=lambda t: len(t['membership']), reverse=True), + key=lambda t: parse(t['last_activity_at']).replace(microsecond=0), + reverse=True + ) + def verify_page_header(self): """Verify that the page header correctly reflects the current topic's name and description.""" self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name']) @@ -504,11 +519,11 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): footer_visible (bool): Whether we expect to see the pagination footer controls. """ - alphabetized_teams = sorted(total_teams, key=lambda team: team['name']) - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), pagination_header_text) + sorted_teams = self.teams_with_default_sort_order(total_teams) + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith(pagination_header_text)) self.verify_teams( self.browse_teams_page, - alphabetized_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE] + sorted_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE] ) self.assertEqual( self.browse_teams_page.pagination_controls_visible(), @@ -516,6 +531,63 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): msg='Expected paging footer to be ' + 'visible' if footer_visible else 'invisible' ) + @ddt.data( + ('open_slots', 'last_activity_at', True), + ('last_activity_at', 'open_slots', True) + ) + @ddt.unpack + def test_sort_teams(self, sort_order, secondary_sort_order, reverse): + """ + Scenario: the user should be able to sort the list of teams by open slots or last activity + Given I am enrolled in a course with team configuration and topics + When I visit the Teams page + And I browse teams within a topic + Then I should see a list of teams for that topic + When I choose a sort order + Then I should see the paginated list of teams in that order + """ + teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) + for i, team in enumerate(random.sample(teams, len(teams))): + for _ in range(i): + user_info = AutoAuthPage(self.browser, course_id=self.course_id).visit().user_info + self.create_membership(user_info['username'], team['id']) + team['open_slots'] = self.max_team_size - i + # Parse last activity date, removing microseconds because + # the Django ORM does not support them. Will be fixed in + # Django 1.8. + team['last_activity_at'] = parse(team['last_activity_at']).replace(microsecond=0) + # Re-authenticate as staff after creating users + AutoAuthPage( + self.browser, + course_id=self.course_id, + staff=True + ).visit() + self.browse_teams_page.visit() + self.browse_teams_page.sort_teams_by(sort_order) + team_names = self.browse_teams_page.team_names + self.assertEqual(len(team_names), self.TEAMS_PAGE_SIZE) + sorted_teams = [ + team['name'] + for team in sorted( + sorted(teams, key=lambda t: t[secondary_sort_order], reverse=reverse), + key=lambda t: t[sort_order], + reverse=reverse + ) + ][:self.TEAMS_PAGE_SIZE] + self.assertEqual(team_names, sorted_teams) + + def test_default_sort_order(self): + """ + Scenario: the list of teams should be sorted by last activity by default + Given I am enrolled in a course with team configuration and topics + When I visit the Teams page + And I browse teams within a topic + Then I should see a list of teams for that topic, sorted by last activity + """ + self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) + self.browse_teams_page.visit() + self.assertEqual(self.browse_teams_page.sort_order, 'last activity') + def test_no_teams(self): """ Scenario: Visiting a topic with no teams should not display any teams. @@ -529,7 +601,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): """ self.browse_teams_page.visit() self.verify_page_header() - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.assertEqual(len(self.browse_teams_page.team_cards), 0, msg='Expected to see no team cards') self.assertFalse( self.browse_teams_page.pagination_controls_visible(), @@ -548,10 +620,12 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): And I should see a button to add a team And I should not see a pagination footer """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE) + teams = self.teams_with_default_sort_order( + self.create_teams(self.topic, self.TEAMS_PAGE_SIZE, time_between_creation=1) + ) self.browse_teams_page.visit() self.verify_page_header() - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1-10 out of 10 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 1-10 out of 10 total')) self.verify_teams(self.browse_teams_page, teams) self.assertFalse( self.browse_teams_page.pagination_controls_visible(), @@ -571,7 +645,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): And when I click on the previous page button Then I should see that I am on the first page of results """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) + teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1, time_between_creation=1) self.browse_teams_page.visit() self.verify_page_header() self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True) @@ -593,7 +667,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): When I input the first page Then I should see that I am on the first page of results """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10) + teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10, time_between_creation=1) self.browse_teams_page.visit() self.verify_page_header() self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True) @@ -848,13 +922,13 @@ class CreateTeamTest(TeamFormActions): Then I should see teams list page without any new team. And if I switch to "My Team", it shows no teams """ - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.verify_and_navigate_to_create_team_page() self.create_or_edit_team_page.cancel_team() self.assertTrue(self.browse_teams_page.is_browser_on_page()) - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.teams_page.click_all_topics() self.teams_page.verify_team_count_in_first_topic(0) diff --git a/lms/djangoapps/teams/static/teams/js/collections/team.js b/lms/djangoapps/teams/static/teams/js/collections/team.js index 417f7cdf4e..a67dbd1fd9 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/team.js +++ b/lms/djangoapps/teams/static/teams/js/collections/team.js @@ -3,6 +3,8 @@ define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'], function(BaseCollection, TeamModel, gettext) { var TeamCollection = BaseCollection.extend({ + sortField: 'last_activity_at', + initialize: function(teams, options) { var self = this; BaseCollection.prototype.initialize.call(this, options); @@ -12,14 +14,14 @@ topic_id: this.topic_id = options.topic_id, expand: 'user', course_id: function () { return encodeURIComponent(self.course_id); }, - order_by: function () { return 'name'; } // TODO surface sort order in UI + order_by: function () { return this.sortField; } }, BaseCollection.prototype.server_api ); delete this.server_api.sort_order; // Sort order is not specified for the Team API - this.registerSortableField('name', gettext('name')); - this.registerSortableField('open_slots', gettext('open_slots')); + this.registerSortableField('last_activity_at', gettext('last activity')); + this.registerSortableField('open_slots', gettext('open slots')); }, model: TeamModel diff --git a/lms/djangoapps/teams/static/teams/js/views/teams.js b/lms/djangoapps/teams/static/teams/js/views/teams.js index f25adaa2df..697dae049a 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams.js @@ -10,10 +10,6 @@ var TeamsView = PaginatedView.extend({ type: 'teams', - events: { - 'click button.action': '' // entry point for team creation - }, - srInfo: { id: "heading-browse-teams", text: gettext('All teams') diff --git a/lms/djangoapps/teams/static/teams/js/views/topic_teams.js b/lms/djangoapps/teams/static/teams/js/views/topic_teams.js index da8b670fc7..a79f09554f 100644 --- a/lms/djangoapps/teams/static/teams/js/views/topic_teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/topic_teams.js @@ -1,9 +1,12 @@ ;(function (define) { 'use strict'; - - define(['backbone', 'gettext', 'teams/js/views/teams', - 'text!teams/templates/team-actions.underscore'], - function (Backbone, gettext, TeamsView, teamActionsTemplate) { + define([ + 'backbone', + 'gettext', + 'teams/js/views/teams', + 'common/js/components/views/paging_header', + 'text!teams/templates/team-actions.underscore' + ], function (Backbone, gettext, TeamsView, PagingHeader, teamActionsTemplate) { var TopicTeamsView = TeamsView.extend({ events: { 'click a.browse-teams': 'browseTeams', @@ -54,6 +57,14 @@ showCreateTeamForm: function (event) { event.preventDefault(); Backbone.history.navigate('topics/' + this.teamParams.topicID + '/create-team', {trigger: true}); + }, + + createHeaderView: function () { + return new PagingHeader({ + collection: this.options.collection, + srInfo: this.srInfo, + showSortControls: true + }); } }); diff --git a/lms/static/sass/views/_teams.scss b/lms/static/sass/views/_teams.scss index 53140ecd7d..8a2bd4b0ba 100644 --- a/lms/static/sass/views/_teams.scss +++ b/lms/static/sass/views/_teams.scss @@ -164,6 +164,7 @@ label { // override color: inherit; font-size: inherit; + cursor: auto; } .listing-sort-select {