Allow sorting of teams in a topic.
TNL-1937
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
label { // override
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.listing-sort-select {
|
||||
|
||||
Reference in New Issue
Block a user