Student can NOT modify membership status with team in instructor managed topic (#22286)

* no student change to instructor managed team

* Address comments & fix unit tests

* fix jenkins test failure

* Fix eslint errors

* Fix xss warnings

* Fix bokchoy test
This commit is contained in:
Alex Wang
2019-11-25 19:59:51 -05:00
committed by GitHub
parent 2113f41b44
commit d044ed28b5
36 changed files with 353 additions and 152 deletions

View File

@@ -62,6 +62,15 @@ def is_team_discussion_private(team):
return getattr(team, 'is_discussion_private', False)
def is_instructor_managed_team(team): # pylint: disable=unused-argument
"""
Return true if the team is managed by instructors.
For now always return false, will complete the logic later.
TODO MST-25
"""
return False
def user_is_a_team_member(user, team):
"""
Return if the user is a member of the team
@@ -189,3 +198,7 @@ def add_team_count(topics, course_id, organization_protection_status):
topics_to_team_count = {d['topic_id']: d['team_count'] for d in teams_per_topic}
for topic in topics:
topic['team_count'] = topics_to_team_count.get(topic['id'], 0)
def can_user_modify_team(user, course_key, team):
return not is_instructor_managed_team(team) or _has_course_staff_privileges(user, course_key)

View File

@@ -175,6 +175,7 @@ class BaseTopicSerializer(serializers.Serializer): # pylint: disable=abstract-m
description = serializers.CharField()
name = serializers.CharField()
id = serializers.CharField() # pylint: disable=invalid-name
type = serializers.CharField()
class TopicSerializer(BaseTopicSerializer): # pylint: disable=abstract-method

View File

@@ -21,17 +21,17 @@
parse: function(response, options) {
if (!response) {
response = {};
response = {}; // eslint-disable-line no-param-reassign
}
if (!response.results) {
response.results = [];
response.results = []; // eslint-disable-line no-param-reassign
}
return PagingCollection.prototype.parse.call(this, response, options);
},
onUpdate: function(event) {
onUpdate: function(event) { // eslint-disable-line no-unused-vars
// Mark the collection as stale so that it knows to refresh when needed.
this.isStale = true;
},
@@ -40,10 +40,11 @@
// remove when backbone.paginator gets a new release
sync: function(method, model, options) {
// do not send total pages and total records in request
var params;
if (method === 'read') {
var params = _.values(_.pick(this.queryParams, ['totalPages', 'totalRecords']));
params = _.values(_.pick(this.queryParams, ['totalPages', 'totalRecords']));
_.each(params, function(param) {
delete options.data[param];
delete options.data[param]; // eslint-disable-line no-param-reassign
});
}

View File

@@ -1,7 +1,7 @@
(function(define) {
'use strict';
define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'],
function(BaseCollection, TeamModel, gettext) {
define(['teams/js/collections/base', 'teams/js/models/team', 'gettext', 'underscore'],
function(BaseCollection, TeamModel, gettext, _) {
var TeamCollection = BaseCollection.extend({
model: TeamModel,

View File

@@ -19,7 +19,7 @@
this.state.sortKey = topics.sort_order;
}
options.perPage = topics.results.length;
options.perPage = topics.results.length; // eslint-disable-line no-param-reassign
BaseCollection.prototype.constructor.call(this, topics, options);
this.registerSortableField('name', gettext('name'));

View File

@@ -13,7 +13,7 @@
},
parse: function(response) {
response.team = new TeamModel(response.team);
response.team = new TeamModel(response.team); // eslint-disable-line no-param-reassign
return response;
}
});

View File

@@ -3,12 +3,12 @@ define(['backbone', 'URI', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/a
function(Backbone, URI, _, AjaxHelpers, TeamSpecHelpers) {
'use strict';
describe('TopicCollection', function() {
var topicCollection;
var topicCollection, testRequestParam;
beforeEach(function() {
topicCollection = TeamSpecHelpers.createMockTopicCollection();
});
var testRequestParam = function(self, param, value) {
testRequestParam = function(self, param, value) {
var requests = AjaxHelpers.requests(self),
request,
url,

View File

@@ -9,6 +9,13 @@ define([
'teams/js/spec_helpers/team_spec_helpers'
], function($, _, Backbone, AjaxHelpers, PageHelpers, TeamEditView, TeamModel, TeamSpecHelpers) {
'use strict';
var assertFormRendersCorrectly,
assertTeamCreateUpdateInfo,
assertValidationMessagesWhenFieldsEmpty,
assertValidationMessagesWhenInvalidData,
assertShowMessageOnError,
assertRedirectsToCorrectUrlOnCancel,
requestMethod;
describe('CreateEditTeam', function() {
var teamsUrl = '/api/team/v0/teams/',
@@ -31,17 +38,18 @@ define([
language: 'en'
},
verifyValidation = function(requests, teamEditView, fieldsData) {
var message = teamEditView.$('.wrapper-msg');
var actionMessage = (
// eslint-disable-next-line no-use-before-define
teamAction === 'create' ? 'Your team could not be created.' : 'Your team could not be updated.'
);
_.each(fieldsData, function(fieldData) {
teamEditView.$(fieldData[0]).val(fieldData[1]);
});
teamEditView.$('.create-team.form-actions .action-primary').click();
var message = teamEditView.$('.wrapper-msg');
expect(message.hasClass('is-hidden')).toBeFalsy();
var actionMessage = (
teamAction === 'create' ? 'Your team could not be created.' : 'Your team could not be updated.'
);
expect(message.find('.title').text().trim()).toBe(actionMessage);
expect(message.find('.copy').text().trim()).toBe(
'Check the highlighted fields below and try again.'
@@ -95,7 +103,7 @@ define([
spyOn(Backbone.history, 'navigate');
});
var assertFormRendersCorrectly = function() {
assertFormRendersCorrectly = function() {
var fieldClasses = [
'.u-field-name',
'.u-field-description',
@@ -120,11 +128,11 @@ define([
}
};
var requestMethod = function() {
requestMethod = function() {
return teamAction === 'create' ? 'POST' : 'PATCH';
};
var assertTeamCreateUpdateInfo = function(that, teamsData, teamsUrl, expectedUrl) {
assertTeamCreateUpdateInfo = function(that, teamsData, teamsUrlLink, expectedUrl) {
var requests = AjaxHelpers.requests(that),
teamEditView = createEditTeamView();
@@ -135,14 +143,14 @@ define([
teamEditView.$('.create-team.form-actions .action-primary').click();
AjaxHelpers.expectJsonRequest(requests, requestMethod(), teamsUrl, teamsData);
AjaxHelpers.expectJsonRequest(requests, requestMethod(), teamsUrlLink, teamsData);
AjaxHelpers.respondWithJson(requests, _.extend({}, teamsData, teamAction === 'create' ? {id: '123'} : {}));
expect(teamEditView.$('.create-team.wrapper-msg .copy').text().trim().length).toBe(0);
expect(Backbone.history.navigate.calls.mostRecent().args[0]).toBe(expectedUrl);
};
var assertValidationMessagesWhenFieldsEmpty = function(that) {
assertValidationMessagesWhenFieldsEmpty = function(that) {
var requests = AjaxHelpers.requests(that),
teamEditView = createEditTeamView();
@@ -163,7 +171,7 @@ define([
]);
};
var assertValidationMessagesWhenInvalidData = function(that) {
assertValidationMessagesWhenInvalidData = function(that) {
var requests = AjaxHelpers.requests(that),
teamEditView = createEditTeamView(),
teamName = new Array(500 + 1).join('$'),
@@ -185,7 +193,7 @@ define([
]);
};
var assertShowMessageOnError = function(that, teamsData, teamsUrl, errorCode) {
assertShowMessageOnError = function(that, teamsData, teamsUrlLink, errorCode) {
var teamEditView = createEditTeamView(),
requests = AjaxHelpers.requests(that);
@@ -195,10 +203,10 @@ define([
teamEditView.$('.create-team.form-actions .action-primary').click();
if (teamAction === 'create') {
teamsData.country = '';
teamsData.language = '';
teamsData.country = ''; // eslint-disable-line no-param-reassign
teamsData.language = ''; // eslint-disable-line no-param-reassign
}
AjaxHelpers.expectJsonRequest(requests, requestMethod(), teamsUrl, teamsData);
AjaxHelpers.expectJsonRequest(requests, requestMethod(), teamsUrlLink, teamsData);
if (errorCode < 500) {
AjaxHelpers.respondWithError(
@@ -213,7 +221,7 @@ define([
}
};
var assertRedirectsToCorrectUrlOnCancel = function(expectedUrl) {
assertRedirectsToCorrectUrlOnCancel = function(expectedUrl) {
var teamEditView = createEditTeamView();
teamEditView.$('.create-team.form-actions .action-cancel').click();
expect(Backbone.history.navigate.calls.mostRecent().args[0]).toBe(expectedUrl);

View File

@@ -19,7 +19,7 @@ define([
teamEvents: TeamSpecHelpers.teamEvents
});
},
deleteTeam = function(view, confirm) {
deleteTeam = function(view, confirm) { // eslint-disable-line no-shadow
view.$('.action-delete').click();
// Confirm delete dialog
if (confirm) {

View File

@@ -6,12 +6,13 @@ define([
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
], function(Backbone, MyTeamsCollection, MyTeamsView, TeamSpecHelpers, AjaxHelpers) {
'use strict';
var createMyTeamsView;
describe('My Teams View', function() {
beforeEach(function() {
setFixtures('<div class="teams-container"></div>');
});
var createMyTeamsView = function(myTeams) {
createMyTeamsView = function(myTeams) {
return new MyTeamsView({
el: '.teams-container',
collection: myTeams,

View File

@@ -88,7 +88,7 @@ define(['jquery',
expectThumbnailsOrder = function(members) {
var thumbnails = view.$('.item-member-thumb img');
expect(thumbnails.length).toBe(members.length);
thumbnails.each(function(index, imgEl) {
thumbnails.each(function(index) {
expect(thumbnails.eq(index).attr('alt')).toBe(members[index].username);
expect(thumbnails.eq(index).attr('src')).toBe(members[index].image_url);
});

View File

@@ -25,26 +25,29 @@ define([
};
};
createHeaderActionsView = function(requests, maxTeamSize, currentUsername, teamModelData, showEditButton) {
var model = new TeamModel(teamModelData, {parse: true}),
context = TeamSpecHelpers.createMockContext({
maxTeamSize: maxTeamSize,
userInfo: TeamSpecHelpers.createMockUserInfo({
username: currentUsername
})
});
createHeaderActionsView =
function(requests, maxTeamSize, currentUsername, teamModelData, showEditButton, isInstructorManagedTopic) {
var model = new TeamModel(teamModelData, {parse: true}),
context = TeamSpecHelpers.createMockContext({
maxTeamSize: maxTeamSize,
userInfo: TeamSpecHelpers.createMockUserInfo({
username: currentUsername
})
});
return new TeamProfileHeaderActionsView(
{
courseID: TeamSpecHelpers.testCourseID,
teamEvents: TeamSpecHelpers.teamEvents,
context: context,
model: model,
topic: TeamSpecHelpers.createMockTopic(),
showEditButton: showEditButton
}
).render();
};
return new TeamProfileHeaderActionsView(
{
courseID: TeamSpecHelpers.testCourseID,
teamEvents: TeamSpecHelpers.teamEvents,
context: context,
model: model,
topic: isInstructorManagedTopic ?
TeamSpecHelpers.createMockInstructorManagedTopic() :
TeamSpecHelpers.createMockTopic(),
showEditButton: showEditButton
}
).render();
};
createMembershipData = function(username) {
return [
@@ -60,7 +63,12 @@ define([
describe('JoinButton', function() {
beforeEach(function() {
setFixtures(
'<div class="teams-content"><div class="msg-content"><div class="copy"></div></div><div class="header-action-view"></div></div>'
'<div class="teams-content">\n' +
'<div class="msg-content">\n' +
'<div class="copy"></div>\n' +
'</div>\n' +
'<div class="header-action-view"></div>\n' +
'</div>'
);
});
@@ -134,7 +142,9 @@ define([
it('shows already member message', function() {
var requests = AjaxHelpers.requests(this);
var currentUsername = 'ma1';
var view = createHeaderActionsView(requests, 1, currentUsername, createTeamModelData('teamA', 'teamAlpha', []));
var view =
createHeaderActionsView(
requests, 1, currentUsername, createTeamModelData('teamA', 'teamAlpha', []));
// a get request will be sent to get user membership info
// because current user is not member of current team
@@ -170,6 +180,36 @@ define([
AjaxHelpers.expectNoRequests(requests);
});
it('shows not join instructor managed team message', function() {
var requests = AjaxHelpers.requests(this);
var currentUsername = 'ma1';
var view = createHeaderActionsView(
requests,
1,
currentUsername,
createTeamModelData('teamA', 'teamAlpha', []),
false,
true);
// a get request will be sent to get user membership info
// because current user is not member of current team
AjaxHelpers.expectRequest(
requests,
'GET',
TeamSpecHelpers.testContext.teamMembershipsUrl + '?' + $.param({
username: currentUsername, course_id: TeamSpecHelpers.testCourseID
})
);
// Mock the response so that current user is not a member of any team
AjaxHelpers.respondWithJson(requests, {count: 0});
// current user is a student and current team belogs to an instructor managed topic
// so the Join Team button is hidden and we should see the correct message
expect(view.$('.action.action-primary').length).toEqual(0);
expect(view.$('.join-team-message').text().trim()).toBe(view.notJoinInstructorManagedTeam);
});
it('shows correct error message if user fails to join team', function() {
var requests = AjaxHelpers.requests(this);
@@ -241,10 +281,11 @@ define([
});
it('can navigate to correct url', function() {
var requests = AjaxHelpers.requests(this);
var requests = AjaxHelpers.requests(this),
editButton;
spyOn(Backbone.history, 'navigate');
createAndAssertView(requests, true);
var editButton = view.$('.action-edit-team');
editButton = view.$('.action-edit-team');
expect(editButton.length).toEqual(1);
$(editButton).click();

View File

@@ -42,7 +42,7 @@ define([
};
};
createTeamProfileView = function(requests, options) {
createTeamProfileView = function(requests, options, isInstructorManagedTopic) {
teamModel = new TeamModel(createTeamModelData(options), {parse: true});
profileView = new TeamProfileView({
el: $('.profile-view'),
@@ -50,6 +50,9 @@ define([
courseID: TeamSpecHelpers.testCourseID,
context: TeamSpecHelpers.testContext,
model: teamModel,
topic: isInstructorManagedTopic ?
TeamSpecHelpers.createMockInstructorManagedTopic() :
TeamSpecHelpers.createMockTopic(),
setFocusToHeaderFunc: function() {
$('.teams-content').focus();
}
@@ -58,7 +61,7 @@ define([
AjaxHelpers.expectRequest(
requests,
'GET',
interpolate(
interpolate( // eslint-disable-line no-undef
'/courses/%(courseID)s/discussion/forum/%(topicID)s/inline' +
'?page=1&sort_key=activity&sort_order=desc&ajax=1',
{
@@ -186,6 +189,16 @@ define([
assertTeamDetails(view, 0, false);
});
it('student can not leave instructor managed team', function() {
var requests = AjaxHelpers.requests(this);
var view = createTeamProfileView(
requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}, true
);
// When a student is in a team of an instructor-managed topic, he can't see the leave team button.
assertTeamDetails(view, 1, false);
});
it("wouldn't do anything if user click on Cancel button on dialog", function() {
var requests = AjaxHelpers.requests(this);

View File

@@ -5,12 +5,13 @@ define([
'teams/js/spec_helpers/team_spec_helpers'
], function(Backbone, TeamCollection, TeamsView, TeamSpecHelpers) {
'use strict';
var createTeamsView;
describe('Teams View', function() {
beforeEach(function() {
setFixtures('<div class="teams-container"></div>');
});
var createTeamsView = function(options) {
createTeamsView = function(options) {
return new TeamsView({
el: '.teams-container',
collection: options.teams || TeamSpecHelpers.createMockTeams(),
@@ -26,11 +27,10 @@ define([
results: testTeamData
})
});
var footerEl = teamsView.$('.teams-paging-footer');
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
var footerEl = teamsView.$('.teams-paging-footer');
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2');
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2'); // eslint-disable-line no-useless-escape
expect(footerEl).not.toHaveClass('hidden');
TeamSpecHelpers.verifyCards(teamsView, testTeamData);

View File

@@ -6,8 +6,9 @@ define([
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/page_helpers',
'teams/js/views/teams_tab',
'teams/js/spec_helpers/team_spec_helpers'
], function($, Backbone, Logger, SpecHelpers, AjaxHelpers, PageHelpers, TeamsTabView, TeamSpecHelpers) {
'teams/js/spec_helpers/team_spec_helpers',
'underscore'
], function($, Backbone, Logger, SpecHelpers, AjaxHelpers, PageHelpers, TeamsTabView, TeamSpecHelpers, _) {
'use strict';
describe('TeamsTab', function() {
@@ -170,7 +171,7 @@ define([
}
],
'fires a page view event for the edit team page': [
'teams/' + TeamSpecHelpers.testTopicID + '/' + 'test_team_id/edit-team',
'teams/' + TeamSpecHelpers.testTopicID + '/test_team_id/edit-team',
{
page_name: 'edit-team',
topic_id: TeamSpecHelpers.testTopicID,
@@ -230,17 +231,17 @@ define([
});
describe('Search', function() {
var performSearch = function(requests, teamsTabView) {
var performSearch = function(reqs, teamsTabView) {
teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click();
verifyTeamsRequest({
order_by: '',
text_search: 'foo'
});
AjaxHelpers.respondWithJson(requests, TeamSpecHelpers.createMockTeamsResponse({results: []}));
AjaxHelpers.respondWithJson(reqs, TeamSpecHelpers.createMockTeamsResponse({results: []}));
// Expect exactly one search request to be fired
AjaxHelpers.expectNoRequests(requests);
AjaxHelpers.expectNoRequests(reqs);
};
it('can search teams', function() {

View File

@@ -3,6 +3,8 @@ define(['jquery',
'teams/js/views/topic_card',
'teams/js/models/topic'],
function($, _, TopicCardView, Topic) {
'use strict';
describe('Topic card view', function() {
var createTopicCardView = function() {
return new TopicCardView({

View File

@@ -8,8 +8,9 @@ define([
'use strict';
describe('Topic Teams View', function() {
var createTopicTeamsView = function(options) {
options = options || {};
var myTeamsCollection = options.myTeamsCollection || TeamSpecHelpers.createMockTeams({results: []});
var myTeamsCollection;
options = options || {}; // eslint-disable-line no-param-reassign
myTeamsCollection = options.myTeamsCollection || TeamSpecHelpers.createMockTeams({results: []});
return new TopicTeamsView({
el: '.teams-container',
model: TeamSpecHelpers.createMockTopic(),
@@ -21,14 +22,15 @@ define([
};
var verifyActions = function(teamsView, options) {
if (!options) {
options = {showActions: true};
}
var expectedTitle = 'Are you having trouble finding a team to join?',
expectedMessage = 'Browse teams in other topics or search teams in this topic. ' +
'If you still can\'t find a team to join, create a new team in this topic.',
title = teamsView.$('.title').text().trim(),
message = teamsView.$('.copy').text().trim();
if (!options) {
options = {showActions: true}; // eslint-disable-line no-param-reassign
}
if (options.showActions) {
expect(title).toBe(expectedTitle);
expect(message).toBe(expectedMessage);
@@ -50,11 +52,9 @@ define([
results: testTeamData
})
});
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
var footerEl = teamsView.$('.teams-paging-footer');
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2');
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2'); // eslint-disable-line no-useless-escape
expect(footerEl).not.toHaveClass('hidden');
TeamSpecHelpers.verifyCards(teamsView, testTeamData);

View File

@@ -40,7 +40,7 @@ define([
expect(currentCard.text()).toMatch(topic.description);
expect(currentCard.text()).toMatch(topic.team_count + ' Teams');
});
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2');
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2'); // eslint-disable-line no-useless-escape
expect(footerEl).not.toHaveClass('hidden');
});

View File

@@ -7,10 +7,13 @@ define([
], function(Backbone, _, TeamCollection, TopicCollection, TopicModel) {
'use strict';
var createMockPostResponse, createMockDiscussionResponse, createAnnotatedContentInfo, createMockThreadResponse,
createMockTopicData, createMockTopicCollection, createMockTopic,
createMockTopicData, createMockTopicCollection, createMockTopic, createMockInstructorManagedTopic,
createMockContext,
testContext,
testCourseID = 'course/1',
testUser = 'testUser',
testTopicID = 'test-topic-1',
testInstructorManagedTopicID = 'test-instructor-managed-topic-1',
testTeamDiscussionID = '12345',
teamEvents = _.clone(Backbone.Events),
testCountries = [
@@ -57,7 +60,7 @@ define([
var createMockTeams = function(responseOptions, options, collectionType) {
if (_.isUndefined(collectionType)) {
collectionType = TeamCollection;
collectionType = TeamCollection; // eslint-disable-line no-param-reassign
}
return new collectionType(
createMockTeamsResponse(responseOptions),
@@ -243,13 +246,26 @@ define([
{
id: testTopicID,
name: 'Test Topic 1',
description: 'Test description 1'
description: 'Test description 1',
type: 'open'
},
options
));
};
var testContext = {
createMockInstructorManagedTopic = function(options) {
return new TopicModel(_.extend(
{
id: testInstructorManagedTopicID,
name: 'Test Instructor Managed Topic 1',
description: 'Test instructor managed topic description 1',
type: 'public_managed'
},
options
));
};
testContext = {
courseID: testCourseID,
topics: {
count: 5,
@@ -270,11 +286,12 @@ define([
userInfo: createMockUserInfo()
};
var createMockContext = function(options) {
createMockContext = function(options) {
return _.extend({}, testContext, options);
};
createMockTopicCollection = function(topicData) {
// eslint-disable-next-line no-param-reassign
topicData = topicData !== undefined ? topicData : createMockTopicData(1, 5);
return new TopicCollection(
@@ -310,6 +327,7 @@ define([
createMockUserInfo: createMockUserInfo,
createMockContext: createMockContext,
createMockTopic: createMockTopic,
createMockInstructorManagedTopic: createMockInstructorManagedTopic,
createMockPostResponse: createMockPostResponse,
createMockDiscussionResponse: createMockDiscussionResponse,
createAnnotatedContentInfo: createAnnotatedContentInfo,

View File

@@ -9,11 +9,11 @@
'logger'
], function(Logger) {
var TeamAnalytics = {
emitPageViewed: function(page_name, topic_id, team_id) {
emitPageViewed: function(pageName, topicId, teamId) {
Logger.log('edx.team.page_viewed', {
page_name: page_name,
topic_id: topic_id,
team_id: team_id
page_name: pageName,
topic_id: topicId,
team_id: teamId
});
}
};

View File

@@ -51,7 +51,9 @@
valueAttribute: 'description',
editable: 'always',
showMessages: false,
helpMessage: gettext('A short description of the team to help other learners understand the goals or direction of the team (maximum 300 characters).')
helpMessage: gettext(
'A short description of the team to help other learners understand the ' +
'goals or direction of the team (maximum 300 characters).')
});
this.teamLanguageField = new FieldViews.DropdownFieldView({
@@ -62,7 +64,8 @@
showMessages: false,
titleIconName: 'fa-comment-o',
options: this.context.languages,
helpMessage: gettext('The language that team members primarily use to communicate with each other.')
helpMessage:
gettext('The language that team members primarily use to communicate with each other.')
});
this.teamCountryField = new FieldViews.DropdownFieldView({
@@ -78,7 +81,7 @@
},
render: function() {
this.$el.html(_.template(editTeamTemplate)({
this.$el.html(_.template(editTeamTemplate)({ // xss-lint: disable=javascript-jquery-html
primaryButtonTitle: this.primaryButtonTitle,
action: this.action,
totalMembers: _.isUndefined(this.teamModel) ? 0 : this.teamModel.get('membership').length
@@ -101,7 +104,7 @@
createOrUpdateTeam: function(event) {
event.preventDefault();
var view = this,
var view = this, // eslint-disable-line vars-on-top
teamLanguage = this.teamLanguageField.fieldValue(),
teamCountry = this.teamCountryField.fieldValue(),
data = {
@@ -122,7 +125,7 @@
saveOptions.contentType = 'application/merge-patch+json';
}
var validationResult = this.validateTeamData(data);
var validationResult = this.validateTeamData(data); // eslint-disable-line vars-on-top
if (validationResult.status === false) {
this.showMessage(validationResult.message, validationResult.srMessage);
return $().promise();
@@ -138,7 +141,7 @@
{trigger: true}
);
})
.fail(function(data) {
.fail(function(data) { // eslint-disable-line no-shadow
var response = JSON.parse(data.responseText);
var message = gettext('An error occurred. Please try again.');
if ('user_message' in response) {
@@ -202,8 +205,8 @@
},
cancelAndGoBack: function(event) {
event.preventDefault();
var url;
event.preventDefault();
if (this.action === 'create') {
url = 'topics/' + this.topic.id;
} else if (this.action === 'edit') {

View File

@@ -11,7 +11,8 @@
'text!teams/templates/edit-team-member.underscore',
'text!teams/templates/date.underscore'
],
function(Backbone, $, _, gettext, TeamModel, TeamUtils, ViewUtils, editTeamMemberTemplate, dateTemplate) {
function(
Backbone, $, _, gettext, TeamModel, TeamUtils, ViewUtils, editTeamMemberTemplate, dateTemplate) {
return Backbone.View.extend({
dateTemplate: _.template(dateTemplate),
teamMemberTemplate: _.template(editTeamMemberTemplate),
@@ -35,9 +36,11 @@
render: function() {
if (this.model.get('membership').length === 0) {
this.$el.html('<p>' + gettext('This team does not have any members.') + '</p>');
this.$el.html( // xss-lint: disable=javascript-jquery-html
// eslint-disable-next-line max-len
'<p>' + gettext('This team does not have any members.') + '</p>'); // xss-lint: disable=javascript-concat-html
} else {
this.$el.html('<ul class="edit-members"></ul>');
this.$el.html('<ul class="edit-members"></ul>'); // xss-lint: disable=javascript-jquery-html
this.renderTeamMembers();
}
return this;
@@ -48,22 +51,29 @@
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)
// eslint-disable-next-line no-undef
dateJoined = interpolate( // xss-lint: disable=javascript-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)
// eslint-disable-next-line no-undef
lastActivity = interpolate( // xss-lint: disable=javascript-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({
// eslint-disable-next-line max-len
self.$('.edit-members').append(self.teamMemberTemplate({ // xss-lint: disable=javascript-jquery-append
imageUrl: membership.user.profile_image.image_url_medium,
username: membership.user.username,
memberProfileUrl: '/u/' + membership.user.username,
@@ -81,7 +91,8 @@
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('This learner will be removed from the team,' +
'allowing another learner to take the available spot.'),
gettext('Remove'),
function() {
$.ajax({

View File

@@ -23,7 +23,7 @@
},
render: function() {
this.$el.html(this.template);
this.$el.html(this.template); // xss-lint: disable=javascript-jquery-html
return this;
},
@@ -31,7 +31,8 @@
event.preventDefault();
ViewUtils.confirmThenRunOperation(
gettext('Delete this team?'),
gettext('Deleting a team is permanent and cannot be undone. All members are removed from the team, and team discussions can no longer be accessed.'),
gettext('Deleting a team is permanent and cannot be undone.' +
'All members are removed from the team, and team discussions can no longer be accessed.'),
gettext('Delete'),
_.bind(this.handleDelete, this)
);

View File

@@ -13,7 +13,9 @@
.done(function() {
TeamsView.prototype.render.call(view);
if (view.collection.length === 0) {
view.$el.append('<p>' + gettext('You are not currently a member of any team.') + '</p>');
view.$el.append( // xss-lint: disable=javascript-jquery-append
// eslint-disable-next-line max-len
'<p>' + gettext('You are not currently a member of any team.') + '</p>'); // xss-lint: disable=javascript-concat-html
}
});
return this;

View File

@@ -41,11 +41,12 @@
}).reverse(),
displayableMemberships = allMemberships.slice(0, 5),
maxMemberCount = this.maxTeamSize;
this.$el.html(this.template({
this.$el.html(this.template({ // xss-lint: disable=javascript-jquery-html
membership_message: TeamUtils.teamCapacityText(allMemberships.length, maxMemberCount),
memberships: displayableMemberships,
has_additional_memberships: displayableMemberships.length < allMemberships.length,
// Translators: "and others" refers to fact that additional members of a team exist that are not displayed.
/* Translators: "and others" refers to fact that additional
* members of a team exist that are not displayed. */
sr_message: gettext('and others')
}));
return this;
@@ -62,7 +63,7 @@
render: function() {
// this.$el should be the card meta div
this.$el.append(this.template({
this.$el.append(this.template({ // xss-lint: disable=javascript-jquery-append
country: this.countries[this.model.get('country')],
language: this.languages[this.model.get('language')]
}));
@@ -82,9 +83,12 @@
var lastActivity = moment(this.date),
currentLanguage = $('html').attr('lang');
lastActivity.locale(currentLanguage);
this.$el.html(
interpolate(
// Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: http://momentjs.com/)
this.$el.html( // xss-lint: disable=javascript-jquery-html
// eslint-disable-next-line no-undef
interpolate( // xss-lint: disable=javascript-interpolate
/* Translators: 'date' is a placeholder for a fuzzy,
* relative timestamp (see: http://momentjs.com/)
*/
gettext('Last activity %(date)s'),
{date: this.template({date: lastActivity.format('MMMM Do YYYY, h:mm:ss a')})},
true
@@ -119,7 +123,8 @@
details: function() { return this.detailViews; },
actionClass: 'action-view',
actionContent: function() {
return interpolate(
// eslint-disable-next-line no-undef
return interpolate( // xss-lint: disable=javascript-interpolate
gettext('View %(span_start)s %(team_name)s %(span_end)s'),
{span_start: '<span class="sr">', team_name: _.escape(this.model.get('name')), span_end: '</span>'},
true

View File

@@ -31,6 +31,7 @@
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.languages);
this.topic = options.topic;
this.listenTo(this.model, 'change', this.render);
},
@@ -38,7 +39,11 @@
render: function() {
var memberships = this.model.get('membership'),
discussionTopicID = this.model.get('discussion_topic_id'),
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.context.userInfo.username);
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.context.userInfo.username),
isAdminOrStaff = this.context.userInfo.privileged || this.context.userInfo.staff,
isInstructorManagedTopic = TeamUtils.isInstructorManagedTopic(this.topic.attributes.type);
var showLeaveLink = isMember && (isAdminOrStaff || !isInstructorManagedTopic);
HtmlUtils.setHtml(
this.$el,
@@ -50,6 +55,7 @@
language: this.languages[this.model.get('language')],
membershipText: TeamUtils.teamCapacityText(memberships.length, this.context.maxTeamSize),
isMember: isMember,
showLeaveLink: showLeaveLink,
hasCapacity: memberships.length < this.context.maxTeamSize,
hasMembers: memberships.length >= 1
})
@@ -87,16 +93,17 @@
leaveTeam: function(event) {
event.preventDefault();
var view = this;
var view = this; // eslint-disable-line vars-on-top
ViewUtils.confirmThenRunOperation(
gettext('Leave this team?'),
gettext("If you leave, you can no longer post in this team's discussions. Your place will be available to another learner."),
gettext("If you leave, you can no longer post in this team's discussions." +
'Your place will be available to another learner.'),
gettext('Confirm'),
function() {
$.ajax({
type: 'DELETE',
url: view.context.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
}).done(function(data) {
}).done(function() {
view.model.fetch()
.done(function() {
view.teamEvents.trigger('teams:update', {

View File

@@ -13,6 +13,7 @@
errorMessage: gettext('An error occurred. Try again.'),
alreadyMemberMessage: gettext('You already belong to another team.'),
teamFullMessage: gettext('This team is full.'),
notJoinInstructorManagedTeam: gettext('Cannot join instructor managed team'),
events: {
'click .action-primary': 'joinTeam',
@@ -39,16 +40,21 @@
// if user is the member of current team then we wouldn't show anything
if (!info.memberOfCurrentTeam) {
showJoinButton = !info.alreadyMember && teamHasSpace;
if (info.alreadyMember) {
showJoinButton = false;
message = info.memberOfCurrentTeam ? '' : view.alreadyMemberMessage;
} else if (!teamHasSpace) {
showJoinButton = false;
message = view.teamFullMessage;
} else if (!info.isAdminOrStaff && info.isInstructorManagedTopic) {
showJoinButton = false;
message = view.notJoinInstructorManagedTeam;
} else {
showJoinButton = true;
}
}
view.$el.html(view.template({
view.$el.html(view.template({ // xss-lint: disable=javascript-jquery-html
showJoinButton: showJoinButton,
message: message,
showEditButton: view.showEditButton
@@ -65,7 +71,7 @@
type: 'POST',
url: view.context.teamMembershipsUrl,
data: {username: view.context.userInfo.username, team_id: view.model.get('id')}
}).done(function(data) {
}).done(function() {
view.model.fetch()
.done(function() {
view.teamEvents.trigger('teams:update', {
@@ -83,11 +89,15 @@
var info = {
alreadyMember: false,
memberOfCurrentTeam: false,
teamHasSpace: false
teamHasSpace: false,
isAdminOrStaff: false,
isInstructorManagedTopic: false
};
var teamHasSpace = this.model.get('membership').length < maxTeamSize;
info.memberOfCurrentTeam = TeamUtils.isUserMemberOfTeam(this.model.get('membership'), username);
var teamHasSpace = this.model.get('membership').length < maxTeamSize;
info.isAdminOrStaff = this.context.userInfo.privileged || this.context.userInfo.staff;
info.isInstructorManagedTopic = TeamUtils.isInstructorManagedTopic(this.topic.attributes.type);
if (info.memberOfCurrentTeam) {
info.alreadyMember = true;
@@ -95,7 +105,7 @@
deferred.resolve(info);
} else {
if (teamHasSpace) {
var view = this;
var view = this; // eslint-disable-line vars-on-top
$.ajax({
type: 'GET',
url: view.context.teamMembershipsUrl,

View File

@@ -1,8 +1,8 @@
/* Team utility methods*/
(function(define) {
'use strict';
define(['jquery', 'underscore'
], function($, _) {
define(['jquery', 'underscore'],
function($, _) {
return {
/**
@@ -20,7 +20,8 @@
},
teamCapacityText: function(memberCount, maxMemberCount) {
return interpolate(
// eslint-disable-next-line no-undef
return interpolate( // xss-lint: disable=javascript-interpolate
// Translators: The following message displays the number of members on a team.
ngettext(
'%(memberCount)s / %(maxMemberCount)s Member',
@@ -46,7 +47,7 @@
showMessage: function(message, type) {
var $messageElement = $('#teams-message');
if (_.isUndefined(type)) {
type = 'warning';
type = 'warning'; // eslint-disable-line no-param-reassign
}
$messageElement.removeClass('is-hidden').addClass(type);
$('.teams-content .msg-content .copy').text(message);
@@ -58,13 +59,20 @@
*/
parseAndShowMessage: function(data, genericErrorMessage, type) {
try {
var errors = JSON.parse(data.responseText);
var errors = JSON.parse(data.responseText); // eslint-disable-line vars-on-top
this.showMessage(
_.isUndefined(errors.user_message) ? genericErrorMessage : errors.user_message, type
);
} catch (error) {
this.showMessage(genericErrorMessage, type);
}
},
isInstructorManagedTopic: function(topicType) {
if (topicType === undefined) {
return false;
}
return topicType.toLowerCase() !== 'open';
}
};
});

View File

@@ -78,7 +78,9 @@
['topics/:topic_id/search(/)', _.bind(this.searchTeams, this)],
['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)],
['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)],
// eslint-disable-next-line no-useless-escape
[new RegExp('^(browse)\/?$'), _.bind(this.goToTab, this)],
// eslint-disable-next-line no-useless-escape
[new RegExp('^(my-teams)\/?$'), _.bind(this.goToTab, this)]
], function(route) {
router.route.apply(router, route);
@@ -133,7 +135,9 @@
this.mainView = this.tabbedView = this.createViewWithHeader({
title: gettext('Teams'),
description: gettext('See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are.'),
description: gettext('See all teams in your course, organized by topic. ' +
'Join a team to collaborate with other learners who are interested' +
'in the same topic as you are.'),
mainView: new TeamsTabbedView({
tabs: [{
title: gettext('My Team'),
@@ -235,7 +239,8 @@
view.mainView = view.createViewWithHeader({
topic: topic,
title: gettext('Create a New Team'),
description: gettext("Create a new team if you can't find an existing team to join, or if you would like to learn with friends you know."),
description: gettext("Create a new team if you can't find an existing team to join, " +
'or if you would like to learn with friends you know.'),
breadcrumbs: view.createBreadcrumbs(topic),
mainView: new TeamEditView({
action: 'create',
@@ -269,7 +274,8 @@
});
editViewWithHeader = self.createViewWithHeader({
title: gettext('Edit Team'),
description: gettext('If you make significant changes, make sure you notify members of the team before making these changes.'),
description: gettext('If you make significant changes, ' +
'make sure you notify members of the team before making these changes.'),
breadcrumbs: self.createBreadcrumbs(topic, team),
mainView: view,
topic: topic,
@@ -298,7 +304,8 @@
mainView: view,
breadcrumbs: self.createBreadcrumbs(topic, team),
title: gettext('Membership'),
description: gettext("You can remove members from this team, especially if they have not participated in the team's activity."),
description: gettext('You can remove members from this team, ' +
"especially if they have not participated in the team's activity."),
topic: topic,
team: team
}
@@ -422,6 +429,7 @@
router: self.router,
context: self.context,
model: team,
topic: topic,
setFocusToHeaderFunc: self.setFocusToHeader
});

View File

@@ -14,8 +14,9 @@
},
render: function() {
var team_count = this.model.get('team_count');
this.$el.html(_.escape(interpolate(
var team_count = this.model.get('team_count'); // eslint-disable-line camelcase
// eslint-disable-next-line no-undef, max-len
this.$el.html(_.escape(interpolate( // xss-lint: disable=javascript-jquery-html,javascript-interpolate
ngettext('%(team_count)s Team', '%(team_count)s Teams', team_count),
{team_count: team_count},
true
@@ -42,11 +43,13 @@
details: function() { return this.detailViews; },
actionClass: 'action-view',
actionContent: function() {
var screenReaderText = _.escape(interpolate(
// eslint-disable-next-line no-undef
var screenReaderText = _.escape(interpolate( // xss-lint: disable=javascript-interpolate
gettext('View Teams in the %(topic_name)s Topic'),
{topic_name: this.model.get('name')}, true
));
return '<span class="sr">' + screenReaderText + '</span><span class="icon fa fa-arrow-right" aria-hidden="true"></span>'; // eslint-disable-line max-len
// eslint-disable-next-line max-len
return '<span class="sr">' + screenReaderText + '</span><span class="icon fa fa-arrow-right" aria-hidden="true"></span>'; // xss-lint: disable=javascript-concat-html
}
});

View File

@@ -5,8 +5,9 @@
'gettext',
'teams/js/views/teams',
'common/js/components/views/paging_header',
'text!teams/templates/team-actions.underscore'
], function(Backbone, gettext, TeamsView, PagingHeader, teamActionsTemplate) {
'text!teams/templates/team-actions.underscore',
'underscore'
], function(Backbone, gettext, TeamsView, PagingHeader, teamActionsTemplate, _) {
var TopicTeamsView = TeamsView.extend({
events: {
'click a.browse-teams': 'browseTeams',
@@ -35,7 +36,7 @@
this.collection.refresh().done(function() {
TeamsView.prototype.render.call(self);
if (self.canUserCreateTeam()) {
var message = interpolate_text(
var message = interpolate_text( // eslint-disable-line vars-on-top, no-undef
// Translators: this string is shown at the bottom of the teams page
// to find a team to join or else to create a new one. There are three
// links that need to be included in the message:
@@ -44,7 +45,10 @@
// 3. create a new team
// Be careful to start each link with the appropriate start indicator
// (e.g. {browse_span_start} for #1) and finish it with {span_end}.
_.escape(gettext("{browse_span_start}Browse teams in other topics{span_end} or {search_span_start}search teams{span_end} in this topic. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")),
_.escape(gettext('{browse_span_start}Browse teams in other topics{span_end} or ' +
'{search_span_start}search teams{span_end} in this topic. ' +
"If you still can't find a team to join, " +
'{create_span_start}create a new team in this topic{span_end}.')),
{
browse_span_start: '<a class="browse-teams" href="">',
search_span_start: '<a class="search-teams" href="">',
@@ -52,7 +56,8 @@
span_end: '</a>'
}
);
self.$el.append(_.template(teamActionsTemplate)({message: message}));
// eslint-disable-next-line max-len
self.$el.append(_.template(teamActionsTemplate)({message: message})); // xss-lint: disable=javascript-jquery-append
}
});
return this;

View File

@@ -5,8 +5,9 @@
'teams/js/views/topic_card',
'teams/js/views/team_utils',
'common/js/components/views/paging_header',
'common/js/components/views/paginated_view'
], function(gettext, TopicCardView, TeamUtils, PagingHeader, PaginatedView) {
'common/js/components/views/paginated_view',
'underscore'
], function(gettext, TopicCardView, TeamUtils, PagingHeader, PaginatedView, _) {
var TopicsView = PaginatedView.extend({
type: 'topics',

View File

@@ -45,7 +45,7 @@
</div>
<% } %>
<% if (isMember) { %>
<% if (showLeaveLink) { %>
<div class="leave-team">
<button class="btn btn-link leave-team-link"><%- gettext("Leave Team") %></button>
</div>

View File

@@ -83,12 +83,12 @@ class TopicSerializerTestCase(SerializerTestCase):
"""
with self.assertNumQueries(1):
serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data_old_format,
self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id},
)
self.assertEqual(
serializer.data,
{u'name': u'Tøpic', u'description': u'The bést topic!', u'id': u'0', u'team_count': 0}
{u'name': u'Tøpic', u'description': u'The bést topic!', u'id': u'0', u'team_count': 0, u'type': u'open'}
)
def test_topic_with_team_count(self):
@@ -101,17 +101,17 @@ class TopicSerializerTestCase(SerializerTestCase):
)
with self.assertNumQueries(1):
serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data_old_format,
self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id},
)
self.assertEqual(
serializer.data,
{u'name': u'Tøpic', u'description': u'The bést topic!', u'id': u'0', u'team_count': 1}
{u'name': u'Tøpic', u'description': u'The bést topic!', u'id': u'0', u'team_count': 1, u'type': u'open'}
)
def test_scoped_within_course(self):
"""Verify that team count is scoped within a course."""
duplicate_topic = self.course.teamsets[0].cleaned_data_old_format
duplicate_topic = self.course.teamsets[0].cleaned_data
second_course = CourseFactory.create(
teams_configuration=TeamsConfig({
"max_team_size": 10,
@@ -122,12 +122,12 @@ class TopicSerializerTestCase(SerializerTestCase):
CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic[u'id'])
with self.assertNumQueries(1):
serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data_old_format,
self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id},
)
self.assertEqual(
serializer.data,
{u'name': u'Tøpic', u'description': u'The bést topic!', u'id': u'0', u'team_count': 1}
{u'name': u'Tøpic', u'description': u'The bést topic!', u'id': u'0', u'team_count': 1, u'type': u'open'}
)
@@ -153,7 +153,12 @@ class BaseTopicSerializerTestCase(SerializerTestCase):
created topics.
"""
topics = [
{u'name': u'Tøpic {}'.format(i), u'description': u'The bést topic! {}'.format(i), u'id': six.text_type(i)}
{
u'name': u'Tøpic {}'.format(i),
u'description': u'The bést topic! {}'.format(i),
u'id': six.text_type(i),
u'type': u'open'
}
for i in six.moves.range(num_topics)
]
for topic in topics:

View File

@@ -1416,6 +1416,21 @@ class TestCreateMembershipAPI(EventTestMixin, TeamAPITestCase):
user='staff'
)
@patch('lms.djangoapps.teams.api.is_instructor_managed_team', return_value=True)
def test_staff_join_instructor_managed_team(self, *args): # pylint: disable=unused-argument
self.post_create_membership(
200,
self.build_membership_data_raw(self.users['staff'].username, self.solar_team.team_id),
user='staff'
)
@patch('lms.djangoapps.teams.api.is_instructor_managed_team', return_value=True)
def test_student_join_instructor_managed_team(self, *args): # pylint: disable=unused-argument
self.post_create_membership(
403,
self.build_membership_data_raw(self.users['student_enrolled_not_on_team'].username, self.solar_team.team_id)
)
@ddt.data('student_enrolled', 'staff', 'course_staff')
def test_join_twice(self, user):
response = self.post_create_membership(
@@ -1577,6 +1592,11 @@ class TestDeleteMembershipAPI(EventTestMixin, TeamAPITestCase):
def test_missing_membership(self):
self.delete_membership(self.wind_team.team_id, self.users['student_enrolled'].username, 404)
@patch('lms.djangoapps.teams.api.is_instructor_managed_team', return_value=True)
def test_student_leave_instructor_managed_team(self, *args): # pylint: disable=unused-argument
self.delete_membership(
self.solar_team.team_id, self.users['student_enrolled'].username, 403, user='student_enrolled')
class TestElasticSearchErrors(TeamAPITestCase):
"""Test that the Team API is robust to Elasticsearch connection errors."""

View File

@@ -34,6 +34,7 @@ from lms.djangoapps.teams.api import (
user_organization_protection_status,
has_specific_team_access,
add_team_count,
can_user_modify_team
)
from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership
from openedx.core.lib.api.parsers import MergePatchParser
@@ -362,7 +363,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = CourseTeamSerializer
def get(self, request):
def get(self, request): # pylint: disable=too-many-statements
"""GET /api/team/v0/teams/"""
result_filter = {}
@@ -852,7 +853,7 @@ def get_alphabetical_topics(course_module):
list: a list of sorted team topics
"""
return sorted(
course_module.teams_configuration.cleaned_data_old_format['topics'],
course_module.teams_configuration.cleaned_data['team_sets'],
key=lambda t: t['name'].lower(),
)
@@ -926,7 +927,7 @@ class TopicDetailView(APIView):
organization_protection_status = user_organization_protection_status(request.user, course_id)
serializer = TopicSerializer(
topic.cleaned_data_old_format,
topic.cleaned_data,
context={
'course_id': course_id,
'organization_protection_status': organization_protection_status
@@ -1167,6 +1168,12 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
status=status.HTTP_400_BAD_REQUEST
)
if not can_user_modify_team(request.user, team.course_id, team):
return Response(
build_api_error(ugettext_noop("You can't join an instructor managed team.")),
status=status.HTTP_403_FORBIDDEN
)
try:
membership = team.add_user(user)
emit_team_event(
@@ -1310,6 +1317,12 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView):
if not has_specific_team_access(request.user, team):
return Response(status=status.HTTP_403_FORBIDDEN)
if not can_user_modify_team(request.user, team.course_id, team):
return Response(
build_api_error(ugettext_noop("You can't leave an instructor managed team.")),
status=status.HTTP_403_FORBIDDEN
)
membership = self.get_membership(username, team)
removal_method = 'self_removal'
if 'admin' in request.query_params: