Commit 1265d540 by Andy Armstrong

Merge pull request #9302 from edx/andya/team-refresh-handling

Implement model refreshing for Teams tab
parents 752226df c84abfab
define(['sinon', 'underscore'], function(sinon, _) {
var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest,
define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) {
'use strict';
var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectJsonRequestURL,
respondWithJson, respondWithError, respondWithTextError, respondWithNoContent;
/* These utility methods are used by Jasmine tests to create a mock server or
......@@ -69,6 +71,24 @@ define(['sinon', 'underscore'], function(sinon, _) {
};
/**
* Expect that a JSON request be made with the given URL and parameters.
* @param requests The collected requests
* @param expectedUrl The expected URL excluding the parameters
* @param expectedParameters An object representing the URL parameters
* @param requestIndex An optional index for the request (by default, the last request is used)
*/
expectJsonRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) {
var request, parameters;
if (_.isUndefined(requestIndex)) {
requestIndex = requests.length - 1;
}
request = requests[requestIndex];
parameters = new URI(request.url).query(true);
delete parameters._; // Ignore the cache-busting argument
expect(parameters).toEqual(expectedParameters);
};
/**
* Intended for use with POST requests using application/x-www-form-urlencoded.
*/
expectPostRequest = function(requests, url, body, requestIndex) {
......@@ -136,6 +156,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
'requests': fakeRequests,
'expectRequest': expectRequest,
'expectJsonRequest': expectJsonRequest,
'expectJsonRequestURL': expectJsonRequestURL,
'expectPostRequest': expectPostRequest,
'respondWithJson': respondWithJson,
'respondWithError': respondWithError,
......
......@@ -45,6 +45,42 @@ class TeamsPage(CoursePage):
""" View the Browse tab of the Teams page. """
self.q(css=BROWSE_BUTTON_CSS).click()
def verify_team_count_in_first_topic(self, expected_count):
"""
Verify that the team count on the first topic card in the topic list is correct
(browse topics page).
"""
self.wait_for(
lambda: self.q(css='.team-count')[0].text == "0 Teams" if expected_count == 0 else "1 Team",
description="Team count text on topic is wrong"
)
def verify_topic_team_count(self, expected_count):
""" Verify the number of teams listed on the topic page (browse teams within topic). """
self.wait_for(
lambda: len(self.q(css='.team-card')) == expected_count,
description="Expected number of teams is wrong"
)
def verify_my_team_count(self, expected_count):
""" Verify the number of teams on 'My Team'. """
# Click to "My Team" and verify that it contains the expected number of teams.
self.q(css=MY_TEAMS_BUTTON_CSS).click()
self.wait_for(
lambda: len(self.q(css='.team-card')) == expected_count,
description="Expected number of teams is wrong"
)
def click_all_topics(self):
""" Click on the "All Topics" breadcrumb """
self.q(css='a.nav-item').filter(text='All Topics')[0].click()
def click_specific_topic(self, topic):
""" Click on the breadcrumb for a specific topic """
self.q(css='a.nav-item').filter(text=topic)[0].click()
class MyTeamsPage(CoursePage, PaginatedUIMixin):
"""
......@@ -284,10 +320,14 @@ class TeamPage(CoursePage, PaginatedUIMixin):
"""Verifies that team leave link is present"""
return self.q(css='.leave-team-link').present
def click_leave_team_link(self):
def click_leave_team_link(self, remaining_members=0):
""" Click on Leave Team link"""
self.q(css='.leave-team-link').first.click()
self.wait_for_ajax()
self.wait_for(
lambda: self.join_team_button_present,
description="Join Team button did not become present"
)
self.wait_for_capacity_text(remaining_members)
@property
def team_members(self):
......@@ -303,10 +343,29 @@ class TeamPage(CoursePage, PaginatedUIMixin):
"""Returns the username of team member"""
return self.q(css='.page-content-secondary .tooltip-custom').text[0]
def click_join_team_button(self):
def click_join_team_button(self, total_members=1):
""" Click on Join Team button"""
self.q(css='.join-team .action-primary').first.click()
self.wait_for_ajax()
self.wait_for(
lambda: not self.join_team_button_present,
description="Join Team button did not go away"
)
self.wait_for_capacity_text(total_members)
def wait_for_capacity_text(self, num_members, max_size=10):
""" Wait for the team capacity text to be correct. """
self.wait_for(
lambda: self.team_capacity_text == self.format_capacity_text(num_members, max_size),
description="Team capacity text is not correct"
)
def format_capacity_text(self, num_members, max_size):
""" Helper method to format the expected team capacity text. """
return '{num_members} / {max_size} {members_text}'.format(
num_members=num_members,
max_size=max_size,
members_text='Member' if num_members == max_size else 'Members'
)
@property
def join_team_message(self):
......@@ -317,7 +376,6 @@ class TeamPage(CoursePage, PaginatedUIMixin):
@property
def join_team_button_present(self):
""" Returns True if Join Team button is present else False """
self.wait_for_ajax()
return self.q(css='.join-team .action-primary').present
@property
......
......@@ -113,6 +113,12 @@ class TeamsTabBase(UniqueCourseTest):
]
map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions)
def verify_my_team_count(self, expected_number_of_teams):
""" Verify the number of teams shown on "My Team". """
# We are doing these operations on this top-level page object to avoid reloading the page.
self.teams_page.verify_my_team_count(expected_number_of_teams)
@ddt.ddt
@attr('shard_5')
......@@ -248,7 +254,7 @@ class MyTeamsTest(TeamsTabBase):
self.assertEqual(len(self.my_teams_page.team_cards), 0, msg='Expected to see no team cards')
self.assertEqual(
self.my_teams_page.q(css='.page-content-main').text,
[u'You are not currently a member of any teams.']
[u'You are not currently a member of any team.']
)
def test_member_of_a_team(self):
......@@ -530,30 +536,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.browse_teams_page.go_to_page(1)
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
def test_teams_membership(self):
"""
Scenario: Team cards correctly reflect membership of the team.
Given I am enrolled in a course with a team configuration and a topic
containing one team
And I add myself to the team
When I visit the Teams page for that topic
Then I should see the correct page header
And I should see the team for that topic
And I should see that the team card shows my membership
"""
teams = self.create_teams(self.topic, 1)
self.browse_teams_page.visit()
self.verify_page_header()
self.verify_teams(self.browse_teams_page, teams)
self.create_membership(self.user_info['username'], teams[0]['id'])
self.browser.refresh()
self.browse_teams_page.wait_for_ajax()
## TODO: fix this!
# self.assertEqual(
# self.browse_teams_page.team_cards[0].find_element_by_css_selector('.member-count').text,
# '1 / 10 Members'
# )
def test_navigation_links(self):
"""
Scenario: User should be able to navigate to "browse all teams" and "search team description" links.
......@@ -715,6 +697,9 @@ class CreateTeamTest(TeamsTabBase):
And I click Create button
Then I should see the page for my team
And I should see the message that says "You are member of this team"
And the new team should be added to the list of teams within the topic
And the number of teams should be updated on the topic card
And if I switch to "My Team", the newly created team is displayed
"""
self.verify_and_navigate_to_create_team_page()
......@@ -728,6 +713,16 @@ class CreateTeamTest(TeamsTabBase):
self.assertEqual(team_page.team_description, 'The Avengers are a fictional team of superheroes.')
self.assertEqual(team_page.team_user_membership_text, 'You are a member of this team.')
# Verify the new team was added to the topic list
self.teams_page.click_specific_topic("Example Topic")
self.teams_page.verify_topic_team_count(1)
self.teams_page.click_all_topics()
self.teams_page.verify_team_count_in_first_topic(1)
# Verify that if one switches to "My Team" without reloading the page, the newly created team is shown.
self.verify_my_team_count(1)
def test_user_can_cancel_the_team_creation(self):
"""
Scenario: The user should be able to cancel the creation of new team.
......@@ -736,6 +731,7 @@ class CreateTeamTest(TeamsTabBase):
Then I should see the Create Team header and form
When I click Cancel button
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')
......@@ -745,6 +741,11 @@ class CreateTeamTest(TeamsTabBase):
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.teams_page.click_all_topics()
self.teams_page.verify_team_count_in_first_topic(0)
self.verify_my_team_count(0)
@attr('shard_5')
@ddt.ddt
......@@ -882,11 +883,7 @@ class TeamPageTest(TeamsTabBase):
"""
self.assertEqual(
self.team_page.team_capacity_text,
'{num_members} / {max_size} {members_text}'.format(
num_members=num_members,
max_size=max_size,
members_text='Member' if num_members == max_size else 'Members'
)
self.team_page.format_capacity_text(num_members, max_size)
)
self.assertEqual(self.team_page.team_location, 'Afghanistan')
self.assertEqual(self.team_page.team_language, 'Afar')
......@@ -975,6 +972,7 @@ class TeamPageTest(TeamsTabBase):
Then there should be no Join Team button and no message
And I should see the updated information under Team Details
And I should see New Post button
And if I switch to "My Team", the team I have joined is displayed
"""
self._set_team_configuration_and_membership(create_membership=False)
self.team_page.visit()
......@@ -984,6 +982,10 @@ class TeamPageTest(TeamsTabBase):
self.assertFalse(self.team_page.join_team_message_present)
self.assert_team_details(num_members=1, is_member=True)
# Verify that if one switches to "My Team" without reloading the page, the newly created team is shown.
self.teams_page.click_all_topics()
self.verify_my_team_count(1)
def test_already_member_message(self):
"""
Scenario: User should see `You are already in a team` if user is a
......@@ -1037,6 +1039,7 @@ class TeamPageTest(TeamsTabBase):
Then user should be removed from team
And I should see Join Team button
And I should not see New Post button
And if I switch to "My Team", the team I have left is not displayed
"""
self._set_team_configuration_and_membership()
self.team_page.visit()
......@@ -1045,3 +1048,7 @@ class TeamPageTest(TeamsTabBase):
self.team_page.click_leave_team_link()
self.assert_team_details(num_members=0, is_member=False)
self.assertTrue(self.team_page.join_team_button_present)
# Verify that if one switches to "My Team" without reloading the page, the old team no longer shows.
self.teams_page.click_all_topics()
self.verify_my_team_count(0)
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection'],
function(PagingCollection) {
var BaseCollection = PagingCollection.extend({
initialize: function(options) {
PagingCollection.prototype.initialize.call(this);
this.course_id = options.course_id;
this.perPage = options.per_page;
this.teamEvents = options.teamEvents;
this.teamEvents.bind('teams:update', this.onUpdate, this);
this.isStale = false;
},
onUpdate: function(event) {
this.isStale = true;
},
/**
* Refreshes the collection if it has been marked as stale.
* @param force If true, it will always refresh.
* @returns {promise} Returns a promise representing the refresh
*/
refresh: function(force) {
var self = this,
deferred = $.Deferred();
if (force || this.isStale) {
this.fetch()
.done(function() {
self.isStale = false;
deferred.resolve();
});
} else {
deferred.resolve();
}
return deferred.promise();
}
});
return BaseCollection;
});
}).call(this, define || RequireJS.define);
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/team', 'gettext'],
function(PagingCollection, TeamModel, gettext) {
var TeamCollection = PagingCollection.extend({
define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'],
function(BaseCollection, TeamModel, gettext) {
var TeamCollection = BaseCollection.extend({
initialize: function(teams, options) {
PagingCollection.prototype.initialize.call(this);
var self = this;
BaseCollection.prototype.initialize.call(this, options);
this.course_id = options.course_id;
this.server_api['topic_id'] = this.topic_id = options.topic_id;
this.server_api['expand'] = 'user';
this.perPage = options.per_page;
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
this.server_api['order_by'] = function () { return 'name'; }; // TODO surface sort order in UI
delete this.server_api['sort_order']; // Sort order is not specified for the Team API
this.server_api = _.extend(
{
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
},
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'));
......
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/team_membership'],
function(PagingCollection, TeamMembershipModel) {
var TeamMembershipCollection = PagingCollection.extend({
define(['teams/js/collections/base', 'teams/js/models/team_membership'],
function(BaseCollection, TeamMembershipModel) {
var TeamMembershipCollection = BaseCollection.extend({
initialize: function(team_memberships, options) {
PagingCollection.prototype.initialize.call(this);
var self = this;
BaseCollection.prototype.initialize.call(this, options);
this.course_id = options.course_id;
this.perPage = options.per_page || 10;
this.username = options.username;
this.privileged = options.privileged;
this.perPage = options.per_page || 10;
this.server_api['expand'] = 'team';
this.server_api['course_id'] = function () { return encodeURIComponent(options.course_id); };
this.server_api['username'] = this.username;
this.server_api = _.extend(
{
expand: 'team',
username: this.username,
course_id: function () { return encodeURIComponent(self.course_id); }
},
BaseCollection.prototype.server_api
);
delete this.server_api['sort_order']; // Sort order is not specified for the TeamMembership API
delete this.server_api['order_by']; // Order by is not specified for the TeamMembership API
},
......
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/topic', 'gettext'],
function(PagingCollection, TopicModel, gettext) {
var TopicCollection = PagingCollection.extend({
define(['teams/js/collections/base', 'teams/js/models/topic', 'gettext'],
function(BaseCollection, TopicModel, gettext) {
var TopicCollection = BaseCollection.extend({
initialize: function(topics, options) {
PagingCollection.prototype.initialize.call(this);
var self = this;
BaseCollection.prototype.initialize.call(this, options);
this.course_id = options.course_id;
this.perPage = topics.results.length;
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
this.server_api['order_by'] = function () { return this.sortField; };
this.server_api = _.extend(
{
course_id: function () { return encodeURIComponent(self.course_id); },
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'));
......@@ -17,6 +24,10 @@
this.registerSortableField('team_count', gettext('team count'));
},
onUpdate: function(event) {
this.isStale = event.action === 'create';
},
model: TopicModel
});
return TopicCollection;
......
define(['URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic'],
function (URI, _, AjaxHelpers, TopicCollection) {
define(['backbone', 'URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic'],
function (Backbone, URI, _, AjaxHelpers, TopicCollection) {
'use strict';
describe('TopicCollection', function () {
var topicCollection;
......@@ -39,7 +39,11 @@ define(['URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/co
],
"sort_order": "name"
},
{course_id: 'my/course/id', parse: true});
{
teamEvents:_.clone(Backbone.Events),
course_id: 'my/course/id',
parse: true
});
});
var testRequestParam = function (self, param, value) {
......
......@@ -3,13 +3,13 @@ define([
'underscore',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'teams/js/views/edit_team'
], function ($, _, Backbone, AjaxHelpers, TeamEditView) {
'teams/js/views/edit_team',
'teams/js/spec_helpers/team_spec_helpers'
], function ($, _, Backbone, AjaxHelpers, TeamEditView, TeamSpecHelpers) {
'use strict';
describe('EditTeam', function () {
var teamEditView,
teamsUrl = '/api/team/v0/teams/',
var teamsUrl = '/api/team/v0/teams/',
teamsData = {
id: null,
name: "TeamName",
......@@ -22,7 +22,7 @@ define([
language: "a",
membership: []
},
verifyValidation = function (requests, fieldsData) {
verifyValidation = function (requests, teamEditView, fieldsData) {
_.each(fieldsData, function (fieldData) {
teamEditView.$(fieldData[0]).val(fieldData[1]);
});
......@@ -45,24 +45,11 @@ define([
});
expect(requests.length).toBe(0);
},
expectContent = function (selector, text) {
expect(teamEditView.$(selector).text().trim()).toBe(text);
},
verifyDropdownData = function (selector, expectedItems) {
var options = teamEditView.$(selector)[0].options;
var renderedItems = $.map(options, function( elem ) {
return [[elem.value, elem.text]];
});
for (var i = 0; i < expectedItems.length; i++) {
expect(renderedItems).toContain(expectedItems[i]);
}
};
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
spyOn(Backbone.history, 'navigate');
teamEditView = new TeamEditView({
var createTeamEditView = function() {
return new TeamEditView({
teamEvents: TeamSpecHelpers.teamEvents,
el: $('.teams-content'),
teamParams: {
teamsUrl: teamsUrl,
......@@ -73,6 +60,11 @@ define([
countries: [['c', 'ccc'], ['d', 'ddd']]
}
}).render();
};
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
spyOn(Backbone.history, 'navigate');
});
it('can render itself correctly', function () {
......@@ -82,7 +74,8 @@ define([
'.u-field-optional_description',
'.u-field-language',
'.u-field-country'
];
],
teamEditView = createTeamEditView();
_.each(fieldClasses, function (fieldClass) {
expect(teamEditView.$el.find(fieldClass).length).toBe(1);
......@@ -93,7 +86,8 @@ define([
});
it('can create a team', function () {
var requests = AjaxHelpers.requests(this);
var requests = AjaxHelpers.requests(this),
teamEditView = createTeamEditView();
teamEditView.$('.u-field-name input').val(teamsData.name);
teamEditView.$('.u-field-textarea textarea').val(teamsData.description);
......@@ -109,46 +103,49 @@ define([
});
it('shows validation error message when field is empty', function () {
var requests = AjaxHelpers.requests(this);
verifyValidation(requests, [
var requests = AjaxHelpers.requests(this),
teamEditView = createTeamEditView();
verifyValidation(requests, teamEditView, [
['.u-field-name input', 'Name', 'success'],
['.u-field-textarea textarea', '', 'error']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', '', 'error'],
['.u-field-textarea textarea', 'description', 'success']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', '', 'error'],
['.u-field-textarea textarea', '', 'error']
]);
});
it('shows validation error message when field value length exceeded the limit', function () {
var requests = AjaxHelpers.requests(this);
var teamName = new Array(500 + 1).join( '$' );
var teamDescription = new Array(500 + 1).join( '$' );
var requests = AjaxHelpers.requests(this),
teamEditView = createTeamEditView(),
teamName = new Array(500 + 1).join( '$'),
teamDescription = new Array(500 + 1).join( '$' );
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', teamName, 'error'],
['.u-field-textarea textarea', 'description', 'success']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', 'name', 'success'],
['.u-field-textarea textarea', teamDescription, 'error']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', teamName, 'error'],
['.u-field-textarea textarea', teamDescription, 'error']
]);
});
it("shows an error message for HTTP 500", function () {
var requests = AjaxHelpers.requests(this);
var teamEditView = createTeamEditView(),
requests = AjaxHelpers.requests(this);
teamEditView.$('.u-field-name input').val(teamsData.name);
teamEditView.$('.u-field-textarea textarea').val(teamsData.description);
......@@ -182,6 +179,7 @@ define([
});
it("changes route on cancel click", function () {
var teamEditView = createTeamEditView();
teamEditView.$('.create-team.form-actions .action-cancel').click();
expect(Backbone.history.navigate.calls[0].args).toContain('topics/awesomeness');
});
......
......@@ -3,8 +3,9 @@ define([
'teams/js/collections/team',
'teams/js/collections/team_membership',
'teams/js/views/my_teams',
'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, MyTeamsView, TeamSpecHelpers) {
'teams/js/spec_helpers/team_spec_helpers',
'common/js/spec_helpers/ajax_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, MyTeamsView, TeamSpecHelpers, AjaxHelpers) {
'use strict';
describe('My Teams View', function () {
beforeEach(function () {
......@@ -28,26 +29,51 @@ define([
it('can render itself', function () {
var teamMembershipsData = TeamSpecHelpers.createMockTeamMembershipsData(1, 5),
teamMemberships = TeamSpecHelpers.createMockTeamMemberships(teamMembershipsData),
teamsView = createMyTeamsView({
myTeamsView = createMyTeamsView({
teams: teamMemberships,
teamMemberships: teamMemberships
});
TeamSpecHelpers.verifyCards(teamsView, teamMembershipsData);
TeamSpecHelpers.verifyCards(myTeamsView, teamMembershipsData);
// Verify that there is no header or footer
expect(teamsView.$('.teams-paging-header').text().trim()).toBe('');
expect(teamsView.$('.teams-paging-footer').text().trim()).toBe('');
expect(myTeamsView.$('.teams-paging-header').text().trim()).toBe('');
expect(myTeamsView.$('.teams-paging-footer').text().trim()).toBe('');
});
it('shows a message when the user is not a member of any teams', function () {
var teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
teamsView = createMyTeamsView({
myTeamsView = createMyTeamsView({
teams: teamMemberships,
teamMemberships: teamMemberships
});
TeamSpecHelpers.verifyCards(teamsView, []);
expect(teamsView.$el.text().trim()).toBe('You are not currently a member of any teams.');
TeamSpecHelpers.verifyCards(myTeamsView, []);
expect(myTeamsView.$el.text().trim()).toBe('You are not currently a member of any team.');
});
it('refreshes a stale membership collection when rendering', function() {
var requests = AjaxHelpers.requests(this),
teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
myTeamsView = createMyTeamsView({
teams: teamMemberships,
teamMemberships: teamMemberships
});
TeamSpecHelpers.verifyCards(myTeamsView, []);
expect(myTeamsView.$el.text().trim()).toBe('You are not currently a member of any team.');
teamMemberships.teamEvents.trigger('teams:update', { action: 'create' });
myTeamsView.render();
AjaxHelpers.expectJsonRequestURL(
requests,
'foo',
{
expand : 'team',
username : 'testUser',
course_id : 'my/course/id',
page : '1',
page_size : '10'
}
);
AjaxHelpers.respondWithJson(requests, {});
});
});
});
define([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/spec_helpers/team_spec_helpers',
'teams/js/views/team_join'
], function (_, AjaxHelpers, TeamModel, TeamSpecHelpers, TeamJoinView) {
'backbone', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/views/team_join', 'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, _, AjaxHelpers, TeamModel, TeamJoinView, TeamSpecHelpers) {
'use strict';
describe('TeamJoinView', function () {
var createTeamsUrl,
......@@ -63,6 +62,7 @@ define([
var teamJoinView = new TeamJoinView(
{
courseID: TeamSpecHelpers.testCourseID,
teamEvents: TeamSpecHelpers.teamEvents,
model: model,
teamsUrl: createTeamsUrl(teamId),
maxTeamSize: maxTeamSize,
......
......@@ -5,7 +5,9 @@ define([
], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) {
'use strict';
describe('TeamProfileView', function () {
var profileView, createTeamProfileView, createTeamModelData, teamModel,
var profileView, createTeamProfileView, createTeamModelData, clickLeaveTeam,
teamModel,
leaveTeamLinkSelector = '.leave-team-link',
DEFAULT_MEMBERSHIP = [
{
'user': {
......@@ -38,6 +40,7 @@ define([
createTeamProfileView = function(requests, options) {
teamModel = new TeamModel(createTeamModelData(options), { parse: true });
profileView = new TeamProfileView({
teamEvents: TeamSpecHelpers.teamEvents,
courseID: TeamSpecHelpers.testCourseID,
model: teamModel,
maxTeamSize: options.maxTeamSize || 3,
......@@ -71,6 +74,21 @@ define([
return profileView;
};
clickLeaveTeam = function(requests, view) {
expect(view.$(leaveTeamLinkSelector).length).toBe(1);
// click on Leave Team link under Team Details
view.$(leaveTeamLinkSelector).click();
// expect a request to DELETE the team membership
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'api/team/v0/team_membership/test-team,bilbo');
AjaxHelpers.respondWithNoContent(requests);
// expect a request to refetch the user's team memberships
AjaxHelpers.expectJsonRequest(requests, 'GET', '/api/team/v0/teams/test-team');
AjaxHelpers.respondWithJson(requests, createTeamModelData({country: 'US', language: 'en'}));
};
describe('DiscussionsView', function() {
it('can render itself', function () {
var requests = AjaxHelpers.requests(this),
......@@ -84,6 +102,7 @@ define([
expect(view.$('.new-post-btn').length).toEqual(0);
teamModel.set('membership', DEFAULT_MEMBERSHIP); // This should re-render the view.
view.render();
expect(view.$('.new-post-btn').length).toEqual(1);
});
......@@ -92,7 +111,7 @@ define([
view = createTeamProfileView(requests, {membership: DEFAULT_MEMBERSHIP});
expect(view.$('.new-post-btn').length).toEqual(1);
teamModel.set('membership', []);
clickLeaveTeam(requests, view);
expect(view.$('.new-post-btn').length).toEqual(0);
});
});
......@@ -119,7 +138,10 @@ define([
assertTeamDetails(view, 0, false);
expect(view.$('.team-user-membership-status').length).toBe(0);
// Verify that the leave team link is not present.
expect(view.$(leaveTeamLinkSelector).length).toBe(0);
});
it('cannot see the country & language if empty', function() {
var requests = AjaxHelpers.requests(this);
var view = createTeamProfileView(requests, {});
......@@ -145,29 +167,21 @@ define([
// assert user profile page url.
expect(view.$('.member-profile').attr('href')).toBe('/u/bilbo');
//Verify that the leave team link is present
expect(view.$(leaveTeamLinkSelector).text()).toContain('Leave Team');
});
it('can leave team successfully', function() {
var requests = AjaxHelpers.requests(this);
var leaveTeamLinkSelector = '.leave-team-link';
var view = createTeamProfileView(
requests, { country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
);
assertTeamDetails(view, 1, true);
expect(view.$(leaveTeamLinkSelector).length).toBe(1);
// click on Leave Team link under Team Details
view.$(leaveTeamLinkSelector).click();
// response to DELETE
AjaxHelpers.respondWithNoContent(requests);
// response to model fetch request
AjaxHelpers.respondWithJson(requests, createTeamModelData({country: 'US', language: 'en'}));
clickLeaveTeam(requests, view);
assertTeamDetails(view, 0, false);
});
it('shows correct error messages', function () {
var requests = AjaxHelpers.requests(this);
......
......@@ -5,11 +5,8 @@ define(['jquery',
function ($, _, TopicCardView, Topic) {
describe('topic card view', function () {
var view;
beforeEach(function () {
spyOn(TopicCardView.prototype, 'action');
view = new TopicCardView({
var createTopicCardView = function() {
return new TopicCardView({
model: new Topic({
'id': 'renewables',
'name': 'Renewable Energy',
......@@ -17,9 +14,14 @@ define(['jquery',
'team_count': 34
})
});
};
beforeEach(function () {
spyOn(TopicCardView.prototype, 'action');
});
it('can render itself', function () {
var view = createTopicCardView();
expect(view.$el).toHaveClass('square-card');
expect(view.$el.find('.card-title').text()).toContain('Renewable Energy');
expect(view.$el.find('.card-description').text()).toContain('changes in <ⓡⓔⓝⓔⓦⓐⓑⓛⓔ> ʎƃɹǝuǝ');
......@@ -28,6 +30,7 @@ define(['jquery',
});
it('navigates when action button is clicked', function () {
var view = createTopicCardView();
view.$el.find('.action').trigger('click');
// TODO test actual navigation once implemented
expect(view.action).toHaveBeenCalled();
......
......@@ -3,8 +3,9 @@ define([
'teams/js/collections/team',
'teams/js/collections/team_membership',
'teams/js/views/topic_teams',
'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers) {
'teams/js/spec_helpers/team_spec_helpers',
'common/js/spec_helpers/ajax_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers, AjaxHelpers) {
'use strict';
describe('Topic Teams View', function () {
var createTopicTeamsView = function(options) {
......@@ -21,6 +22,24 @@ define([
}).render();
};
var verifyActions = function(teamsView, options) {
if (!options) {
options = {showActions: true};
}
var expectedTitle = 'Are you having trouble finding a team to join?',
expectedMessage = 'Try browsing all teams or searching team descriptions. If you ' +
'still can\'t find a team to join, create a new team in this topic.',
title = teamsView.$('.title').text().trim(),
message = teamsView.$('.copy').text().trim();
if (options.showActions) {
expect(title).toBe(expectedTitle);
expect(message).toBe(expectedMessage);
} else {
expect(title).not.toBe(expectedTitle);
expect(message).not.toBe(expectedMessage);
}
};
beforeEach(function () {
setFixtures('<div class="teams-container"></div>');
});
......@@ -39,12 +58,7 @@ define([
expect(footerEl).not.toHaveClass('hidden');
TeamSpecHelpers.verifyCards(teamsView, testTeamData);
expect(teamsView.$('.title').text()).toBe('Are you having trouble finding a team to join?');
expect(teamsView.$('.copy').text()).toBe(
"Try browsing all teams or searching team descriptions. If you " +
"still can't find a team to join, create a new team in this topic."
);
verifyActions(teamsView);
});
it('can browse all teams', function () {
......@@ -74,9 +88,7 @@ define([
it('does not show actions for a user already in a team', function () {
var teamsView = createTopicTeamsView({});
expect(teamsView.$el.text()).not.toContain(
'Are you having trouble finding a team to join?'
);
verifyActions(teamsView, {showActions: false});
});
it('shows actions for a privileged user already in a team', function () {
......@@ -85,9 +97,32 @@ define([
{ privileged: true }
),
teamsView = createTopicTeamsView({ teamMemberships: staffMembership });
expect(teamsView.$el.text()).toContain(
'Are you having trouble finding a team to join?'
verifyActions(teamsView);
});
/*
// TODO: make this ready for prime time
it('refreshes when the team membership changes', function() {
var requests = AjaxHelpers.requests(this),
teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
teamsView = createTopicTeamsView({ teamMemberships: teamMemberships });
verifyActions(teamsView, {showActions: true});
teamMemberships.teamEvents.trigger('teams:update', { action: 'create' });
teamsView.render();
AjaxHelpers.expectJsonRequestURL(
requests,
'foo',
{
expand : 'team',
username : 'testUser',
course_id : 'my/course/id',
page : '1',
page_size : '10'
}
);
AjaxHelpers.respondWithJson(requests, {});
verifyActions(teamsView, {showActions: false});
});
*/
});
});
define([
'teams/js/collections/topic', 'teams/js/views/topics'
], function (TopicCollection, TopicsView) {
'backbone', 'teams/js/collections/topic', 'teams/js/views/topics',
'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, TopicCollection, TopicsView, TeamSpecHelpers) {
'use strict';
describe('TopicsView', function () {
var initialTopics, topicCollection, topicsView,
var initialTopics, topicCollection, createTopicsView,
generateTopics = function (startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
return {
......@@ -15,6 +16,14 @@ define([
});
};
createTopicsView = function() {
return new TopicsView({
teamEvents: TeamSpecHelpers.teamEvents,
el: '.topics-container',
collection: topicCollection
}).render();
};
beforeEach(function () {
setFixtures('<div class="topics-container"></div>');
initialTopics = generateTopics(1, 5);
......@@ -26,13 +35,17 @@ define([
"start": 0,
"results": initialTopics
},
{course_id: 'my/course/id', parse: true}
{
teamEvents: TeamSpecHelpers.teamEvents,
course_id: 'my/course/id',
parse: true
}
);
topicsView = new TopicsView({el: '.topics-container', collection: topicCollection}).render();
});
it('can render the first of many pages', function () {
var footerEl = topicsView.$('.topics-paging-footer'),
var topicsView = createTopicsView(),
footerEl = topicsView.$('.topics-paging-footer'),
topicCards = topicsView.$('.topic-card');
expect(topicsView.$('.topics-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
_.each(initialTopics, function (topic, index) {
......
define([
'backbone',
'underscore',
'teams/js/collections/team',
'teams/js/collections/team_membership',
], function (_, TeamCollection, TeamMembershipCollection) {
], function (Backbone, _, TeamCollection, TeamMembershipCollection) {
'use strict';
var createMockPostResponse, createMockDiscussionResponse, createAnnotatedContentInfo, createMockThreadResponse,
testCourseID = 'course/1',
testUser = 'testUser',
testTeamDiscussionID = "12345",
teamEvents = _.clone(Backbone.Events),
testCountries = [
['', ''],
['US', 'United States'],
......@@ -47,6 +49,7 @@ define([
results: teamData
},
{
teamEvents: teamEvents,
course_id: 'my/course/id',
parse: true
}
......@@ -79,6 +82,7 @@ define([
results: teamMembershipData
},
_.extend(_.extend({}, {
teamEvents: teamEvents,
course_id: 'my/course/id',
parse: true,
url: 'api/teams/team_memberships',
......@@ -225,6 +229,7 @@ define([
};
return {
teamEvents: teamEvents,
testCourseID: testCourseID,
testUser: testUser,
testCountries: testCountries,
......
......@@ -20,6 +20,7 @@
},
initialize: function(options) {
this.teamEvents = options.teamEvents;
this.courseID = options.teamParams.courseID;
this.topicID = options.teamParams.topicID;
this.collection = options.collection;
......@@ -113,6 +114,10 @@
this.teamModel.save(data, { wait: true })
.done(function(result) {
view.teamEvents.trigger('teams:update', {
action: 'create',
team: result
});
Backbone.history.navigate(
'teams/' + view.topicID + '/' + view.teamModel.id,
{trigger: true}
......
......@@ -5,10 +5,17 @@
function (Backbone, gettext, TeamsView) {
var MyTeamsView = TeamsView.extend({
render: function() {
TeamsView.prototype.render.call(this);
if (this.collection.length === 0) {
this.$el.append('<p>' + gettext('You are not currently a member of any teams.') + '</p>');
var view = this;
if (this.collection.isStale) {
this.$el.html('');
}
this.collection.refresh()
.done(function() {
TeamsView.prototype.render.call(view);
if (view.collection.length === 0) {
view.$el.append('<p>' + gettext('You are not currently a member of any team.') + '</p>');
}
});
return this;
},
......
;(function (define) {
'use strict';
'use strict';
define(['backbone',
define(['backbone',
'underscore',
'gettext',
'teams/js/views/team_utils',
......@@ -18,6 +18,7 @@ define(['backbone',
},
initialize: function(options) {
this.teamEvents = options.teamEvents;
this.template = _.template(teamJoinTemplate);
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
......@@ -28,11 +29,10 @@ define(['backbone',
},
render: function() {
var message,
var view = this,
message,
showButton,
teamHasSpace;
var view = this;
this.getUserTeamInfo(this.currentUsername, view.maxTeamSize).done(function (info) {
teamHasSpace = info.teamHasSpace;
......@@ -59,7 +59,13 @@ define(['backbone',
url: view.teamMembershipsUrl,
data: {'username': view.currentUsername, 'team_id': view.model.get('id')}
}).done(function (data) {
view.model.fetch({});
view.model.fetch()
.done(function() {
view.teamEvents.trigger('teams:update', {
action: 'join',
team: view.model
});
});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
......
......@@ -16,7 +16,7 @@
'click .leave-team-link': 'leaveTeam'
},
initialize: function (options) {
this.listenTo(this.model, "change", this.render);
this.teamEvents = options.teamEvents;
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.requestUsername = options.requestUsername;
......@@ -26,13 +26,13 @@
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages);
this.listenTo(this.model, "change", this.render);
},
render: function () {
var memberships = this.model.get('membership'),
discussionTopicID = this.model.get('discussion_topic_id'),
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername);
this.$el.html(_.template(teamTemplate, {
courseID: this.courseID,
discussionTopicID: discussionTopicID,
......@@ -76,7 +76,13 @@
type: 'DELETE',
url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
}).done(function (data) {
view.model.fetch({});
view.model.fetch()
.done(function() {
view.teamEvents.trigger('teams:update', {
action: 'leave',
team: view.model
});
});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
......
......@@ -81,9 +81,13 @@
router.route.apply(router, route);
});
// Create an event queue to track team changes
this.teamEvents = _.clone(Backbone.Events);
this.teamMemberships = new TeamMembershipCollection(
this.userInfo.team_memberships_data,
{
teamEvents: this.teamEvents,
url: this.teamMembershipsUrl,
course_id: this.courseID,
username: this.userInfo.username,
......@@ -94,6 +98,7 @@
this.myTeamsView = new MyTeamsView({
router: this.router,
teamEvents: this.teamEvents,
collection: this.teamMemberships,
teamMemberships: this.teamMemberships,
maxTeamSize: this.maxTeamSize,
......@@ -107,12 +112,18 @@
this.topicsCollection = new TopicCollection(
this.topics,
{url: options.topicsUrl, course_id: this.courseID, parse: true}
{
teamEvents: this.teamEvents,
url: options.topicsUrl,
course_id: this.courseID,
parse: true
}
).bootstrap();
this.topicsView = new TopicsView({
collection: this.topicsCollection,
router: this.router
router: this.router,
teamEvents: this.teamEvents,
collection: this.topicsCollection
});
this.mainView = this.tabbedView = new ViewWithHeader({
......@@ -196,6 +207,7 @@
})
}),
main: new TeamEditView({
teamEvents: self.teamEvents,
tagName: 'create-new-team',
teamParams: teamsView.main.teamParams,
primaryButtonTitle: 'Create'
......@@ -220,6 +232,7 @@
this.getTopic(topicID)
.done(function(topic) {
var collection = new TeamCollection([], {
teamEvents: self.teamEvents,
course_id: self.courseID,
topic_id: topicID,
url: self.teamsUrl,
......@@ -229,7 +242,7 @@
collection.goTo(1)
.done(function() {
var teamsView = new TopicTeamsView({
router: router,
router: self.router,
topic: topic,
collection: collection,
teamMemberships: self.teamMemberships,
......@@ -278,6 +291,8 @@
self.getTopic(topicID).done(function(topic) {
self.getTeam(teamID, true).done(function(team) {
var view = new TeamProfileView({
teamEvents: self.teamEvents,
router: self.router,
courseID: courseID,
model: team,
maxTeamSize: self.maxTeamSize,
......@@ -287,16 +302,15 @@
languages: self.languages,
teamMembershipDetailUrl: self.teamMembershipDetailUrl
});
var teamJoinView = new TeamJoinView(
{
var teamJoinView = new TeamJoinView({
teamEvents: self.teamEvents,
courseID: courseID,
model: team,
teamsUrl: self.teamsUrl,
maxTeamSize: self.maxTeamSize,
currentUsername: self.userInfo.username,
teamMembershipsUrl: self.teamMembershipsUrl
}
);
});
deferred.resolve(
self.createViewWithHeader(
{
......
......@@ -17,9 +17,14 @@
},
render: function() {
TeamsView.prototype.render.call(this);
if (this.teamMemberships.canUserCreateTeam()) {
var self = this;
$.when(
this.collection.refresh(),
this.teamMemberships.refresh()
).done(function() {
TeamsView.prototype.render.call(self);
if (self.teamMemberships.canUserCreateTeam()) {
var message = interpolate_text(
_.escape(gettext("Try {browse_span_start}browsing all teams{span_end} or {search_span_start}searching team descriptions{span_end}. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")),
{
......@@ -29,8 +34,9 @@
'span_end': '</a>'
}
);
this.$el.append(_.template(teamActionsTemplate, {message: message}));
self.$el.append(_.template(teamActionsTemplate, {message: message}));
}
});
return this;
},
......
;(function (define) {
'use strict';
define([
'gettext',
'teams/js/views/topic_card',
'common/js/components/views/paginated_view'
], function (TopicCardView, PaginatedView) {
], function (gettext, TopicCardView, PaginatedView) {
var TopicsView = PaginatedView.extend({
type: 'topics',
......@@ -18,6 +19,16 @@
srInfo: this.srInfo
});
PaginatedView.prototype.initialize.call(this);
},
render: function() {
var self = this;
this.collection.refresh()
.done(function() {
self.collection.isStale = false;
PaginatedView.prototype.render.call(self);
});
return this;
}
});
return TopicsView;
......
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