Commit 2c774fd2 by Peter Fogg

Allow sorting of teams in a topic.

TNL-1937
parent a8284058
...@@ -20,6 +20,25 @@ TEAMS_HEADER_CSS = '.teams-header' ...@@ -20,6 +20,25 @@ TEAMS_HEADER_CSS = '.teams-header'
CREATE_TEAM_LINK_CSS = '.create-team' 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): class TeamsPage(CoursePage):
""" """
Teams page/tab. Teams page/tab.
...@@ -84,7 +103,7 @@ class TeamsPage(CoursePage): ...@@ -84,7 +103,7 @@ class TeamsPage(CoursePage):
self.q(css='a.nav-item').filter(text=topic)[0].click() 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. The 'My Teams' tab of the Teams page.
""" """
...@@ -98,11 +117,6 @@ class MyTeamsPage(CoursePage, PaginatedUIMixin): ...@@ -98,11 +117,6 @@ class MyTeamsPage(CoursePage, PaginatedUIMixin):
return False return False
return 'is-active' in button_classes[0] 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): class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
""" """
...@@ -145,7 +159,7 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin): ...@@ -145,7 +159,7 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
self.wait_for_ajax() 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 The paginated UI for browsing teams within a Topic on the Teams
page. page.
...@@ -179,9 +193,13 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): ...@@ -179,9 +193,13 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin):
return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text
@property @property
def team_cards(self): def sort_order(self):
"""Get all the team cards on the page.""" """Return the current sort order on the page."""
return self.q(css='.team-card') return self.q(
css='#paging-header-select option'
).filter(
lambda e: e.is_selected()
).results[0].text.strip()
def click_create_team_link(self): def click_create_team_link(self):
""" Click on create team link.""" """ Click on create team link."""
...@@ -204,6 +222,13 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): ...@@ -204,6 +222,13 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin):
query.first.click() query.first.click()
self.wait_for_ajax() 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): class CreateOrEditTeamPage(CoursePage, FieldsMixin):
""" """
......
...@@ -3,7 +3,9 @@ Acceptance tests for the teams feature. ...@@ -3,7 +3,9 @@ Acceptance tests for the teams feature.
""" """
import json import json
import random import random
import time
from dateutil.parser import parse
import ddt import ddt
from flaky import flaky from flaky import flaky
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -38,7 +40,7 @@ class TeamsTabBase(UniqueCourseTest): ...@@ -38,7 +40,7 @@ class TeamsTabBase(UniqueCourseTest):
"""Create `num_topics` test topics.""" """Create `num_topics` test topics."""
return [{u"description": i, u"name": i, u"id": i} for i in map(str, xrange(num_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`.""" """Create `num_teams` teams belonging to `topic`."""
teams = [] teams = []
for i in xrange(num_teams): for i in xrange(num_teams):
...@@ -55,6 +57,10 @@ class TeamsTabBase(UniqueCourseTest): ...@@ -55,6 +57,10 @@ class TeamsTabBase(UniqueCourseTest):
data=json.dumps(team), data=json.dumps(team),
headers=self.course_fixture.headers 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)) teams.append(json.loads(response.text))
return teams return teams
...@@ -107,15 +113,8 @@ class TeamsTabBase(UniqueCourseTest): ...@@ -107,15 +113,8 @@ class TeamsTabBase(UniqueCourseTest):
self.assertEqual(expected_team['name'], team_card_name) self.assertEqual(expected_team['name'], team_card_name)
self.assertEqual(expected_team['description'], team_card_description) self.assertEqual(expected_team['description'], team_card_description)
team_cards = page.team_cards team_card_names = page.team_names
team_card_names = [ team_card_descriptions = page.team_descriptions
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
]
map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions) map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions)
def verify_my_team_count(self, expected_number_of_teams): def verify_my_team_count(self, expected_number_of_teams):
...@@ -473,6 +472,7 @@ class BrowseTopicsTest(TeamsTabBase): ...@@ -473,6 +472,7 @@ class BrowseTopicsTest(TeamsTabBase):
@attr('shard_5') @attr('shard_5')
@ddt.ddt
class BrowseTeamsWithinTopicTest(TeamsTabBase): class BrowseTeamsWithinTopicTest(TeamsTabBase):
""" """
Tests for browsing Teams within a Topic on the Teams page. Tests for browsing Teams within a Topic on the Teams page.
...@@ -482,10 +482,25 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -482,10 +482,25 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
def setUp(self): def setUp(self):
super(BrowseTeamsWithinTopicTest, self).setUp() super(BrowseTeamsWithinTopicTest, self).setUp()
self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} 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.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic)
self.topics_page = BrowseTopicsPage(self.browser, self.course_id) 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): def verify_page_header(self):
"""Verify that the page header correctly reflects the current topic's name and description.""" """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']) self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name'])
...@@ -504,11 +519,11 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -504,11 +519,11 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
footer_visible (bool): Whether we expect to see the pagination footer_visible (bool): Whether we expect to see the pagination
footer controls. footer controls.
""" """
alphabetized_teams = sorted(total_teams, key=lambda team: team['name']) sorted_teams = self.teams_with_default_sort_order(total_teams)
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), pagination_header_text) self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith(pagination_header_text))
self.verify_teams( self.verify_teams(
self.browse_teams_page, 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.assertEqual(
self.browse_teams_page.pagination_controls_visible(), self.browse_teams_page.pagination_controls_visible(),
...@@ -516,6 +531,63 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -516,6 +531,63 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
msg='Expected paging footer to be ' + 'visible' if footer_visible else 'invisible' 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): def test_no_teams(self):
""" """
Scenario: Visiting a topic with no teams should not display any teams. Scenario: Visiting a topic with no teams should not display any teams.
...@@ -529,7 +601,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -529,7 +601,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
""" """
self.browse_teams_page.visit() self.browse_teams_page.visit()
self.verify_page_header() 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.assertEqual(len(self.browse_teams_page.team_cards), 0, msg='Expected to see no team cards')
self.assertFalse( self.assertFalse(
self.browse_teams_page.pagination_controls_visible(), self.browse_teams_page.pagination_controls_visible(),
...@@ -548,10 +620,12 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -548,10 +620,12 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And I should see a button to add a team And I should see a button to add a team
And I should not see a pagination footer 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.browse_teams_page.visit()
self.verify_page_header() 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.verify_teams(self.browse_teams_page, teams)
self.assertFalse( self.assertFalse(
self.browse_teams_page.pagination_controls_visible(), self.browse_teams_page.pagination_controls_visible(),
...@@ -571,7 +645,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -571,7 +645,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And when I click on the previous page button And when I click on the previous page button
Then I should see that I am on the first page of results 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.browse_teams_page.visit()
self.verify_page_header() self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True) self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True)
...@@ -593,7 +667,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -593,7 +667,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
When I input the first page When I input the first page
Then I should see that I am on the first page of results 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.browse_teams_page.visit()
self.verify_page_header() self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True) self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
...@@ -848,13 +922,13 @@ class CreateTeamTest(TeamFormActions): ...@@ -848,13 +922,13 @@ class CreateTeamTest(TeamFormActions):
Then I should see teams list page without any new team. Then I should see teams list page without any new team.
And if I switch to "My Team", it shows no teams 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.verify_and_navigate_to_create_team_page()
self.create_or_edit_team_page.cancel_team() self.create_or_edit_team_page.cancel_team()
self.assertTrue(self.browse_teams_page.is_browser_on_page()) 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.click_all_topics()
self.teams_page.verify_team_count_in_first_topic(0) self.teams_page.verify_team_count_in_first_topic(0)
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'], define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'],
function(BaseCollection, TeamModel, gettext) { function(BaseCollection, TeamModel, gettext) {
var TeamCollection = BaseCollection.extend({ var TeamCollection = BaseCollection.extend({
sortField: 'last_activity_at',
initialize: function(teams, options) { initialize: function(teams, options) {
var self = this; var self = this;
BaseCollection.prototype.initialize.call(this, options); BaseCollection.prototype.initialize.call(this, options);
...@@ -12,14 +14,14 @@ ...@@ -12,14 +14,14 @@
topic_id: this.topic_id = options.topic_id, topic_id: this.topic_id = options.topic_id,
expand: 'user', expand: 'user',
course_id: function () { return encodeURIComponent(self.course_id); }, 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 BaseCollection.prototype.server_api
); );
delete this.server_api.sort_order; // Sort order is not specified for the Team API delete this.server_api.sort_order; // Sort order is not specified for the Team API
this.registerSortableField('name', gettext('name')); this.registerSortableField('last_activity_at', gettext('last activity'));
this.registerSortableField('open_slots', gettext('open_slots')); this.registerSortableField('open_slots', gettext('open slots'));
}, },
model: TeamModel model: TeamModel
......
...@@ -10,10 +10,6 @@ ...@@ -10,10 +10,6 @@
var TeamsView = PaginatedView.extend({ var TeamsView = PaginatedView.extend({
type: 'teams', type: 'teams',
events: {
'click button.action': '' // entry point for team creation
},
srInfo: { srInfo: {
id: "heading-browse-teams", id: "heading-browse-teams",
text: gettext('All teams') text: gettext('All teams')
......
;(function (define) { ;(function (define) {
'use strict'; 'use strict';
define([
define(['backbone', 'gettext', 'teams/js/views/teams', 'backbone',
'text!teams/templates/team-actions.underscore'], 'gettext',
function (Backbone, gettext, TeamsView, teamActionsTemplate) { '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({ var TopicTeamsView = TeamsView.extend({
events: { events: {
'click a.browse-teams': 'browseTeams', 'click a.browse-teams': 'browseTeams',
...@@ -54,6 +57,14 @@ ...@@ -54,6 +57,14 @@
showCreateTeamForm: function (event) { showCreateTeamForm: function (event) {
event.preventDefault(); event.preventDefault();
Backbone.history.navigate('topics/' + this.teamParams.topicID + '/create-team', {trigger: true}); 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
});
} }
}); });
......
...@@ -164,6 +164,7 @@ ...@@ -164,6 +164,7 @@
label { // override label { // override
color: inherit; color: inherit;
font-size: inherit; font-size: inherit;
cursor: auto;
} }
.listing-sort-select { .listing-sort-select {
......
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