From 5fc612079b8635f8532eef7dc79bb62578e1bff7 Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Tue, 11 Aug 2015 12:15:55 +0500 Subject: [PATCH] Support leaving a team TNL-1910 --- common/test/acceptance/pages/lms/teams.py | 10 ++ .../test/acceptance/tests/lms/test_teams.py | 37 ++++- .../teams/js/spec/views/team_join_spec.js | 63 ++++++-- .../teams/js/spec/views/team_profile_spec.js | 153 +++++++++++++----- .../teams/static/teams/js/views/team_join.js | 20 +-- .../static/teams/js/views/team_profile.js | 34 +++- .../teams/static/teams/js/views/team_utils.js | 24 ++- .../teams/static/teams/js/views/teams_tab.js | 13 +- .../teams/templates/teams/teams.html | 1 + lms/djangoapps/teams/views.py | 1 + 10 files changed, 265 insertions(+), 91 deletions(-) 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,