Commit 3662fbc1 by cahrens Committed by Eric Fischer

Edit Membership functionality and tests

TNL-1913
parent 0c3be8e1
...@@ -317,9 +317,7 @@ class TeamManagementPage(CoursePage, FieldsMixin, BreadcrumbsMixin): ...@@ -317,9 +317,7 @@ class TeamManagementPage(CoursePage, FieldsMixin, BreadcrumbsMixin):
def is_browser_on_page(self): def is_browser_on_page(self):
"""Check if we're on the create team page for a particular topic.""" """Check if we're on the create team page for a particular topic."""
has_correct_url = self.url.endswith(self.url_path) return self.q(css='.team-edit-fields').present
teams_create_view_present = self.q(css='.team-edit-fields').present
return has_correct_url and teams_create_view_present
@property @property
def header_page_name(self): def header_page_name(self):
...@@ -351,6 +349,63 @@ class TeamManagementPage(CoursePage, FieldsMixin, BreadcrumbsMixin): ...@@ -351,6 +349,63 @@ class TeamManagementPage(CoursePage, FieldsMixin, BreadcrumbsMixin):
"""Returns the 'delete team' button.""" """Returns the 'delete team' button."""
return self.q(css='.action-delete').first return self.q(css='.action-delete').first
def click_membership_button(self):
"""Clicks the 'edit membership' button"""
self.q(css='.action-edit-members').first.click()
self.wait_for_ajax()
@property
def membership_button_present(self):
"""Checks if the edit membership button is present"""
return self.q(css='.action-edit-members').present
class EditMembershipPage(CoursePage):
"""
Staff or discussion-privileged user page to remove troublesome or inactive
students from a team
"""
def __init__(self, browser, course_id, team):
"""
Set up `self.url_path` on instantiation, since it dynamically
reflects the current team.
"""
super(EditMembershipPage, self).__init__(browser, course_id)
self.team = team
self.url_path = "teams/#teams/{topic_id}/{team_id}/edit-team/manage-members".format(
topic_id=self.team['topic_id'], team_id=self.team['id']
)
def is_browser_on_page(self):
"""Check if we're on the team membership page for a particular team."""
self.wait_for_ajax()
if self.q(css='.edit-members').present:
return True
empty_query = self.q(css='.teams-main>.page-content>p').first
return (
len(empty_query.results) > 0 and
empty_query[0].text == "This team does not have any members."
)
@property
def team_members(self):
"""Returns the number of team members shown on the page."""
return len(self.q(css='.team-member'))
def click_first_remove(self):
"""Clicks the remove link on the first member listed."""
self.q(css='.action-remove-member').first.click()
def confirm_delete_membership_dialog(self):
"""Click 'delete' on the warning dialog."""
confirm_prompt(self, require_notification=False)
self.wait_for_ajax()
def cancel_delete_membership_dialog(self):
"""Click 'delete' on the warning dialog."""
confirm_prompt(self, cancel=True)
class TeamPage(CoursePage, PaginatedUIMixin, BreadcrumbsMixin): class TeamPage(CoursePage, PaginatedUIMixin, BreadcrumbsMixin):
""" """
......
...@@ -28,6 +28,7 @@ from ...pages.lms.teams import ( ...@@ -28,6 +28,7 @@ from ...pages.lms.teams import (
BrowseTopicsPage, BrowseTopicsPage,
BrowseTeamsPage, BrowseTeamsPage,
TeamManagementPage, TeamManagementPage,
EditMembershipPage,
TeamPage TeamPage
) )
from ...pages.common.utils import confirm_prompt from ...pages.common.utils import confirm_prompt
...@@ -207,7 +208,7 @@ class TeamsTabTest(TeamsTabBase): ...@@ -207,7 +208,7 @@ class TeamsTabTest(TeamsTabBase):
@ddt.data( @ddt.data(
'topics/{topic_id}', 'topics/{topic_id}',
'topics/{topic_id}/search', 'topics/{topic_id}/search',
'topics/{topic_id}/{team_id}/edit-team', 'teams/{topic_id}/{team_id}/edit-team',
'teams/{topic_id}/{team_id}' 'teams/{topic_id}/{team_id}'
) )
def test_unauthorized_error_message(self, route): def test_unauthorized_error_message(self, route):
...@@ -217,10 +218,10 @@ class TeamsTabTest(TeamsTabBase): ...@@ -217,10 +218,10 @@ class TeamsTabTest(TeamsTabBase):
""" """
topics = self.create_topics(1) topics = self.create_topics(1)
topic = topics[0] topic = topics[0]
self.set_team_configuration({ self.set_team_configuration(
u'max_team_size': 10, {u'max_team_size': 10, u'topics': topics},
u'topics': topics global_staff=True
}) )
team = self.create_teams(topic, 1)[0] team = self.create_teams(topic, 1)[0]
self.teams_page.visit() self.teams_page.visit()
self.browser.delete_cookie('sessionid') self.browser.delete_cookie('sessionid')
...@@ -1424,6 +1425,90 @@ class EditTeamTest(TeamFormActions): ...@@ -1424,6 +1425,90 @@ class EditTeamTest(TeamFormActions):
self.verify_and_navigate_to_edit_team_page() self.verify_and_navigate_to_edit_team_page()
@ddt.ddt
class EditMembershipTest(TeamFormActions):
"""
Tests for administrating from the team membership page
"""
def setUp(self):
super(EditMembershipTest, self).setUp()
self.set_team_configuration(
{'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]},
global_staff=True
)
self.team_management_page = TeamManagementPage(self.browser, self.course_id, self.topic)
self.team = self.create_teams(self.topic, num_teams=1)[0]
#make sure a user exists on this team so we can edit the membership
self.create_membership(self.user_info['username'], self.team['id'])
self.edit_membership_page = EditMembershipPage(self.browser, self.course_id, self.team)
self.team_page = TeamPage(self.browser, self.course_id, team=self.team)
def edit_membership_helper(self, role, cancel=False):
""" Helper for common functionality in edit membership tests """
if role is not None:
AutoAuthPage(
self.browser,
course_id=self.course_id,
staff=False,
roles=role
).visit()
self.team_page.visit()
self.team_page.click_edit_team_button()
self.team_management_page.wait_for_page()
self.assertTrue(
self.team_management_page.membership_button_present
)
self.team_management_page.click_membership_button()
self.edit_membership_page.wait_for_page()
self.edit_membership_page.click_first_remove()
if cancel:
self.edit_membership_page.cancel_delete_membership_dialog()
self.assertEqual(self.edit_membership_page.team_members, 1)
else:
self.edit_membership_page.confirm_delete_membership_dialog()
self.assertEqual(self.edit_membership_page.team_members, 0)
self.assertTrue(self.edit_membership_page.is_browser_on_page)
@ddt.data('Moderator', 'Community TA', 'Administrator', None)
def test_remove_membership(self, role):
"""
Scenario: The user should be able to remove a membership
Given I am staff user for a course with a team
When I visit the Team profile page
Then I should see the Edit Team button
And When I click edit team button
Then I should see the Edit Membership button
And When I click the edit membership button
Then I should see the edit membership page
And When I click the remove button and confirm the dialog
Then my membership should be removed, and I should remain on the page
"""
self.edit_membership_helper(role, cancel=False)
@ddt.data('Moderator', 'Community TA', 'Administrator', None)
def test_cancel_remove_membership(self, role):
"""
Scenario: The user should be able to remove a membership
Given I am staff user for a course with a team
When I visit the Team profile page
Then I should see the Edit Team button
And When I click edit team button
Then I should see the Edit Membership button
And When I click the edit membership button
Then I should see the edit membership page
And When I click the remove button and cancel the dialog
Then my membership should not be removed, and I should remain on the page
"""
self.edit_membership_helper(role, cancel=True)
@attr('shard_5') @attr('shard_5')
@ddt.ddt @ddt.ddt
class TeamPageTest(TeamsTabBase): class TeamPageTest(TeamsTabBase):
......
define([
'jquery',
'underscore',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'teams/js/views/edit_team_members',
'teams/js/models/team',
'teams/js/views/team_utils',
'teams/js/spec_helpers/team_spec_helpers'
], function ($, _, Backbone, AjaxHelpers, TeamEditMembershipView, TeamModel, TeamUtils, TeamSpecHelpers) {
'use strict';
describe('CreateEditTeam', function() {
var editTeamID = 'av',
DEFAULT_MEMBERSHIP = [
{
'user': {
'username': 'frodo',
'profile_image': {
'has_image': true,
'image_url_medium': '/frodo-image-url'
},
},
last_activity_at: "2015-08-21T18:53:01.145Z",
date_joined: "2014-01-01T18:53:01.145Z"
}
],
deleteTeamMemember = function (view, confirm) {
view.$('.action-remove-member').click();
// Confirm delete dialog
if (confirm) {
$('.action-primary').click();
}
else {
$('.action-secondary').click();
}
},
verifyTeamMembersView = function (view) {
expect(view.$('.team-member').length).toEqual(1);
expect(view.$('.member-profile').attr('href')).toEqual('/u/frodo');
expect(view.$('img.image-url').attr('src')).toEqual('/frodo-image-url');
expect(view.$('.member-info-container .primary').text()).toBe('frodo');
expect(view.$el.find('#last-active abbr').attr('title')).toEqual("2015-08-21T18:53:01.145Z");
expect(view.$el.find('#date-joined abbr').attr('title')).toEqual("2014-01-01T18:53:01.145Z");
},
verifyNoMembersView = function (view){
expect(view.$el.text().trim()).toBe('This team does not have any members.');
},
createTeamModelData = function (membership) {
return {
id: editTeamID,
name: 'Avengers',
description: 'Team of dumbs',
language: 'en',
country: 'US',
membership: membership,
url: '/api/team/v0/teams/' + editTeamID
};
},
createEditTeamMembersView = function (membership) {
var teamModel = new TeamModel(
createTeamModelData(membership),
{ parse: true }
);
return new TeamEditMembershipView({
teamEvents: TeamSpecHelpers.teamEvents,
el: $('.teams-content'),
model: teamModel,
context: TeamSpecHelpers.testContext
}).render();
};
beforeEach(function () {
setFixtures('<div id="page-prompt"></div><div class="teams-content"></div>');
spyOn(Backbone.history, 'navigate');
spyOn(TeamUtils, 'showMessage');
});
it('can render a message when there are no members', function () {
var view = createEditTeamMembersView([]);
verifyNoMembersView(view);
});
it('can delete a team member and update the view', function () {
var requests = AjaxHelpers.requests(this),
view = createEditTeamMembersView(DEFAULT_MEMBERSHIP);
spyOn(view.teamEvents, 'trigger');
verifyTeamMembersView(view);
deleteTeamMemember(view, true);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/api/team/v0/team_membership/av,frodo', null);
AjaxHelpers.respondWithNoContent(requests);
expect(view.teamEvents.trigger).toHaveBeenCalledWith(
'teams:update', {
action: 'leave',
team: view.model
}
);
AjaxHelpers.expectJsonRequest(requests, 'GET', view.model.get('url'));
AjaxHelpers.respondWithJson(requests, createTeamModelData([]));
verifyNoMembersView(view);
});
it('can show an error message if removing the user fails', function () {
var requests = AjaxHelpers.requests(this),
view = createEditTeamMembersView(DEFAULT_MEMBERSHIP);
spyOn(view.teamEvents, 'trigger');
verifyTeamMembersView(view);
deleteTeamMemember(view, true);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/api/team/v0/team_membership/av,frodo', null);
AjaxHelpers.respondWithError(requests);
expect(TeamUtils.showMessage).toHaveBeenCalledWith(
'An error occurred while removing the member from the team. Try again.',
undefined
);
expect(view.teamEvents.trigger).not.toHaveBeenCalled();
verifyTeamMembersView(view);
});
it('can cancel team membership deletion', function () {
var requests = AjaxHelpers.requests(this);
var view = createEditTeamMembersView(DEFAULT_MEMBERSHIP);
spyOn(view.teamEvents, 'trigger');
verifyTeamMembersView(view);
deleteTeamMemember(view, false);
expect(requests.length).toBe(0);
expect(view.teamEvents.trigger).not.toHaveBeenCalled();
verifyTeamMembersView(view);
});
});
});
...@@ -15,7 +15,7 @@ define([ ...@@ -15,7 +15,7 @@ define([
createInstructorTools = function () { createInstructorTools = function () {
return new InstructorToolsView({ return new InstructorToolsView({
team: new Team(TeamSpecHelpers.createMockTeamData(1, 1)[0]), team: new Team(TeamSpecHelpers.createMockTeamData(1, 1)[0]),
teamEvents: TeamSpecHelpers.teamEvents, teamEvents: TeamSpecHelpers.teamEvents
}); });
}, },
deleteTeam = function (view, confirm) { deleteTeam = function (view, confirm) {
...@@ -79,6 +79,14 @@ define([ ...@@ -79,6 +79,14 @@ define([
deleteTeam(view, true); deleteTeam(view, true);
AjaxHelpers.respondWithError(requests, 404); AjaxHelpers.respondWithError(requests, 404);
expectSuccessMessage(view.team); expectSuccessMessage(view.team);
});
it('can trigger the edit membership view', function () {
view.$('.action-edit-members').click();
expect(Backbone.history.navigate).toHaveBeenCalledWith(
'teams/' + view.team.get('topic_id') + "/" + view.team.id + "/edit-team/manage-members",
{trigger: true}
);
}); });
}); });
}); });
...@@ -37,6 +37,7 @@ define([ ...@@ -37,6 +37,7 @@ define([
country: testCountries[i%4][0], country: testCountries[i%4][0],
membership: [], membership: [],
last_activity_at: '', last_activity_at: '',
topic_id: 'topic_id' + i,
url: 'api/team/v0/teams/' + id url: 'api/team/v0/teams/' + id
}; };
}); });
......
;(function (define) {
'use strict';
define(['backbone',
'jquery',
'underscore',
'gettext',
'teams/js/models/team',
'teams/js/views/team_utils',
'common/js/components/utils/view_utils',
'text!teams/templates/edit-team-member.underscore',
'text!teams/templates/date.underscore'
],
function (Backbone, $, _, gettext, TeamModel, TeamUtils, ViewUtils, editTeamMemberTemplate, dateTemplate) {
return Backbone.View.extend({
dateTemplate: _.template(dateTemplate),
teamMemberTemplate: _.template(editTeamMemberTemplate),
errorMessage: gettext("An error occurred while removing the member from the team. Try again."),
events: {
'click .action-remove-member': 'removeMember'
},
initialize: function(options) {
this.teamMembershipDetailUrl = options.context.teamMembershipDetailUrl;
// The URL ends with team_id,request_username. We want to replace
// the last occurrence of team_id with the actual team_id, and remove request_username
// as the actual user to be removed from the team will be added on before calling DELETE.
this.teamMembershipDetailUrl = this.teamMembershipDetailUrl.substring(
0, this.teamMembershipDetailUrl.lastIndexOf('team_id')
) + this.model.get('id') + ",";
this.teamEvents = options.teamEvents;
},
render: function() {
if (this.model.get('membership').length === 0) {
this.$el.html('<p>' + gettext('This team does not have any members.') + '</p>');
}
else {
this.$el.html('<ul class="edit-members"></ul>');
this.renderTeamMembers();
}
return this;
},
renderTeamMembers: function() {
var self = this, dateJoined, lastActivity;
_.each(this.model.get('membership'), function(membership) {
dateJoined = interpolate(
// Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
gettext("Joined %(date)s"),
{date: self.dateTemplate({date: membership.date_joined})},
true
);
lastActivity = interpolate(
// Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
gettext("Last Activity %(date)s"),
{date: self.dateTemplate({date: membership.last_activity_at})},
true
);
// It is assumed that the team member array is automatically in the order of date joined.
self.$('.edit-members').append(self.teamMemberTemplate({
imageUrl: membership.user.profile_image.image_url_medium,
username: membership.user.username,
memberProfileUrl: '/u/' + membership.user.username,
dateJoined: dateJoined,
lastActive: lastActivity
}));
});
this.$('abbr').timeago();
},
removeMember: function (event) {
var self = this, username = $(event.currentTarget).data('username');
event.preventDefault();
ViewUtils.confirmThenRunOperation(
gettext('Remove this team member?'),
gettext('This learner will be removed from the team, allowing another learner to take the available spot.'),
gettext('Remove'),
function () {
$.ajax({
type: 'DELETE',
url: self.teamMembershipDetailUrl + username
}).done(function () {
self.teamEvents.trigger('teams:update', {
action: 'leave',
team: self.model
});
self.model.fetch().done(function() { self.render(); });
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, self.errorMessage);
});
}
);
}
});
});
}).call(this, define || RequireJS.define);
...@@ -38,8 +38,10 @@ ...@@ -38,8 +38,10 @@
editMembership: function (event) { editMembership: function (event) {
event.preventDefault(); event.preventDefault();
alert("You clicked the button!"); Backbone.history.navigate(
//placeholder; will route to remove team member page 'teams/' + this.team.get('topic_id') + '/' + this.team.id +'/edit-team/manage-members',
{trigger: true}
);
}, },
handleDelete: function () { handleDelete: function () {
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
'teams/js/views/team_utils', 'teams/js/views/team_utils',
'text!teams/templates/team-membership-details.underscore', 'text!teams/templates/team-membership-details.underscore',
'text!teams/templates/team-country-language.underscore', 'text!teams/templates/team-country-language.underscore',
'text!teams/templates/team-activity.underscore' 'text!teams/templates/date.underscore'
], function ( ], function (
Backbone, Backbone,
_, _,
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
TeamUtils, TeamUtils,
teamMembershipDetailsTemplate, teamMembershipDetailsTemplate,
teamCountryLanguageTemplate, teamCountryLanguageTemplate,
teamActivityTemplate dateTemplate
) { ) {
var TeamMembershipView, TeamCountryLanguageView, TeamActivityView, TeamCardView; var TeamMembershipView, TeamCountryLanguageView, TeamActivityView, TeamCardView;
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
TeamActivityView = Backbone.View.extend({ TeamActivityView = Backbone.View.extend({
tagName: 'div', tagName: 'div',
className: 'team-activity', className: 'team-activity',
template: _.template(teamActivityTemplate), template: _.template(dateTemplate),
initialize: function (options) { initialize: function (options) {
this.date = options.date; this.date = options.date;
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
'use strict'; 'use strict';
define(['backbone', define(['backbone',
'jquery',
'underscore', 'underscore',
'gettext', 'gettext',
'teams/js/views/team_utils', 'teams/js/views/team_utils',
'text!teams/templates/team-profile-header-actions.underscore'], 'text!teams/templates/team-profile-header-actions.underscore'],
function (Backbone, _, gettext, TeamUtils, teamProfileHeaderActionsTemplate) { function (Backbone, $, _, gettext, TeamUtils, teamProfileHeaderActionsTemplate) {
return Backbone.View.extend({ return Backbone.View.extend({
errorMessage: gettext("An error occurred. Try again."), errorMessage: gettext("An error occurred. Try again."),
...@@ -56,8 +57,10 @@ ...@@ -56,8 +57,10 @@
return view; return view;
}, },
joinTeam: function () { joinTeam: function (event) {
var view = this; var view = this;
event.preventDefault();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: view.context.teamMembershipsUrl, url: view.context.teamMembershipsUrl,
...@@ -117,7 +120,7 @@ ...@@ -117,7 +120,7 @@
editTeam: function (event) { editTeam: function (event) {
event.preventDefault(); event.preventDefault();
Backbone.history.navigate( Backbone.history.navigate(
'topics/' + this.topic.id + '/' + this.model.get('id') +'/edit-team', 'teams/' + this.topic.id + '/' + this.model.get('id') +'/edit-team',
{trigger: true} {trigger: true}
); );
} }
......
/* Team utility methods*/ /* Team utility methods*/
;(function (define) { ;(function (define) {
'use strict'; 'use strict';
define([ define(["jquery", "underscore"
], function () { ], function ($, _) {
return { return {
/** /**
......
<li class="team-member">
<a class="member-profile" href="<%= memberProfileUrl %>">
<p class="tooltip-custom"><%= username %></p>
<img class="image-url" src="<%= imageUrl %>" alt="profile page" />
</a>
<span class="date-joined"><%= dateJoined %></span>
<span>|</span>
<span class="last-active"><%= lastActive %></span>
<a class="action-remove-member" data-username="<%= username %>" href=""><%- gettext("Remove") %></a>
</li>
...@@ -702,6 +702,7 @@ ...@@ -702,6 +702,7 @@
'lms/include/teams/js/spec/collections/topic_collection_spec.js', 'lms/include/teams/js/spec/collections/topic_collection_spec.js',
'lms/include/teams/js/spec/teams_tab_factory_spec.js', 'lms/include/teams/js/spec/teams_tab_factory_spec.js',
'lms/include/teams/js/spec/views/edit_team_spec.js', 'lms/include/teams/js/spec/views/edit_team_spec.js',
'lms/include/teams/js/spec/views/edit_team_members_spec.js',
'lms/include/teams/js/spec/views/instructor_tools_spec.js', 'lms/include/teams/js/spec/views/instructor_tools_spec.js',
'lms/include/teams/js/spec/views/my_teams_spec.js', 'lms/include/teams/js/spec/views/my_teams_spec.js',
'lms/include/teams/js/spec/views/team_card_spec.js', 'lms/include/teams/js/spec/views/team_card_spec.js',
......
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