Fix displaying wrong membership on "My Team" view.
The heart of this fix is to change the "My Team" view to use a collection of teams instead of memberships. The team card is refactored to only take a team, rather than attempt to be polymorphic over teams and team memberships. This change enabled removing a good amount of old code. This also requires adding a `username` parameter to the teams list endpoint which allows getting a list of all teams for a single user, in order to allow refreshing the "My Teams" view correctly. Currently this list is of length at most one. TNL-3410
This commit is contained in:
@@ -46,6 +46,11 @@ class TeamCardsMixin(object):
|
||||
"""Return the names of each team on the page."""
|
||||
return self.q(css=self._bounded_selector('p.card-description')).map(lambda e: e.text).results
|
||||
|
||||
@property
|
||||
def team_memberships(self):
|
||||
"""Return the team memberships text for each card on the page."""
|
||||
return self.q(css=self._bounded_selector('.member-count')).map(lambda e: e.text).results
|
||||
|
||||
|
||||
class BreadcrumbsMixin(object):
|
||||
"""Provides common operations on teams page breadcrumb links."""
|
||||
|
||||
@@ -78,6 +78,18 @@ class TeamsTabBase(EventsTestMixin, UniqueCourseTest):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return json.loads(response.text)
|
||||
|
||||
def create_memberships(self, num_memberships, team_id):
|
||||
"""Create `num_memberships` users and assign them to `team_id`. The
|
||||
last user created becomes the current user."""
|
||||
memberships = []
|
||||
for __ in xrange(num_memberships):
|
||||
user_info = AutoAuthPage(self.browser, course_id=self.course_id).visit().user_info
|
||||
memberships.append(user_info)
|
||||
self.create_membership(user_info['username'], team_id)
|
||||
#pylint: disable=attribute-defined-outside-init
|
||||
self.user_info = memberships[-1]
|
||||
return memberships
|
||||
|
||||
def create_membership(self, username, team_id):
|
||||
"""Assign `username` to `team_id`."""
|
||||
response = self.course_fixture.session.post(
|
||||
@@ -339,6 +351,18 @@ class MyTeamsTest(TeamsTabBase):
|
||||
self.my_teams_page.visit()
|
||||
self.verify_teams(self.my_teams_page, teams)
|
||||
|
||||
def test_multiple_team_members(self):
|
||||
"""
|
||||
Scenario: Visiting the My Teams page when user is a member of a team should display the teams.
|
||||
Given I am a member of a team with multiple members
|
||||
When I visit the My Teams page
|
||||
Then I should see the correct number of team members on my membership
|
||||
"""
|
||||
teams = self.create_teams(self.topic, 1)
|
||||
self.create_memberships(4, teams[0]['id'])
|
||||
self.my_teams_page.visit()
|
||||
self.assertEqual(self.my_teams_page.team_memberships[0], '4 / 10 Members')
|
||||
|
||||
|
||||
@attr('shard_5')
|
||||
@ddt.ddt
|
||||
|
||||
15
lms/djangoapps/teams/static/teams/js/collections/my_teams.js
Normal file
15
lms/djangoapps/teams/static/teams/js/collections/my_teams.js
Normal file
@@ -0,0 +1,15 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['teams/js/collections/team'], function (TeamCollection) {
|
||||
var MyTeamsCollection = TeamCollection.extend({
|
||||
initialize: function (teams, options) {
|
||||
TeamCollection.prototype.initialize.call(this, teams, options);
|
||||
delete this.server_api.topic_id;
|
||||
this.server_api = _.extend(this.server_api, {
|
||||
username: options.username
|
||||
});
|
||||
}
|
||||
});
|
||||
return MyTeamsCollection;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
this.perPage = options.per_page || 10;
|
||||
this.username = options.username;
|
||||
this.privileged = options.privileged;
|
||||
this.staff = options.staff;
|
||||
|
||||
this.server_api = _.extend(
|
||||
{
|
||||
@@ -24,15 +22,7 @@
|
||||
delete this.server_api['order_by']; // Order by is not specified for the TeamMembership API
|
||||
},
|
||||
|
||||
model: TeamMembershipModel,
|
||||
|
||||
canUserCreateTeam: function() {
|
||||
// Note: non-staff and non-privileged users are automatically added to any team
|
||||
// that they create. This means that if multiple team membership is
|
||||
// disabled that they cannot create a new team when they already
|
||||
// belong to one.
|
||||
return this.privileged || this.staff || this.length === 0;
|
||||
}
|
||||
model: TeamMembershipModel
|
||||
});
|
||||
return TeamMembershipCollection;
|
||||
});
|
||||
|
||||
@@ -1,39 +1,30 @@
|
||||
define([
|
||||
'backbone',
|
||||
'teams/js/collections/team',
|
||||
'teams/js/collections/team_membership',
|
||||
'teams/js/collections/my_teams',
|
||||
'teams/js/views/my_teams',
|
||||
'teams/js/spec_helpers/team_spec_helpers',
|
||||
'common/js/spec_helpers/ajax_helpers'
|
||||
], function (Backbone, TeamCollection, TeamMembershipCollection, MyTeamsView, TeamSpecHelpers, AjaxHelpers) {
|
||||
], function (Backbone, MyTeamsCollection, MyTeamsView, TeamSpecHelpers, AjaxHelpers) {
|
||||
'use strict';
|
||||
describe('My Teams View', function () {
|
||||
beforeEach(function () {
|
||||
setFixtures('<div class="teams-container"></div>');
|
||||
});
|
||||
|
||||
var createMyTeamsView = function(options) {
|
||||
return new MyTeamsView(_.extend(
|
||||
{
|
||||
el: '.teams-container',
|
||||
collection: options.teams || TeamSpecHelpers.createMockTeams(),
|
||||
teamMemberships: TeamSpecHelpers.createMockTeamMemberships(),
|
||||
showActions: true,
|
||||
context: TeamSpecHelpers.testContext
|
||||
},
|
||||
options
|
||||
)).render();
|
||||
var createMyTeamsView = function(myTeams) {
|
||||
return new MyTeamsView({
|
||||
el: '.teams-container',
|
||||
collection: myTeams,
|
||||
showActions: true,
|
||||
context: TeamSpecHelpers.testContext
|
||||
}).render();
|
||||
};
|
||||
|
||||
it('can render itself', function () {
|
||||
var teamMembershipsData = TeamSpecHelpers.createMockTeamMembershipsData(1, 5),
|
||||
teamMemberships = TeamSpecHelpers.createMockTeamMemberships(teamMembershipsData),
|
||||
myTeamsView = createMyTeamsView({
|
||||
teams: teamMemberships,
|
||||
teamMemberships: teamMemberships
|
||||
});
|
||||
|
||||
TeamSpecHelpers.verifyCards(myTeamsView, teamMembershipsData);
|
||||
var teamsData = TeamSpecHelpers.createMockTeamData(1, 5),
|
||||
teams = TeamSpecHelpers.createMockTeams({results: teamsData}),
|
||||
myTeamsView = createMyTeamsView(teams);
|
||||
TeamSpecHelpers.verifyCards(myTeamsView, teamsData);
|
||||
|
||||
// Verify that there is no header or footer
|
||||
expect(myTeamsView.$('.teams-paging-header').text().trim()).toBe('');
|
||||
@@ -41,36 +32,37 @@ define([
|
||||
});
|
||||
|
||||
it('shows a message when the user is not a member of any teams', function () {
|
||||
var teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
|
||||
myTeamsView = createMyTeamsView({
|
||||
teams: teamMemberships,
|
||||
teamMemberships: teamMemberships
|
||||
});
|
||||
var teams = TeamSpecHelpers.createMockTeams({results: []}),
|
||||
myTeamsView = createMyTeamsView(teams);
|
||||
TeamSpecHelpers.verifyCards(myTeamsView, []);
|
||||
expect(myTeamsView.$el.text().trim()).toBe('You are not currently a member of any team.');
|
||||
});
|
||||
|
||||
it('refreshes a stale membership collection when rendering', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
|
||||
myTeamsView = createMyTeamsView({
|
||||
teams: teamMemberships,
|
||||
teamMemberships: teamMemberships
|
||||
});
|
||||
teams = TeamSpecHelpers.createMockTeams({
|
||||
results: []
|
||||
}, {
|
||||
per_page: 2,
|
||||
url: TeamSpecHelpers.testContext.myTeamsUrl,
|
||||
username: TeamSpecHelpers.testContext.userInfo.username
|
||||
}, MyTeamsCollection),
|
||||
myTeamsView = createMyTeamsView(teams);
|
||||
TeamSpecHelpers.verifyCards(myTeamsView, []);
|
||||
expect(myTeamsView.$el.text().trim()).toBe('You are not currently a member of any team.');
|
||||
teamMemberships.teamEvents.trigger('teams:update', { action: 'create' });
|
||||
TeamSpecHelpers.teamEvents.trigger('teams:update', { action: 'create' });
|
||||
myTeamsView.render();
|
||||
AjaxHelpers.expectRequestURL(
|
||||
requests,
|
||||
TeamSpecHelpers.testContext.teamMembershipsUrl,
|
||||
TeamSpecHelpers.testContext.myTeamsUrl,
|
||||
{
|
||||
expand : 'team,user',
|
||||
expand : 'user',
|
||||
username : TeamSpecHelpers.testContext.userInfo.username,
|
||||
course_id : TeamSpecHelpers.testContext.courseID,
|
||||
page : '1',
|
||||
page_size : '10',
|
||||
text_search: ''
|
||||
page_size : '2',
|
||||
text_search: '',
|
||||
order_by: 'last_activity_at'
|
||||
}
|
||||
);
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
define([
|
||||
'backbone',
|
||||
'teams/js/collections/team',
|
||||
'teams/js/collections/team_membership',
|
||||
'teams/js/views/teams',
|
||||
'teams/js/spec_helpers/team_spec_helpers'
|
||||
], function (Backbone, TeamCollection, TeamMembershipCollection, TeamsView, TeamSpecHelpers) {
|
||||
], function (Backbone, TeamCollection, TeamsView, TeamSpecHelpers) {
|
||||
'use strict';
|
||||
describe('Teams View', function () {
|
||||
beforeEach(function () {
|
||||
@@ -15,7 +14,6 @@ define([
|
||||
return new TeamsView({
|
||||
el: '.teams-container',
|
||||
collection: options.teams || TeamSpecHelpers.createMockTeams(),
|
||||
teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(),
|
||||
showActions: true,
|
||||
context: TeamSpecHelpers.testContext
|
||||
}).render();
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
define([
|
||||
'backbone',
|
||||
'teams/js/collections/team',
|
||||
'teams/js/collections/team_membership',
|
||||
'underscore',
|
||||
'teams/js/views/topic_teams',
|
||||
'teams/js/spec_helpers/team_spec_helpers',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'common/js/spec_helpers/page_helpers'
|
||||
], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers,
|
||||
AjaxHelpers, PageHelpers) {
|
||||
], function (Backbone, _, TopicTeamsView, TeamSpecHelpers, PageHelpers) {
|
||||
'use strict';
|
||||
describe('Topic Teams View', function () {
|
||||
var createTopicTeamsView = function(options) {
|
||||
options = options || {};
|
||||
var myTeamsCollection = options.myTeamsCollection || TeamSpecHelpers.createMockTeams({results: []});
|
||||
return new TopicTeamsView({
|
||||
el: '.teams-container',
|
||||
model: TeamSpecHelpers.createMockTopic(),
|
||||
collection: options.teams || TeamSpecHelpers.createMockTeams(),
|
||||
teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(),
|
||||
myTeamsCollection: myTeamsCollection,
|
||||
showActions: true,
|
||||
context: TeamSpecHelpers.testContext
|
||||
context: _.extend({}, TeamSpecHelpers.testContext, options)
|
||||
}).render();
|
||||
};
|
||||
|
||||
@@ -49,8 +48,7 @@ define([
|
||||
teamsView = createTopicTeamsView({
|
||||
teams: TeamSpecHelpers.createMockTeams({
|
||||
results: testTeamData
|
||||
}),
|
||||
teamMemberships: TeamSpecHelpers.createMockTeamMemberships([])
|
||||
})
|
||||
});
|
||||
|
||||
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
|
||||
@@ -64,24 +62,21 @@ define([
|
||||
});
|
||||
|
||||
it('can browse all teams', function () {
|
||||
var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]),
|
||||
teamsView = createTopicTeamsView({ teamMemberships: emptyMembership });
|
||||
var teamsView = createTopicTeamsView();
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
teamsView.$('.browse-teams').click();
|
||||
expect(Backbone.history.navigate.calls[0].args).toContain('browse');
|
||||
});
|
||||
|
||||
it('gives the search field focus when clicking on the search teams link', function () {
|
||||
var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]),
|
||||
teamsView = createTopicTeamsView({ teamMemberships: emptyMembership });
|
||||
var teamsView = createTopicTeamsView();
|
||||
spyOn($.fn, 'focus').andCallThrough();
|
||||
teamsView.$('.search-teams').click();
|
||||
expect(teamsView.$('.search-field').first().focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can show the create team modal', function () {
|
||||
var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]),
|
||||
teamsView = createTopicTeamsView({ teamMemberships: emptyMembership });
|
||||
var teamsView = createTopicTeamsView();
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
teamsView.$('a.create-team').click();
|
||||
expect(Backbone.history.navigate.calls[0].args).toContain(
|
||||
@@ -90,25 +85,17 @@ define([
|
||||
});
|
||||
|
||||
it('does not show actions for a user already in a team', function () {
|
||||
var teamsView = createTopicTeamsView({});
|
||||
var teamsView = createTopicTeamsView({myTeamsCollection: TeamSpecHelpers.createMockTeams()});
|
||||
verifyActions(teamsView, {showActions: false});
|
||||
});
|
||||
|
||||
it('shows actions for a privileged user already in a team', function () {
|
||||
var staffMembership = TeamSpecHelpers.createMockTeamMemberships(
|
||||
TeamSpecHelpers.createMockTeamMembershipsData(1, 5),
|
||||
{ privileged: true }
|
||||
),
|
||||
teamsView = createTopicTeamsView({ teamMemberships: staffMembership });
|
||||
var teamsView = createTopicTeamsView({ privileged: true });
|
||||
verifyActions(teamsView);
|
||||
});
|
||||
|
||||
it('shows actions for a staff user already in a team', function () {
|
||||
var staffMembership = TeamSpecHelpers.createMockTeamMemberships(
|
||||
TeamSpecHelpers.createMockTeamMembershipsData(1, 5),
|
||||
{ privileged: false, staff: true }
|
||||
),
|
||||
teamsView = createTopicTeamsView({ teamMemberships: staffMembership });
|
||||
var teamsView = createTopicTeamsView({ privileged: false, staff: true });
|
||||
verifyActions(teamsView);
|
||||
});
|
||||
|
||||
|
||||
@@ -56,14 +56,18 @@ define([
|
||||
);
|
||||
};
|
||||
|
||||
var createMockTeams = function(options) {
|
||||
return new TeamCollection(
|
||||
createMockTeamsResponse(options),
|
||||
{
|
||||
var createMockTeams = function(responseOptions, options, collectionType) {
|
||||
if(_.isUndefined(collectionType)) {
|
||||
collectionType = TeamCollection;
|
||||
}
|
||||
return new collectionType(
|
||||
createMockTeamsResponse(responseOptions),
|
||||
_.extend({
|
||||
teamEvents: teamEvents,
|
||||
course_id: testCourseID,
|
||||
per_page: 2,
|
||||
parse: true
|
||||
}
|
||||
}, options)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,35 +87,6 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var createMockTeamMemberships = function(teamMembershipData, options) {
|
||||
if (!teamMembershipData) {
|
||||
teamMembershipData = createMockTeamMembershipsData(1, 5);
|
||||
}
|
||||
return new TeamMembershipCollection(
|
||||
{
|
||||
count: 11,
|
||||
num_pages: 3,
|
||||
current_page: 1,
|
||||
start: 0,
|
||||
sort_order: 'last_activity_at',
|
||||
results: teamMembershipData
|
||||
},
|
||||
_.extend(
|
||||
{},
|
||||
{
|
||||
teamEvents: teamEvents,
|
||||
course_id: testCourseID,
|
||||
parse: true,
|
||||
url: testContext.teamMembershipsUrl,
|
||||
username: testUser,
|
||||
privileged: false,
|
||||
staff: false
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
var createMockUserInfo = function(options) {
|
||||
return _.extend(
|
||||
{
|
||||
@@ -291,6 +266,7 @@ define([
|
||||
teamsDetailUrl: '/api/team/v0/teams/team_id',
|
||||
teamMembershipsUrl: '/api/team/v0/team_memberships/',
|
||||
teamMembershipDetailUrl: '/api/team/v0/team_membership/team_id,' + testUser,
|
||||
myTeamsUrl: '/api/team/v0/teams/',
|
||||
userInfo: createMockUserInfo()
|
||||
};
|
||||
|
||||
@@ -331,8 +307,6 @@ define([
|
||||
createMockTeamData: createMockTeamData,
|
||||
createMockTeamsResponse: createMockTeamsResponse,
|
||||
createMockTeams: createMockTeams,
|
||||
createMockTeamMembershipsData: createMockTeamMembershipsData,
|
||||
createMockTeamMemberships: createMockTeamMemberships,
|
||||
createMockUserInfo: createMockUserInfo,
|
||||
createMockContext: createMockContext,
|
||||
createMockTopic: createMockTopic,
|
||||
|
||||
@@ -22,12 +22,11 @@
|
||||
},
|
||||
|
||||
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.teamMembershipDetailUrl = options.context.teamMembershipDetailUrl.substring(
|
||||
0, this.options.context.teamMembershipDetailUrl.lastIndexOf('team_id')
|
||||
) + this.model.get('id') + ",";
|
||||
|
||||
this.teamEvents = options.teamEvents;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
;(function (define) {
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery',
|
||||
@@ -99,48 +99,34 @@
|
||||
CardView.prototype.initialize.apply(this, arguments);
|
||||
// TODO: show last activity detail view
|
||||
this.detailViews = [
|
||||
new TeamMembershipView({memberships: this.getMemberships(), maxTeamSize: this.maxTeamSize}),
|
||||
new TeamMembershipView({memberships: this.model.get('membership'), maxTeamSize: this.maxTeamSize}),
|
||||
new TeamCountryLanguageView({
|
||||
model: this.teamModel(),
|
||||
model: this.model,
|
||||
countries: this.countries,
|
||||
languages: this.languages
|
||||
}),
|
||||
new TeamActivityView({date: this.teamModel().get('last_activity_at')})
|
||||
new TeamActivityView({date: this.model.get('last_activity_at')})
|
||||
];
|
||||
this.model.on('change:membership', function () {
|
||||
this.detailViews[0].memberships = this.getMemberships();
|
||||
this.detailViews[0].memberships = this.model.get('membership');
|
||||
}, this);
|
||||
},
|
||||
|
||||
teamModel: function () {
|
||||
if (this.model.has('team')) { return this.model.get('team'); }
|
||||
return this.model;
|
||||
},
|
||||
|
||||
getMemberships: function () {
|
||||
if (this.model.has('team')) {
|
||||
return [this.model.attributes];
|
||||
}
|
||||
else {
|
||||
return this.model.get('membership');
|
||||
}
|
||||
},
|
||||
|
||||
configuration: 'list_card',
|
||||
cardClass: 'team-card',
|
||||
title: function () { return this.teamModel().get('name'); },
|
||||
description: function () { return this.teamModel().get('description'); },
|
||||
title: function () { return this.model.get('name'); },
|
||||
description: function () { return this.model.get('description'); },
|
||||
details: function () { return this.detailViews; },
|
||||
actionClass: 'action-view',
|
||||
actionContent: function() {
|
||||
return interpolate(
|
||||
gettext('View %(span_start)s %(team_name)s %(span_end)s'),
|
||||
{span_start: '<span class="sr">', team_name: _.escape(this.teamModel().get('name')), span_end: '</span>'},
|
||||
{span_start: '<span class="sr">', team_name: _.escape(this.model.get('name')), span_end: '</span>'},
|
||||
true
|
||||
);
|
||||
},
|
||||
actionUrl: function () {
|
||||
return '#teams/' + this.teamModel().get('topic_id') + '/' + this.teamModel().get('id');
|
||||
return '#teams/' + this.model.get('topic_id') + '/' + this.model.get('id');
|
||||
}
|
||||
});
|
||||
return TeamCardView;
|
||||
|
||||
@@ -16,12 +16,9 @@
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.topic = options.topic;
|
||||
this.teamMemberships = options.teamMemberships;
|
||||
this.context = options.context;
|
||||
this.itemViewClass = TeamCardView.extend({
|
||||
router: options.router,
|
||||
topic: options.topic,
|
||||
maxTeamSize: this.context.maxTeamSize,
|
||||
srInfo: this.srInfo,
|
||||
countries: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
'teams/js/collections/topic',
|
||||
'teams/js/models/team',
|
||||
'teams/js/collections/team',
|
||||
'teams/js/collections/team_membership',
|
||||
'teams/js/collections/my_teams',
|
||||
'teams/js/utils/team_analytics',
|
||||
'teams/js/views/teams_tabbed_view',
|
||||
'teams/js/views/topics',
|
||||
@@ -26,7 +26,7 @@
|
||||
'teams/js/views/instructor_tools',
|
||||
'text!teams/templates/teams_tab.underscore'],
|
||||
function (Backbone, $, _, gettext, SearchFieldView, HeaderView, HeaderModel,
|
||||
TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, TeamAnalytics,
|
||||
TopicModel, TopicCollection, TeamModel, TeamCollection, MyTeamsCollection, TeamAnalytics,
|
||||
TeamsTabbedView, TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView,
|
||||
TeamMembersEditView, TeamProfileHeaderActionsView, TeamUtils, InstructorToolsView, teamsTemplate) {
|
||||
var TeamsHeaderModel = HeaderModel.extend({
|
||||
@@ -95,26 +95,22 @@
|
||||
|
||||
// Create an event queue to track team changes
|
||||
this.teamEvents = _.clone(Backbone.Events);
|
||||
|
||||
this.teamMemberships = new TeamMembershipCollection(
|
||||
this.context.userInfo.team_memberships_data,
|
||||
this.myTeamsCollection = new MyTeamsCollection(
|
||||
this.context.userInfo.teams,
|
||||
{
|
||||
teamEvents: this.teamEvents,
|
||||
url: this.context.teamMembershipsUrl,
|
||||
course_id: this.context.courseID,
|
||||
username: this.context.userInfo.username,
|
||||
privileged: this.context.userInfo.privileged,
|
||||
staff: this.context.userInfo.staff,
|
||||
parse: true
|
||||
per_page: 2,
|
||||
parse: true,
|
||||
url: this.context.myTeamsUrl
|
||||
}
|
||||
).bootstrap();
|
||||
|
||||
);
|
||||
this.myTeamsView = new MyTeamsView({
|
||||
router: this.router,
|
||||
teamEvents: this.teamEvents,
|
||||
context: this.context,
|
||||
collection: this.teamMemberships,
|
||||
teamMemberships: this.teamMemberships
|
||||
collection: this.myTeamsCollection
|
||||
});
|
||||
|
||||
this.topicsCollection = new TopicCollection(
|
||||
@@ -176,7 +172,7 @@
|
||||
// 1. If the user belongs to at least one team, jump to the "My Teams" page
|
||||
// 2. If not, then jump to the "Browse" page
|
||||
if (Backbone.history.getFragment() === '') {
|
||||
if (this.teamMemberships.length > 0) {
|
||||
if (this.myTeamsCollection.length > 0) {
|
||||
this.router.navigate('my-teams', {trigger: true});
|
||||
} else {
|
||||
this.router.navigate('browse', {trigger: true});
|
||||
@@ -351,12 +347,13 @@
|
||||
createTeamsListView: function(options) {
|
||||
var topic = options.topic,
|
||||
collection = options.collection,
|
||||
self = this,
|
||||
teamsView = new TopicTeamsView({
|
||||
router: this.router,
|
||||
context: this.context,
|
||||
model: topic,
|
||||
collection: collection,
|
||||
teamMemberships: this.teamMemberships,
|
||||
myTeamsCollection: this.myTeamsCollection,
|
||||
showSortControls: options.showSortControls
|
||||
}),
|
||||
searchFieldView = new SearchFieldView({
|
||||
|
||||
@@ -16,38 +16,44 @@
|
||||
|
||||
initialize: function(options) {
|
||||
this.showSortControls = options.showSortControls;
|
||||
this.context = options.context;
|
||||
this.myTeamsCollection = options.myTeamsCollection;
|
||||
TeamsView.prototype.initialize.call(this, options);
|
||||
},
|
||||
|
||||
canUserCreateTeam: function () {
|
||||
// Note: non-staff and non-privileged users are automatically added to any team
|
||||
// that they create. This means that if multiple team membership is
|
||||
// disabled that they cannot create a new team when they already
|
||||
// belong to one.
|
||||
return this.context.staff || this.context.privileged || this.myTeamsCollection.length === 0;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
$.when(
|
||||
this.collection.refresh(),
|
||||
this.teamMemberships.refresh()
|
||||
).done(function() {
|
||||
TeamsView.prototype.render.call(self);
|
||||
|
||||
if (self.teamMemberships.canUserCreateTeam()) {
|
||||
var message = interpolate_text(
|
||||
// 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:
|
||||
// 1. Browse teams in other topics
|
||||
// 2. search teams
|
||||
// 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}.")),
|
||||
{
|
||||
'browse_span_start': '<a class="browse-teams" href="">',
|
||||
'search_span_start': '<a class="search-teams" href="">',
|
||||
'create_span_start': '<a class="create-team" href="">',
|
||||
'span_end': '</a>'
|
||||
}
|
||||
);
|
||||
self.$el.append(_.template(teamActionsTemplate, {message: message}));
|
||||
}
|
||||
});
|
||||
this.collection.refresh().done(function() {
|
||||
TeamsView.prototype.render.call(self);
|
||||
if (self.canUserCreateTeam()) {
|
||||
var message = interpolate_text(
|
||||
// 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:
|
||||
// 1. Browse teams in other topics
|
||||
// 2. search teams
|
||||
// 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}.")),
|
||||
{
|
||||
'browse_span_start': '<a class="browse-teams" href="">',
|
||||
'search_span_start': '<a class="search-teams" href="">',
|
||||
'create_span_start': '<a class="create-team" href="">',
|
||||
'span_end': '</a>'
|
||||
}
|
||||
);
|
||||
self.$el.append(_.template(teamActionsTemplate, {message: message}));
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -68,7 +74,10 @@
|
||||
|
||||
showCreateTeamForm: function (event) {
|
||||
event.preventDefault();
|
||||
Backbone.history.navigate('topics/' + this.model.id + '/create-team', {trigger: true});
|
||||
Backbone.history.navigate(
|
||||
'topics/' + this.model.id + '/create-team',
|
||||
{trigger: true}
|
||||
);
|
||||
},
|
||||
|
||||
createHeaderView: function () {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
teamsDetailUrl: '${ teams_detail_url }',
|
||||
teamMembershipsUrl: '${ team_memberships_url }',
|
||||
teamMembershipDetailUrl: '${ team_membership_detail_url }',
|
||||
myTeamsUrl: '${ my_teams_url }',
|
||||
maxTeamSize: ${ course.teams_max_size },
|
||||
languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) },
|
||||
countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) },
|
||||
|
||||
@@ -507,6 +507,10 @@ class TestListTeamsAPI(EventTestMixin, TeamAPITestCase):
|
||||
def test_filter_topic_id(self):
|
||||
self.verify_names({'course_id': self.test_course_1.id, 'topic_id': 'topic_0'}, 200, [u'Sólar team'])
|
||||
|
||||
def test_filter_username(self):
|
||||
self.verify_names({'course_id': self.test_course_1.id, 'username': 'student_enrolled'}, 200, [u'Sólar team'])
|
||||
self.verify_names({'course_id': self.test_course_1.id, 'username': 'staff'}, 200, [])
|
||||
|
||||
@ddt.data(
|
||||
(None, 200, ['Nuclear Team', u'Sólar team', 'Wind Team']),
|
||||
('name', 200, ['Nuclear Team', u'Sólar team', 'Wind Team']),
|
||||
|
||||
@@ -107,8 +107,8 @@ class TopicsPagination(TeamAPIPagination):
|
||||
page_size = TOPICS_PER_PAGE
|
||||
|
||||
|
||||
class MembershipPagination(TeamAPIPagination):
|
||||
"""Paginate memberships. """
|
||||
class MyTeamsPagination(TeamAPIPagination):
|
||||
"""Paginate the user's teams. """
|
||||
page_size = TEAM_MEMBERSHIPS_PER_PAGE
|
||||
|
||||
|
||||
@@ -153,14 +153,15 @@ class TeamsDashboardView(GenericAPIView):
|
||||
)
|
||||
topics_data["sort_order"] = sort_order
|
||||
|
||||
# Paginate and serialize team membership data.
|
||||
team_memberships = CourseTeamMembership.get_memberships(user.username, [course.id])
|
||||
memberships_data = self._serialize_and_paginate(
|
||||
MembershipPagination,
|
||||
team_memberships,
|
||||
user = request.user
|
||||
|
||||
user_teams = CourseTeam.objects.filter(membership__user=user)
|
||||
user_teams_data = self._serialize_and_paginate(
|
||||
MyTeamsPagination,
|
||||
user_teams,
|
||||
request,
|
||||
MembershipSerializer,
|
||||
{'expand': ('team', 'user',)}
|
||||
CourseTeamSerializer,
|
||||
{'expand': ('user',)}
|
||||
)
|
||||
|
||||
context = {
|
||||
@@ -173,7 +174,7 @@ class TeamsDashboardView(GenericAPIView):
|
||||
"username": user.username,
|
||||
"privileged": has_discussion_privileges(user, course_key),
|
||||
"staff": bool(has_access(user, 'staff', course_key)),
|
||||
"team_memberships_data": memberships_data,
|
||||
"teams": user_teams_data
|
||||
},
|
||||
"topic_url": reverse(
|
||||
'topics_detail', kwargs={'topic_id': 'topic_id', 'course_id': str(course_id)}, request=request
|
||||
@@ -182,6 +183,7 @@ class TeamsDashboardView(GenericAPIView):
|
||||
"teams_url": reverse('teams_list', request=request),
|
||||
"teams_detail_url": reverse('teams_detail', args=['team_id']),
|
||||
"team_memberships_url": reverse('team_membership_list', request=request),
|
||||
"my_teams_url": reverse('teams_list', request=request),
|
||||
"team_membership_detail_url": reverse('team_membership_detail', args=['team_id', user.username]),
|
||||
"languages": [[lang[0], _(lang[1])] for lang in settings.ALL_LANGUAGES], # pylint: disable=translation-of-non-string
|
||||
"countries": list(countries),
|
||||
@@ -283,6 +285,8 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
* last_activity_at: Orders result by team activity, with most active first
|
||||
(for tie-breaking, open_slots is used, with most open slots first).
|
||||
|
||||
* username: Return teams whose membership contains the given user.
|
||||
|
||||
* page_size: Number of results to return per page.
|
||||
|
||||
* page: Page number to retrieve.
|
||||
@@ -414,6 +418,10 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
username = request.query_params.get('username', None)
|
||||
if username is not None:
|
||||
result_filter.update({'membership__user__username': username})
|
||||
|
||||
topic_id = request.query_params.get('topic_id', None)
|
||||
if topic_id is not None:
|
||||
if topic_id not in [topic['id'] for topic in course_module.teams_configuration['topics']]:
|
||||
|
||||
Reference in New Issue
Block a user