diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py
index daeeb6e3c4..3446a3a915 100644
--- a/common/test/acceptance/pages/lms/teams.py
+++ b/common/test/acceptance/pages/lms/teams.py
@@ -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
diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py
index cc57aaeadf..eaedb1a2dc 100644
--- a/common/test/acceptance/tests/lms/test_teams.py
+++ b/common/test/acceptance/tests/lms/test_teams.py
@@ -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)
diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_join_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_join_spec.js
index 2ca1afdebe..62fed60abc 100644
--- a/lms/djangoapps/teams/static/teams/js/spec/views/team_join_spec.js
+++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_join_spec.js
@@ -1,23 +1,35 @@
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(
- '
'
+ ''
);
});
+ 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': 'Awesome! You got an error.'}),
- 'Awesome! You got an error.'
+ 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.'
+ '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': "Can't return user memberships"}),
+ "Can't return user memberships",
+ false
+ );
+
+ // verify generic error message
+ verifyErrorMessage(
+ requests,
+ '',
+ 'An error occurred. Try again.',
+ false
);
});
});
diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js
index 449e3e2dd8..b178fbf4af 100644
--- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js
+++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js
@@ -5,27 +5,41 @@ define([
], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) {
'use strict';
describe('TeamProfileView', function () {
- var profileView, createTeamProfileView;
+ var profileView, createTeamProfileView, createTeamModelData, teamModel,
+ DEFAULT_MEMBERSHIP = [
+ {
+ 'user': {
+ 'username': 'bilbo',
+ 'profile_image': {
+ 'has_image': true,
+ 'image_url_medium': '/image-url'
+ }
+ }
+ }
+ ];
beforeEach(function () {
+ setFixtures('');
DiscussionSpecHelper.setUnderscoreFixtures();
});
+ createTeamModelData = function (options) {
+ return {
+ id: "test-team",
+ name: "Test Team",
+ discussion_topic_id: TeamSpecHelpers.testTeamDiscussionID,
+ country: options.country || '',
+ language: options.language || '',
+ membership: options.membership || [],
+ url: '/api/team/v0/teams/test-team'
+ };
+ };
+
createTeamProfileView = function(requests, options) {
- var model = new TeamModel(
- {
- id: "test-team",
- name: "Test Team",
- discussion_topic_id: TeamSpecHelpers.testTeamDiscussionID,
- country: options.country || '',
- language: options.language || '',
- membership: options.membership || []
- },
- { parse: true }
- );
+ teamModel = new TeamModel(createTeamModelData(options), { parse: true });
profileView = new TeamProfileView({
courseID: TeamSpecHelpers.testCourseID,
- model: model,
+ model: teamModel,
maxTeamSize: options.maxTeamSize || 3,
requestUsername: 'bilbo',
countries : [
@@ -37,7 +51,8 @@ define([
['', ''],
['en', 'English'],
['fr', 'French']
- ]
+ ],
+ teamMembershipDetailUrl: 'api/team/v0/team_membership/team_id,bilbo'
});
profileView.render();
AjaxHelpers.expectRequest(
@@ -62,16 +77,35 @@ define([
view = createTeamProfileView(requests, {});
expect(view.$('.discussion-thread').length).toEqual(3);
});
+
+ it('shows New Post button when user joins a team', function () {
+ var requests = AjaxHelpers.requests(this),
+ view = createTeamProfileView(requests, {});
+
+ expect(view.$('.new-post-btn').length).toEqual(0);
+ teamModel.set('membership', DEFAULT_MEMBERSHIP); // This should re-render the view.
+ expect(view.$('.new-post-btn').length).toEqual(1);
+ });
+
+ it('hides New Post button when user left a team', function () {
+ var requests = AjaxHelpers.requests(this),
+ view = createTeamProfileView(requests, {membership: DEFAULT_MEMBERSHIP});
+
+ expect(view.$('.new-post-btn').length).toEqual(1);
+ teamModel.set('membership', []);
+ expect(view.$('.new-post-btn').length).toEqual(0);
+ });
});
describe('TeamDetailsView', function() {
- var assertTeamDetails = function(view, members) {
+ var assertTeamDetails = function(view, members, memberOfTeam) {
expect(view.$('.team-detail-header').text()).toBe('Team Details');
expect(view.$('.team-country').text()).toContain('United States');
expect(view.$('.team-language').text()).toContain('English');
expect(view.$('.team-capacity').text()).toContain(members + ' / 3 Members');
expect(view.$('.team-member').length).toBe(members);
+ expect(Boolean(view.$('.leave-team-link').length)).toBe(memberOfTeam);
};
describe('Non-Member', function() {
@@ -82,7 +116,7 @@ define([
country: 'US',
language: 'en'
});
- assertTeamDetails(view, 0);
+ assertTeamDetails(view, 0, false);
expect(view.$('.team-user-membership-status').length).toBe(0);
// Verify that invite and leave team sections are not present.
@@ -105,17 +139,9 @@ define([
var view = createTeamProfileView(requests, {
country: 'US',
language: 'en',
- membership: [{
- 'user': {
- 'username': 'bilbo',
- 'profile_image': {
- 'has_image': true,
- 'image_url_medium': '/image-url'
- }
- }
- }]
+ membership: DEFAULT_MEMBERSHIP
});
- assertTeamDetails(view, 1);
+ assertTeamDetails(view, 1, true);
expect(view.$('.team-user-membership-status').text().trim()).toBe('You are a member of this team.');
// assert tooltip text.
@@ -164,7 +190,7 @@ define([
}]
});
- assertTeamDetails(view, 3);
+ assertTeamDetails(view, 3, true);
expect(view.$('.invite-header').text()).toContain('Invite Others');
expect(view.$('.invite-text').text()).toContain('No invitations are available. This team is full.');
expect(view.$('.invite-link-input').length).toBe(0);
@@ -173,26 +199,71 @@ define([
var requests = AjaxHelpers.requests(this);
spyOn(TeamProfileView.prototype, 'selectText');
- var view = createTeamProfileView(requests, {
- country: 'US',
- language: 'en',
- membership: [{
- 'user': {
- 'username': 'bilbo',
- 'profile_image': {
- 'has_image': true,
- 'image_url_medium': '/image-url'
- }
- }
- }]
- });
- assertTeamDetails(view, 1);
+ var view = createTeamProfileView(
+ requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
+ );
+ assertTeamDetails(view, 1, true);
expect(view.$('.invite-link-input').length).toBe(1);
view.$('.invite-link-input').click();
expect(view.selectText).toHaveBeenCalled();
});
+ 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}
+ );
+ 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'}));
+
+ assertTeamDetails(view, 0, false);
+ });
+ it('shows correct error messages', function () {
+ var requests = AjaxHelpers.requests(this);
+
+ var verifyErrorMessage = function (requests, errorMessage, expectedMessage) {
+ var view = createTeamProfileView(
+ requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
+ );
+ view.$('.leave-team-link').click();
+ AjaxHelpers.respondWithTextError(requests, 400, errorMessage);
+ expect($('.msg-content .copy').text().trim()).toBe(expectedMessage);
+ };
+
+ // verify user_message
+ verifyErrorMessage(
+ requests,
+ JSON.stringify({'user_message': "can't remove user from team"}),
+ "can't remove user from team"
+ );
+
+ // verify generic error message
+ verifyErrorMessage(
+ requests,
+ '',
+ 'An error occurred. Try again.'
+ );
+
+ // verify error message when json parsing succeeded but error message format is incorrect
+ verifyErrorMessage(
+ requests,
+ JSON.stringify({'blah': "can't remove user from team"}),
+ 'An error occurred. Try again.'
+ );
+ });
});
});
});
diff --git a/lms/djangoapps/teams/static/teams/js/views/team_join.js b/lms/djangoapps/teams/static/teams/js/views/team_join.js
index 213ba0cf5d..7b5af9b219 100644
--- a/lms/djangoapps/teams/static/teams/js/views/team_join.js
+++ b/lms/djangoapps/teams/static/teams/js/views/team_join.js
@@ -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();
}
});
});
diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js
index f6ee371f76..b08810a7f9 100644
--- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js
+++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js
@@ -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);
+ });
}
});
diff --git a/lms/djangoapps/teams/static/teams/js/views/team_utils.js b/lms/djangoapps/teams/static/teams/js/views/team_utils.js
index 5623e73d8d..cdf4014c78 100644
--- a/lms/djangoapps/teams/static/teams/js/views/team_utils.js
+++ b/lms/djangoapps/teams/static/teams/js/views/team_utils.js
@@ -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);
diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js
index c43038d1a7..d2c3c1b5bb 100644
--- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js
+++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js
@@ -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
diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html
index d3e57db3af..1ad546763e 100644
--- a/lms/djangoapps/teams/templates/teams/teams.html
+++ b/lms/djangoapps/teams/templates/teams/teams.html
@@ -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) },
diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py
index c742db51c2..d767f5ac16 100644
--- a/lms/djangoapps/teams/views.py
+++ b/lms/djangoapps/teams/views.py
@@ -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,