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):
"""Verifies that team leave link is 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
def team_invite_section_present(self):
"""Verifies that invite section is present"""
......@@ -334,3 +339,8 @@ class TeamPage(CoursePage, PaginatedUIMixin):
def join_team_message_present(self):
""" Returns True if Join Team message is present else False """
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):
@ddt.ddt
class TeamPageTest(TeamsTabBase):
"""Tests for viewing a specific team"""
SEND_INVITE_TEXT = 'Send this link to friends so that they can join too.'
def setUp(self):
super(TeamPageTest, self).setUp()
self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"}
......@@ -900,10 +903,12 @@ class TeamPageTest(TeamsTabBase):
self.assertTrue(self.team_page.team_leave_link_present)
self.assertTrue(self.team_page.team_invite_section_present)
self.assertEqual(self.team_page.team_invite_help_text, invite_text)
self.assertTrue(self.team_page.new_post_button_present)
else:
self.assertEqual(self.team_page.team_user_membership_text, '')
self.assertFalse(self.team_page.team_leave_link_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):
"""
......@@ -922,7 +927,7 @@ class TeamPageTest(TeamsTabBase):
self.assert_team_details(
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):
......@@ -994,7 +999,7 @@ class TeamPageTest(TeamsTabBase):
self.assert_team_details(
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))
......@@ -1006,9 +1011,11 @@ class TeamPageTest(TeamsTabBase):
and a team belonging to that topic
And I visit the Team page for that team
Then I should see Join Team button
And I should not see New Post button
When I click on Join Team button
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
"""
self._set_team_configuration_and_membership(create_membership=False)
self.team_page.visit()
......@@ -1016,7 +1023,7 @@ class TeamPageTest(TeamsTabBase):
self.team_page.click_join_team_button()
self.assertFalse(self.team_page.join_team_button_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):
"""
......@@ -1055,3 +1062,27 @@ class TeamPageTest(TeamsTabBase):
self.team_page.visit()
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)
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([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/views/team_join', 'teams/js/views/team_profile'
], function (_, AjaxHelpers, TeamModel, TeamJoinView, TeamProfileView) {
'teams/js/views/team_join'
], function (_, AjaxHelpers, TeamModel, TeamJoinView) {
'use strict';
describe('TeamJoinView', function () {
var createTeamsUrl,
createTeamModelData,
createMembershipData,
createJoinView,
verifyErrorMessage,
ACCOUNTS_API_URL = '/api/user/v1/accounts/',
TEAMS_URL = '/api/team/v0/teams/',
TEAMS_MEMBERSHIP_URL = '/api/team/v0/team_membership/';
beforeEach(function () {
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) {
return TEAMS_URL + teamId + '?expand=user';
};
......@@ -148,27 +160,52 @@ define([
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 verifyErrorMessage = function (requests, errorMessage, expectedMessage) {
createJoinView(1, 'ma', createTeamModelData('teamA', 'teamAlpha', []));
AjaxHelpers.respondWithTextError(requests, 400, errorMessage);
expect($('.msg-content .copy').text().trim()).toBe(expectedMessage);
};
// verify user_message
verifyErrorMessage(
requests,
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
verifyErrorMessage(
requests,
JSON.stringify({'user_message': 'Awesome! You got an error.'}),
'Awesome! You got an error.'
JSON.stringify({'user_message': "Can't return user memberships"}),
"Can't return user memberships",
false
);
// verify generic error message
verifyErrorMessage(
requests,
'',
'An error occurred. Try again.'
'An error occurred. Try again.',
false
);
});
});
......
......@@ -60,12 +60,7 @@ define(['backbone',
}).done(function (data) {
view.model.fetch({});
}).fail(function (data) {
try {
var errors = JSON.parse(data.responseText);
view.showMessage(errors.user_message);
} catch (error) {
view.showMessage(view.errorMessage);
}
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
},
......@@ -97,12 +92,7 @@ define(['backbone',
info.teamHasSpace = teamHasSpace;
deferred.resolve(info);
}).fail(function (data) {
try {
var errors = JSON.parse(data.responseText);
view.showMessage(errors.user_message);
} catch (error) {
view.showMessage(view.errorMessage);
}
TeamUtils.parseAndShowMessage(data, view.errorMessage);
deferred.reject();
});
} else {
......@@ -111,12 +101,6 @@ define(['backbone',
}
return deferred.promise();
},
showMessage: function (message) {
$('.wrapper-msg').removeClass('is-hidden');
$('.msg-content .copy').text(message);
$('.wrapper-msg').focus();
}
});
});
......
......@@ -6,21 +6,24 @@
define(['backbone', 'underscore', 'gettext', 'teams/js/views/team_discussion',
'teams/js/views/team_utils',
'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) {
var TeamProfileView = Backbone.View.extend({
errorMessage: gettext("An error occurred. Try again."),
events: {
'click .invite-link-input': 'selectText'
'click .invite-link-input': 'selectText',
'click .leave-team-link': 'leaveTeam'
},
initialize: function (options) {
this.listenTo(this.model, "change", this.render);
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.readOnly = options.readOnly;
this.requestUsername = options.requestUsername;
this.isPrivileged = options.isPrivileged;
this.teamInviteUrl = options.teamInviteUrl;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages);
......@@ -28,16 +31,18 @@
},
render: function () {
var memberships = this.model.get('membership');
var discussionTopicID = this.model.get('discussion_topic_id');
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,
readOnly: this.readOnly,
readOnly: !(this.isPrivileged || isMember),
country: this.countries[this.model.get('country')],
language: this.languages[this.model.get('language')],
membershipText: TeamUtils.teamCapacityText(memberships.length, this.maxTeamSize),
isMember: TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername),
isMember: isMember,
hasCapacity: memberships.length < this.maxTeamSize,
inviteLink: this.teamInviteUrl
......@@ -65,6 +70,19 @@
selectText: function(event) {
event.preventDefault();
$(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 @@
maxMemberCount
),
{memberCount: memberCount, maxMemberCount: maxMemberCount}, true
)
);
},
isUserMemberOfTeam: function(memberships, requestUsername) {
return _.isObject(
_.find(memberships, function(membership)
......@@ -37,8 +38,27 @@
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);
......@@ -53,6 +53,7 @@
this.topicUrl = options.topicUrl;
this.teamsUrl = options.teamsUrl;
this.teamMembershipsUrl = options.teamMembershipsUrl;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.maxTeamSize = options.maxTeamSize;
this.languages = options.languages;
this.countries = options.countries;
......@@ -276,16 +277,16 @@
courseID = this.courseID;
self.getTopic(topicID).done(function(topic) {
self.getTeam(teamID, true).done(function(team) {
var readOnly = self.readOnlyDiscussion(team),
view = new TeamProfileView({
var view = new TeamProfileView({
courseID: courseID,
model: team,
readOnly: readOnly,
maxTeamSize: self.maxTeamSize,
isPrivileged: self.userInfo.privileged,
requestUsername: self.userInfo.username,
countries: self.countries,
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(
{
......@@ -389,11 +390,11 @@
var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null,
self = this,
deferred = $.Deferred(),
teamUrl;
teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': '');
if (team) {
team.url = teamUrl;
deferred.resolve(team);
} else {
teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': '');
team = new TeamModel({
id: teamID,
url: teamUrl
......
......@@ -40,6 +40,7 @@
topicsUrl: '${ topics_url }',
teamsUrl: '${ teams_url }',
teamMembershipsUrl: '${ team_memberships_url }',
teamMembershipDetailUrl: '${ team_membership_detail_url }',
maxTeamSize: ${ course.teams_max_size },
languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) },
countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) },
......
......@@ -108,6 +108,7 @@ class TeamsDashboardView(View):
"topics_url": reverse('topics_list', request=request),
"teams_url": reverse('teams_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,
"countries": list(countries),
"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