Commit b9cf31f4 by Usman Khalid

Merge pull request #9299 from edx/ammar/tnl-1910-support-leaving-a-team

Support leaving a team.
parents 1319602a 5fc61207
...@@ -284,6 +284,11 @@ class TeamPage(CoursePage, PaginatedUIMixin): ...@@ -284,6 +284,11 @@ class TeamPage(CoursePage, PaginatedUIMixin):
"""Verifies that team leave link is present""" """Verifies that team leave link is present"""
return self.q(css='.leave-team-link').present return self.q(css='.leave-team-link').present
def click_leave_team_link(self):
""" Click on Leave Team link"""
self.q(css='.leave-team-link').first.click()
self.wait_for_ajax()
@property @property
def team_invite_section_present(self): def team_invite_section_present(self):
"""Verifies that invite section is present""" """Verifies that invite section is present"""
...@@ -334,3 +339,8 @@ class TeamPage(CoursePage, PaginatedUIMixin): ...@@ -334,3 +339,8 @@ class TeamPage(CoursePage, PaginatedUIMixin):
def join_team_message_present(self): def join_team_message_present(self):
""" Returns True if Join Team message is present else False """ """ Returns True if Join Team message is present else False """
return self.q(css='.join-team .join-team-message').present return self.q(css='.join-team .join-team-message').present
@property
def new_post_button_present(self):
""" Returns True if New Post button is present else False """
return self.q(css='.discussion-module .new-post-btn').present
...@@ -750,6 +750,9 @@ class CreateTeamTest(TeamsTabBase): ...@@ -750,6 +750,9 @@ class CreateTeamTest(TeamsTabBase):
@ddt.ddt @ddt.ddt
class TeamPageTest(TeamsTabBase): class TeamPageTest(TeamsTabBase):
"""Tests for viewing a specific team""" """Tests for viewing a specific team"""
SEND_INVITE_TEXT = 'Send this link to friends so that they can join too.'
def setUp(self): def setUp(self):
super(TeamPageTest, self).setUp() super(TeamPageTest, 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"}
...@@ -900,10 +903,12 @@ class TeamPageTest(TeamsTabBase): ...@@ -900,10 +903,12 @@ class TeamPageTest(TeamsTabBase):
self.assertTrue(self.team_page.team_leave_link_present) self.assertTrue(self.team_page.team_leave_link_present)
self.assertTrue(self.team_page.team_invite_section_present) self.assertTrue(self.team_page.team_invite_section_present)
self.assertEqual(self.team_page.team_invite_help_text, invite_text) self.assertEqual(self.team_page.team_invite_help_text, invite_text)
self.assertTrue(self.team_page.new_post_button_present)
else: else:
self.assertEqual(self.team_page.team_user_membership_text, '') self.assertEqual(self.team_page.team_user_membership_text, '')
self.assertFalse(self.team_page.team_leave_link_present) self.assertFalse(self.team_page.team_leave_link_present)
self.assertFalse(self.team_page.team_invite_section_present) self.assertFalse(self.team_page.team_invite_section_present)
self.assertFalse(self.team_page.new_post_button_present)
def test_team_member_can_see_full_team_details(self): def test_team_member_can_see_full_team_details(self):
""" """
...@@ -922,7 +927,7 @@ class TeamPageTest(TeamsTabBase): ...@@ -922,7 +927,7 @@ class TeamPageTest(TeamsTabBase):
self.assert_team_details( self.assert_team_details(
num_members=1, num_members=1,
invite_text='Send this link to friends so that they can join too.' invite_text=self.SEND_INVITE_TEXT
) )
def test_other_users_can_see_limited_team_details(self): def test_other_users_can_see_limited_team_details(self):
...@@ -994,7 +999,7 @@ class TeamPageTest(TeamsTabBase): ...@@ -994,7 +999,7 @@ class TeamPageTest(TeamsTabBase):
self.assert_team_details( self.assert_team_details(
num_members=1, num_members=1,
invite_text='Send this link to friends so that they can join too.' invite_text=self.SEND_INVITE_TEXT
) )
self.assertEqual(self.team_page.team_invite_url, '{0}?invite=true'.format(self.team_page.url)) self.assertEqual(self.team_page.team_invite_url, '{0}?invite=true'.format(self.team_page.url))
...@@ -1006,9 +1011,11 @@ class TeamPageTest(TeamsTabBase): ...@@ -1006,9 +1011,11 @@ class TeamPageTest(TeamsTabBase):
and a team belonging to that topic and a team belonging to that topic
And I visit the Team page for that team And I visit the Team page for that team
Then I should see Join Team button Then I should see Join Team button
And I should not see New Post button
When I click on Join Team button When I click on Join Team button
Then there should be no Join Team button and no message 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 the updated information under Team Details
And I should see New Post button
""" """
self._set_team_configuration_and_membership(create_membership=False) self._set_team_configuration_and_membership(create_membership=False)
self.team_page.visit() self.team_page.visit()
...@@ -1016,7 +1023,7 @@ class TeamPageTest(TeamsTabBase): ...@@ -1016,7 +1023,7 @@ class TeamPageTest(TeamsTabBase):
self.team_page.click_join_team_button() self.team_page.click_join_team_button()
self.assertFalse(self.team_page.join_team_button_present) self.assertFalse(self.team_page.join_team_button_present)
self.assertFalse(self.team_page.join_team_message_present) self.assertFalse(self.team_page.join_team_message_present)
self.assert_team_details(num_members=1, invite_text='Send this link to friends so that they can join too.') self.assert_team_details(num_members=1, is_member=True, invite_text=self.SEND_INVITE_TEXT)
def test_already_member_message(self): def test_already_member_message(self):
""" """
...@@ -1055,3 +1062,27 @@ class TeamPageTest(TeamsTabBase): ...@@ -1055,3 +1062,27 @@ class TeamPageTest(TeamsTabBase):
self.team_page.visit() self.team_page.visit()
self.assertEqual(self.team_page.join_team_message, 'This team is full.') self.assertEqual(self.team_page.join_team_message, 'This team is full.')
self.assert_team_details(num_members=1, is_member=False, max_size=1) self.assert_team_details(num_members=1, is_member=False, max_size=1)
def test_leave_team(self):
"""
Scenario: User can leave a team.
Given I am enrolled in a course with a team configuration, a topic,
and a team belonging to that topic
And I am a member of team
And I visit the team
And I should not see Join Team button
And I should see New Post button
Then I should see Leave Team link
When I click on Leave Team link
Then user should be removed from team
And I should see Join Team button
And I should not see New Post button
"""
self._set_team_configuration_and_membership()
self.team_page.visit()
self.assertFalse(self.team_page.join_team_button_present)
self.assert_team_details(num_members=1, invite_text=self.SEND_INVITE_TEXT)
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)
define([ define([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/views/team_join', 'teams/js/views/team_profile' 'teams/js/views/team_join'
], function (_, AjaxHelpers, TeamModel, TeamJoinView, TeamProfileView) { ], function (_, AjaxHelpers, TeamModel, TeamJoinView) {
'use strict'; 'use strict';
describe('TeamJoinView', function () { describe('TeamJoinView', function () {
var createTeamsUrl, var createTeamsUrl,
createTeamModelData, createTeamModelData,
createMembershipData, createMembershipData,
createJoinView, createJoinView,
verifyErrorMessage,
ACCOUNTS_API_URL = '/api/user/v1/accounts/', ACCOUNTS_API_URL = '/api/user/v1/accounts/',
TEAMS_URL = '/api/team/v0/teams/', TEAMS_URL = '/api/team/v0/teams/',
TEAMS_MEMBERSHIP_URL = '/api/team/v0/team_membership/'; TEAMS_MEMBERSHIP_URL = '/api/team/v0/team_membership/';
beforeEach(function () { beforeEach(function () {
setFixtures( setFixtures(
'<div class="msg-content"><div class="copy"></div></div><div class="header-action-view"></div>' '<div class="teams-content"><div class="msg-content"><div class="copy"></div></div><div class="header-action-view"></div></div>'
); );
}); });
verifyErrorMessage = function (requests, errorMessage, expectedMessage, joinTeam) {
var view = createJoinView(1, 'ma', createTeamModelData('teamA', 'teamAlpha', []));
if (joinTeam) {
// if we want the error to return when user try to join team, respond with no membership
AjaxHelpers.respondWithJson(requests, {"count": 0});
view.$('.action.action-primary').click();
}
AjaxHelpers.respondWithTextError(requests, 400, errorMessage);
expect($('.msg-content .copy').text().trim()).toBe(expectedMessage);
};
createTeamsUrl = function (teamId) { createTeamsUrl = function (teamId) {
return TEAMS_URL + teamId + '?expand=user'; return TEAMS_URL + teamId + '?expand=user';
}; };
...@@ -148,27 +160,52 @@ define([ ...@@ -148,27 +160,52 @@ define([
expect(requests.length).toBe(0); expect(requests.length).toBe(0);
}); });
it('shows correct error messages', function () { it('shows correct error message if user fails to join team', function () {
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
var verifyErrorMessage = function (requests, errorMessage, expectedMessage) { // verify user_message
createJoinView(1, 'ma', createTeamModelData('teamA', 'teamAlpha', [])); verifyErrorMessage(
AjaxHelpers.respondWithTextError(requests, 400, errorMessage); requests,
expect($('.msg-content .copy').text().trim()).toBe(expectedMessage); JSON.stringify({'user_message': "Can't be made member"}),
}; "Can't be made member",
true
);
// verify generic error message
verifyErrorMessage(
requests,
'',
'An error occurred. Try again.',
true
);
// verify error message when json parsing succeeded but error message format is incorrect
verifyErrorMessage(
requests,
JSON.stringify({'blah': "Can't be made member"}),
'An error occurred. Try again.',
true
);
});
it('shows correct error message if initializing the view fails', function () {
// Rendering the view sometimes require fetching user's memberships. This may fail.
var requests = AjaxHelpers.requests(this);
// verify user_message // verify user_message
verifyErrorMessage( verifyErrorMessage(
requests, requests,
JSON.stringify({'user_message': 'Awesome! You got an error.'}), JSON.stringify({'user_message': "Can't return user memberships"}),
'Awesome! You got an error.' "Can't return user memberships",
false
); );
// verify generic error message // verify generic error message
verifyErrorMessage( verifyErrorMessage(
requests, requests,
'', '',
'An error occurred. Try again.' 'An error occurred. Try again.',
false
); );
}); });
}); });
......
...@@ -60,12 +60,7 @@ define(['backbone', ...@@ -60,12 +60,7 @@ define(['backbone',
}).done(function (data) { }).done(function (data) {
view.model.fetch({}); view.model.fetch({});
}).fail(function (data) { }).fail(function (data) {
try { TeamUtils.parseAndShowMessage(data, view.errorMessage);
var errors = JSON.parse(data.responseText);
view.showMessage(errors.user_message);
} catch (error) {
view.showMessage(view.errorMessage);
}
}); });
}, },
...@@ -97,12 +92,7 @@ define(['backbone', ...@@ -97,12 +92,7 @@ define(['backbone',
info.teamHasSpace = teamHasSpace; info.teamHasSpace = teamHasSpace;
deferred.resolve(info); deferred.resolve(info);
}).fail(function (data) { }).fail(function (data) {
try { TeamUtils.parseAndShowMessage(data, view.errorMessage);
var errors = JSON.parse(data.responseText);
view.showMessage(errors.user_message);
} catch (error) {
view.showMessage(view.errorMessage);
}
deferred.reject(); deferred.reject();
}); });
} else { } else {
...@@ -111,12 +101,6 @@ define(['backbone', ...@@ -111,12 +101,6 @@ define(['backbone',
} }
return deferred.promise(); return deferred.promise();
},
showMessage: function (message) {
$('.wrapper-msg').removeClass('is-hidden');
$('.msg-content .copy').text(message);
$('.wrapper-msg').focus();
} }
}); });
}); });
......
...@@ -6,21 +6,24 @@ ...@@ -6,21 +6,24 @@
define(['backbone', 'underscore', 'gettext', 'teams/js/views/team_discussion', define(['backbone', 'underscore', 'gettext', 'teams/js/views/team_discussion',
'teams/js/views/team_utils', 'teams/js/views/team_utils',
'text!teams/templates/team-profile.underscore', 'text!teams/templates/team-profile.underscore',
'text!teams/templates/team-member.underscore' 'text!teams/templates/team-member.underscore'],
],
function (Backbone, _, gettext, TeamDiscussionView, TeamUtils, teamTemplate, teamMemberTemplate) { function (Backbone, _, gettext, TeamDiscussionView, TeamUtils, teamTemplate, teamMemberTemplate) {
var TeamProfileView = Backbone.View.extend({ var TeamProfileView = Backbone.View.extend({
errorMessage: gettext("An error occurred. Try again."),
events: { events: {
'click .invite-link-input': 'selectText' 'click .invite-link-input': 'selectText',
'click .leave-team-link': 'leaveTeam'
}, },
initialize: function (options) { initialize: function (options) {
this.listenTo(this.model, "change", this.render); this.listenTo(this.model, "change", this.render);
this.courseID = options.courseID; this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize; this.maxTeamSize = options.maxTeamSize;
this.readOnly = options.readOnly;
this.requestUsername = options.requestUsername; this.requestUsername = options.requestUsername;
this.isPrivileged = options.isPrivileged;
this.teamInviteUrl = options.teamInviteUrl; this.teamInviteUrl = options.teamInviteUrl;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries); this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages); this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages);
...@@ -28,16 +31,18 @@ ...@@ -28,16 +31,18 @@
}, },
render: function () { render: function () {
var memberships = this.model.get('membership'); var memberships = this.model.get('membership'),
var discussionTopicID = this.model.get('discussion_topic_id'); discussionTopicID = this.model.get('discussion_topic_id'),
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername);
this.$el.html(_.template(teamTemplate, { this.$el.html(_.template(teamTemplate, {
courseID: this.courseID, courseID: this.courseID,
discussionTopicID: discussionTopicID, discussionTopicID: discussionTopicID,
readOnly: this.readOnly, readOnly: !(this.isPrivileged || isMember),
country: this.countries[this.model.get('country')], country: this.countries[this.model.get('country')],
language: this.languages[this.model.get('language')], language: this.languages[this.model.get('language')],
membershipText: TeamUtils.teamCapacityText(memberships.length, this.maxTeamSize), membershipText: TeamUtils.teamCapacityText(memberships.length, this.maxTeamSize),
isMember: TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername), isMember: isMember,
hasCapacity: memberships.length < this.maxTeamSize, hasCapacity: memberships.length < this.maxTeamSize,
inviteLink: this.teamInviteUrl inviteLink: this.teamInviteUrl
...@@ -65,6 +70,19 @@ ...@@ -65,6 +70,19 @@
selectText: function(event) { selectText: function(event) {
event.preventDefault(); event.preventDefault();
$(event.currentTarget).select(); $(event.currentTarget).select();
},
leaveTeam: function (event) {
event.preventDefault();
var view = this;
$.ajax({
type: 'DELETE',
url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
}).done(function (data) {
view.model.fetch({});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
} }
}); });
......
...@@ -28,8 +28,9 @@ ...@@ -28,8 +28,9 @@
maxMemberCount maxMemberCount
), ),
{memberCount: memberCount, maxMemberCount: maxMemberCount}, true {memberCount: memberCount, maxMemberCount: maxMemberCount}, true
) );
}, },
isUserMemberOfTeam: function(memberships, requestUsername) { isUserMemberOfTeam: function(memberships, requestUsername) {
return _.isObject( return _.isObject(
_.find(memberships, function(membership) _.find(memberships, function(membership)
...@@ -37,8 +38,27 @@ ...@@ -37,8 +38,27 @@
return membership.user.username === requestUsername; return membership.user.username === requestUsername;
}) })
); );
},
showMessage: function (message) {
var messageElement = $('.teams-content .wrapper-msg');
messageElement.removeClass('is-hidden');
$('.teams-content .msg-content .copy').text(message);
messageElement.focus();
},
/**
* Parse `data` and show user message. If parsing fails than show `genericErrorMessage`
*/
parseAndShowMessage: function (data, genericErrorMessage) {
try {
var errors = JSON.parse(data.responseText);
this.showMessage(_.isUndefined(errors.user_message) ? genericErrorMessage : errors.user_message);
} catch (error) {
this.showMessage(genericErrorMessage);
}
} }
} };
}); });
}).call(this, define || RequireJS.define); }).call(this, define || RequireJS.define);
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
this.topicUrl = options.topicUrl; this.topicUrl = options.topicUrl;
this.teamsUrl = options.teamsUrl; this.teamsUrl = options.teamsUrl;
this.teamMembershipsUrl = options.teamMembershipsUrl; this.teamMembershipsUrl = options.teamMembershipsUrl;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.maxTeamSize = options.maxTeamSize; this.maxTeamSize = options.maxTeamSize;
this.languages = options.languages; this.languages = options.languages;
this.countries = options.countries; this.countries = options.countries;
...@@ -276,16 +277,16 @@ ...@@ -276,16 +277,16 @@
courseID = this.courseID; courseID = this.courseID;
self.getTopic(topicID).done(function(topic) { self.getTopic(topicID).done(function(topic) {
self.getTeam(teamID, true).done(function(team) { self.getTeam(teamID, true).done(function(team) {
var readOnly = self.readOnlyDiscussion(team), var view = new TeamProfileView({
view = new TeamProfileView({
courseID: courseID, courseID: courseID,
model: team, model: team,
readOnly: readOnly,
maxTeamSize: self.maxTeamSize, maxTeamSize: self.maxTeamSize,
isPrivileged: self.userInfo.privileged,
requestUsername: self.userInfo.username, requestUsername: self.userInfo.username,
countries: self.countries, countries: self.countries,
languages: self.languages, languages: self.languages,
teamInviteUrl: self.teamsBaseUrl + '#teams/' + topicID + '/' + teamID + '?invite=true' teamInviteUrl: self.teamsBaseUrl + '#teams/' + topicID + '/' + teamID + '?invite=true',
teamMembershipDetailUrl: self.teamMembershipDetailUrl
}); });
var teamJoinView = new TeamJoinView( var teamJoinView = new TeamJoinView(
{ {
...@@ -389,11 +390,11 @@ ...@@ -389,11 +390,11 @@
var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null, var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null,
self = this, self = this,
deferred = $.Deferred(), deferred = $.Deferred(),
teamUrl; teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': '');
if (team) { if (team) {
team.url = teamUrl;
deferred.resolve(team); deferred.resolve(team);
} else { } else {
teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': '');
team = new TeamModel({ team = new TeamModel({
id: teamID, id: teamID,
url: teamUrl url: teamUrl
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
topicsUrl: '${ topics_url }', topicsUrl: '${ topics_url }',
teamsUrl: '${ teams_url }', teamsUrl: '${ teams_url }',
teamMembershipsUrl: '${ team_memberships_url }', teamMembershipsUrl: '${ team_memberships_url }',
teamMembershipDetailUrl: '${ team_membership_detail_url }',
maxTeamSize: ${ course.teams_max_size }, maxTeamSize: ${ course.teams_max_size },
languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) }, languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) },
countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) }, countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) },
......
...@@ -108,6 +108,7 @@ class TeamsDashboardView(View): ...@@ -108,6 +108,7 @@ class TeamsDashboardView(View):
"topics_url": reverse('topics_list', request=request), "topics_url": reverse('topics_list', request=request),
"teams_url": reverse('teams_list', request=request), "teams_url": reverse('teams_list', request=request),
"team_memberships_url": reverse('team_membership_list', request=request), "team_memberships_url": reverse('team_membership_list', request=request),
"team_membership_detail_url": reverse('team_membership_detail', args=['team_id', user.username]),
"languages": settings.ALL_LANGUAGES, "languages": settings.ALL_LANGUAGES,
"countries": list(countries), "countries": list(countries),
"disable_courseware_js": True, "disable_courseware_js": True,
......
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