Commit a24e3cb0 by Andy Armstrong

Implement model refreshing for Teams tab

TNL-2976
parent 04d3cb98
define(['sinon', 'underscore'], function(sinon, _) {
var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest,
define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) {
'use strict';
var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectJsonRequestURL,
respondWithJson, respondWithError, respondWithTextError, respondWithNoContent;
/* These utility methods are used by Jasmine tests to create a mock server or
......@@ -69,6 +71,24 @@ define(['sinon', 'underscore'], function(sinon, _) {
};
/**
* Expect that a JSON request be made with the given URL and parameters.
* @param requests The collected requests
* @param expectedUrl The expected URL excluding the parameters
* @param expectedParameters An object representing the URL parameters
* @param requestIndex An optional index for the request (by default, the last request is used)
*/
expectJsonRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) {
var request, parameters;
if (_.isUndefined(requestIndex)) {
requestIndex = requests.length - 1;
}
request = requests[requestIndex];
parameters = new URI(request.url).query(true);
delete parameters._; // Ignore the cache-busting argument
expect(parameters).toEqual(expectedParameters);
};
/**
* Intended for use with POST requests using application/x-www-form-urlencoded.
*/
expectPostRequest = function(requests, url, body, requestIndex) {
......@@ -136,6 +156,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
'requests': fakeRequests,
'expectRequest': expectRequest,
'expectJsonRequest': expectJsonRequest,
'expectJsonRequestURL': expectJsonRequestURL,
'expectPostRequest': expectPostRequest,
'respondWithJson': respondWithJson,
'respondWithError': respondWithError,
......
......@@ -248,7 +248,7 @@ class MyTeamsTest(TeamsTabBase):
self.assertEqual(len(self.my_teams_page.team_cards), 0, msg='Expected to see no team cards')
self.assertEqual(
self.my_teams_page.q(css='.page-content-main').text,
[u'You are not currently a member of any teams.']
[u'You are not currently a member of any team.']
)
def test_member_of_a_team(self):
......@@ -530,7 +530,12 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.browse_teams_page.go_to_page(1)
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
def test_teams_membership(self):
@ddt.data(
['Moderator', False]
['Community TA', False],
['Administrator', False]
)
def test_create_team(self, role, should_join_team):
"""
Scenario: Team cards correctly reflect membership of the team.
Given I am enrolled in a course with a team configuration and a topic
......@@ -540,6 +545,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
Then I should see the correct page header
And I should see the team for that topic
And I should see that the team card shows my membership
When I visit the My Teams page
"""
teams = self.create_teams(self.topic, 1)
self.browse_teams_page.visit()
......@@ -548,11 +554,10 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.create_membership(self.user_info['username'], teams[0]['id'])
self.browser.refresh()
self.browse_teams_page.wait_for_ajax()
## TODO: fix this!
# self.assertEqual(
# self.browse_teams_page.team_cards[0].find_element_by_css_selector('.member-count').text,
# '1 / 10 Members'
# )
self.assertEqual(
self.browse_teams_page.team_cards[0].find_element_by_css_selector('.member-count').text,
'1 / 10 Members'
)
def test_navigation_links(self):
"""
......
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection'],
function(PagingCollection) {
var BaseCollection = PagingCollection.extend({
initialize: function(options) {
PagingCollection.prototype.initialize.call(this);
this.course_id = options.course_id;
this.perPage = options.per_page;
this.teamEvents = options.teamEvents;
this.teamEvents.bind('teams:update', this.onUpdate, this);
this.isStale = false;
},
onUpdate: function(event) {
this.isStale = true;
},
/**
* Refreshes the collection if it has been marked as stale.
* @param force If true, it will always refresh.
* @returns {promise} Returns a promise representing the refresh
*/
refresh: function(force) {
var self = this,
deferred = $.Deferred();
if (force || this.isStale) {
this.fetch()
.done(function() {
self.isStale = false;
deferred.resolve();
});
} else {
deferred.resolve();
}
return deferred.promise();
}
});
return BaseCollection;
});
}).call(this, define || RequireJS.define);
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/team', 'gettext'],
function(PagingCollection, TeamModel, gettext) {
var TeamCollection = PagingCollection.extend({
define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'],
function(BaseCollection, TeamModel, gettext) {
var TeamCollection = BaseCollection.extend({
initialize: function(teams, options) {
PagingCollection.prototype.initialize.call(this);
var self = this;
BaseCollection.prototype.initialize.call(this, options);
this.course_id = options.course_id;
this.server_api['topic_id'] = this.topic_id = options.topic_id;
this.server_api['expand'] = 'user';
this.perPage = options.per_page;
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
this.server_api['order_by'] = function () { return 'name'; }; // TODO surface sort order in UI
delete this.server_api['sort_order']; // Sort order is not specified for the Team API
this.server_api = _.extend(
{
topic_id: this.topic_id = options.topic_id,
expand: 'user',
course_id: function () { return encodeURIComponent(self.course_id); },
order_by: function () { return 'name'; } // TODO surface sort order in UI
},
BaseCollection.prototype.server_api
);
delete this.server_api.sort_order; // Sort order is not specified for the Team API
this.registerSortableField('name', gettext('name'));
this.registerSortableField('open_slots', gettext('open_slots'));
......@@ -21,5 +25,5 @@
model: TeamModel
});
return TeamCollection;
});
});
}).call(this, define || RequireJS.define);
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/team_membership'],
function(PagingCollection, TeamMembershipModel) {
var TeamMembershipCollection = PagingCollection.extend({
define(['teams/js/collections/base', 'teams/js/models/team_membership'],
function(BaseCollection, TeamMembershipModel) {
var TeamMembershipCollection = BaseCollection.extend({
initialize: function(team_memberships, options) {
PagingCollection.prototype.initialize.call(this);
var self = this;
BaseCollection.prototype.initialize.call(this, options);
this.course_id = options.course_id;
this.perPage = options.per_page || 10;
this.username = options.username;
this.privileged = options.privileged;
this.perPage = options.per_page || 10;
this.server_api['expand'] = 'team';
this.server_api['course_id'] = function () { return encodeURIComponent(options.course_id); };
this.server_api['username'] = this.username;
this.server_api = _.extend(
{
expand: 'team',
username: this.username,
course_id: function () { return encodeURIComponent(self.course_id); }
},
BaseCollection.prototype.server_api
);
delete this.server_api['sort_order']; // Sort order is not specified for the TeamMembership API
delete this.server_api['order_by']; // Order by is not specified for the TeamMembership API
},
......@@ -28,5 +34,5 @@
}
});
return TeamMembershipCollection;
});
});
}).call(this, define || RequireJS.define);
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/topic', 'gettext'],
function(PagingCollection, TopicModel, gettext) {
var TopicCollection = PagingCollection.extend({
define(['teams/js/collections/base', 'teams/js/models/topic', 'gettext'],
function(BaseCollection, TopicModel, gettext) {
var TopicCollection = BaseCollection.extend({
initialize: function(topics, options) {
PagingCollection.prototype.initialize.call(this);
var self = this;
BaseCollection.prototype.initialize.call(this, options);
this.course_id = options.course_id;
this.perPage = topics.results.length;
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
this.server_api['order_by'] = function () { return this.sortField; };
this.server_api = _.extend(
{
course_id: function () { return encodeURIComponent(self.course_id); },
order_by: function () { return this.sortField; }
},
BaseCollection.prototype.server_api
);
delete this.server_api['sort_order']; // Sort order is not specified for the Team API
this.registerSortableField('name', gettext('name'));
......@@ -17,8 +24,12 @@
this.registerSortableField('team_count', gettext('team count'));
},
onUpdate: function(event) {
this.isStale = event.action === 'create';
},
model: TopicModel
});
return TopicCollection;
});
});
}).call(this, define || RequireJS.define);
define(['URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic'],
function (URI, _, AjaxHelpers, TopicCollection) {
define(['backbone', 'URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic'],
function (Backbone, URI, _, AjaxHelpers, TopicCollection) {
'use strict';
describe('TopicCollection', function () {
var topicCollection;
......@@ -39,7 +39,11 @@ define(['URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/co
],
"sort_order": "name"
},
{course_id: 'my/course/id', parse: true});
{
teamEvents:_.clone(Backbone.Events),
course_id: 'my/course/id',
parse: true
});
});
var testRequestParam = function (self, param, value) {
......
......@@ -3,13 +3,13 @@ define([
'underscore',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'teams/js/views/edit_team'
], function ($, _, Backbone, AjaxHelpers, TeamEditView) {
'teams/js/views/edit_team',
'teams/js/spec_helpers/team_spec_helpers'
], function ($, _, Backbone, AjaxHelpers, TeamEditView, TeamSpecHelpers) {
'use strict';
describe('EditTeam', function () {
var teamEditView,
teamsUrl = '/api/team/v0/teams/',
var teamsUrl = '/api/team/v0/teams/',
teamsData = {
id: null,
name: "TeamName",
......@@ -22,7 +22,7 @@ define([
language: "a",
membership: []
},
verifyValidation = function (requests, fieldsData) {
verifyValidation = function (requests, teamEditView, fieldsData) {
_.each(fieldsData, function (fieldData) {
teamEditView.$(fieldData[0]).val(fieldData[1]);
});
......@@ -45,24 +45,11 @@ define([
});
expect(requests.length).toBe(0);
},
expectContent = function (selector, text) {
expect(teamEditView.$(selector).text().trim()).toBe(text);
},
verifyDropdownData = function (selector, expectedItems) {
var options = teamEditView.$(selector)[0].options;
var renderedItems = $.map(options, function( elem ) {
return [[elem.value, elem.text]];
});
for (var i = 0; i < expectedItems.length; i++) {
expect(renderedItems).toContain(expectedItems[i]);
}
};
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
spyOn(Backbone.history, 'navigate');
teamEditView = new TeamEditView({
var createTeamEditView = function() {
return new TeamEditView({
teamEvents: TeamSpecHelpers.teamEvents,
el: $('.teams-content'),
teamParams: {
teamsUrl: teamsUrl,
......@@ -73,6 +60,11 @@ define([
countries: [['c', 'ccc'], ['d', 'ddd']]
}
}).render();
};
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
spyOn(Backbone.history, 'navigate');
});
it('can render itself correctly', function () {
......@@ -82,7 +74,8 @@ define([
'.u-field-optional_description',
'.u-field-language',
'.u-field-country'
];
],
teamEditView = createTeamEditView();
_.each(fieldClasses, function (fieldClass) {
expect(teamEditView.$el.find(fieldClass).length).toBe(1);
......@@ -93,7 +86,8 @@ define([
});
it('can create a team', function () {
var requests = AjaxHelpers.requests(this);
var requests = AjaxHelpers.requests(this),
teamEditView = createTeamEditView();
teamEditView.$('.u-field-name input').val(teamsData.name);
teamEditView.$('.u-field-textarea textarea').val(teamsData.description);
......@@ -109,46 +103,49 @@ define([
});
it('shows validation error message when field is empty', function () {
var requests = AjaxHelpers.requests(this);
verifyValidation(requests, [
var requests = AjaxHelpers.requests(this),
teamEditView = createTeamEditView();
verifyValidation(requests, teamEditView, [
['.u-field-name input', 'Name', 'success'],
['.u-field-textarea textarea', '', 'error']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', '', 'error'],
['.u-field-textarea textarea', 'description', 'success']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', '', 'error'],
['.u-field-textarea textarea', '', 'error']
]);
});
it('shows validation error message when field value length exceeded the limit', function () {
var requests = AjaxHelpers.requests(this);
var teamName = new Array(500 + 1).join( '$' );
var teamDescription = new Array(500 + 1).join( '$' );
var requests = AjaxHelpers.requests(this),
teamEditView = createTeamEditView(),
teamName = new Array(500 + 1).join( '$'),
teamDescription = new Array(500 + 1).join( '$' );
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', teamName, 'error'],
['.u-field-textarea textarea', 'description', 'success']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', 'name', 'success'],
['.u-field-textarea textarea', teamDescription, 'error']
]);
teamEditView.render();
verifyValidation(requests, [
verifyValidation(requests, teamEditView, [
['.u-field-name input', teamName, 'error'],
['.u-field-textarea textarea', teamDescription, 'error']
]);
});
it("shows an error message for HTTP 500", function () {
var requests = AjaxHelpers.requests(this);
var teamEditView = createTeamEditView(),
requests = AjaxHelpers.requests(this);
teamEditView.$('.u-field-name input').val(teamsData.name);
teamEditView.$('.u-field-textarea textarea').val(teamsData.description);
......@@ -163,6 +160,7 @@ define([
});
it("changes route on cancel click", function () {
var teamEditView = createTeamEditView();
teamEditView.$('.create-team.form-actions .action-cancel').click();
expect(Backbone.history.navigate.calls[0].args).toContain('topics/awesomeness');
});
......
......@@ -3,8 +3,9 @@ define([
'teams/js/collections/team',
'teams/js/collections/team_membership',
'teams/js/views/my_teams',
'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, MyTeamsView, TeamSpecHelpers) {
'teams/js/spec_helpers/team_spec_helpers',
'common/js/spec_helpers/ajax_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, MyTeamsView, TeamSpecHelpers, AjaxHelpers) {
'use strict';
describe('My Teams View', function () {
beforeEach(function () {
......@@ -28,26 +29,51 @@ define([
it('can render itself', function () {
var teamMembershipsData = TeamSpecHelpers.createMockTeamMembershipsData(1, 5),
teamMemberships = TeamSpecHelpers.createMockTeamMemberships(teamMembershipsData),
teamsView = createMyTeamsView({
myTeamsView = createMyTeamsView({
teams: teamMemberships,
teamMemberships: teamMemberships
});
TeamSpecHelpers.verifyCards(teamsView, teamMembershipsData);
TeamSpecHelpers.verifyCards(myTeamsView, teamMembershipsData);
// Verify that there is no header or footer
expect(teamsView.$('.teams-paging-header').text().trim()).toBe('');
expect(teamsView.$('.teams-paging-footer').text().trim()).toBe('');
expect(myTeamsView.$('.teams-paging-header').text().trim()).toBe('');
expect(myTeamsView.$('.teams-paging-footer').text().trim()).toBe('');
});
it('shows a message when the user is not a member of any teams', function () {
var teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
teamsView = createMyTeamsView({
myTeamsView = createMyTeamsView({
teams: teamMemberships,
teamMemberships: teamMemberships
});
TeamSpecHelpers.verifyCards(teamsView, []);
expect(teamsView.$el.text().trim()).toBe('You are not currently a member of any 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
});
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' });
myTeamsView.render();
AjaxHelpers.expectJsonRequestURL(
requests,
'foo',
{
expand : 'team',
username : 'testUser',
course_id : 'my/course/id',
page : '1',
page_size : '10'
}
);
AjaxHelpers.respondWithJson(requests, {});
});
});
});
define([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/spec_helpers/team_spec_helpers',
'teams/js/views/team_join'
], function (_, AjaxHelpers, TeamModel, TeamSpecHelpers, TeamJoinView) {
'backbone', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/views/team_join', 'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, _, AjaxHelpers, TeamModel, TeamJoinView, TeamSpecHelpers) {
'use strict';
describe('TeamJoinView', function () {
var createTeamsUrl,
......@@ -63,6 +62,7 @@ define([
var teamJoinView = new TeamJoinView(
{
courseID: TeamSpecHelpers.testCourseID,
teamEvents: TeamSpecHelpers.teamEvents,
model: model,
teamsUrl: createTeamsUrl(teamId),
maxTeamSize: maxTeamSize,
......
......@@ -5,16 +5,18 @@ define([
], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) {
'use strict';
describe('TeamProfileView', function () {
var profileView, createTeamProfileView, createTeamModelData, teamModel,
var profileView, createTeamProfileView, createTeamModelData, clickLeaveTeam,
teamModel,
leaveTeamLinkSelector = '.leave-team-link',
DEFAULT_MEMBERSHIP = [
{
'user': {
'username': 'bilbo',
'profile_image': {
'has_image': true,
'image_url_medium': '/image-url'
}
'image_url_medium': '/image-url'
}
}
}
];
......@@ -38,6 +40,7 @@ define([
createTeamProfileView = function(requests, options) {
teamModel = new TeamModel(createTeamModelData(options), { parse: true });
profileView = new TeamProfileView({
teamEvents: TeamSpecHelpers.teamEvents,
courseID: TeamSpecHelpers.testCourseID,
model: teamModel,
maxTeamSize: options.maxTeamSize || 3,
......@@ -71,6 +74,21 @@ define([
return profileView;
};
clickLeaveTeam = function(requests, view) {
expect(view.$(leaveTeamLinkSelector).length).toBe(1);
// click on Leave Team link under Team Details
view.$(leaveTeamLinkSelector).click();
// expect a request to DELETE the team membership
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'api/team/v0/team_membership/test-team,bilbo');
AjaxHelpers.respondWithNoContent(requests);
// expect a request to refetch the user's team memberships
AjaxHelpers.expectJsonRequest(requests, 'GET', '/api/team/v0/teams/test-team');
AjaxHelpers.respondWithJson(requests, createTeamModelData({country: 'US', language: 'en'}));
};
describe('DiscussionsView', function() {
it('can render itself', function () {
var requests = AjaxHelpers.requests(this),
......@@ -84,6 +102,7 @@ define([
expect(view.$('.new-post-btn').length).toEqual(0);
teamModel.set('membership', DEFAULT_MEMBERSHIP); // This should re-render the view.
view.render();
expect(view.$('.new-post-btn').length).toEqual(1);
});
......@@ -92,7 +111,7 @@ define([
view = createTeamProfileView(requests, {membership: DEFAULT_MEMBERSHIP});
expect(view.$('.new-post-btn').length).toEqual(1);
teamModel.set('membership', []);
clickLeaveTeam(requests, view);
expect(view.$('.new-post-btn').length).toEqual(0);
});
});
......@@ -119,7 +138,10 @@ define([
assertTeamDetails(view, 0, false);
expect(view.$('.team-user-membership-status').length).toBe(0);
// Verify that the leave team link is not present.
expect(view.$(leaveTeamLinkSelector).length).toBe(0);
});
it('cannot see the country & language if empty', function() {
var requests = AjaxHelpers.requests(this);
var view = createTeamProfileView(requests, {});
......@@ -145,29 +167,21 @@ define([
// assert user profile page url.
expect(view.$('.member-profile').attr('href')).toBe('/u/bilbo');
//Verify that the leave team link is present
expect(view.$(leaveTeamLinkSelector).text()).toContain('Leave Team');
});
it('can leave team successfully', function() {
var requests = AjaxHelpers.requests(this);
var leaveTeamLinkSelector = '.leave-team-link';
var view = createTeamProfileView(
requests, { country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
);
assertTeamDetails(view, 1, true);
expect(view.$(leaveTeamLinkSelector).length).toBe(1);
// click on Leave Team link under Team Details
view.$(leaveTeamLinkSelector).click();
// response to DELETE
AjaxHelpers.respondWithNoContent(requests);
// response to model fetch request
AjaxHelpers.respondWithJson(requests, createTeamModelData({country: 'US', language: 'en'}));
clickLeaveTeam(requests, view);
assertTeamDetails(view, 0, false);
});
it('shows correct error messages', function () {
var requests = AjaxHelpers.requests(this);
......
......@@ -5,11 +5,8 @@ define(['jquery',
function ($, _, TopicCardView, Topic) {
describe('topic card view', function () {
var view;
beforeEach(function () {
spyOn(TopicCardView.prototype, 'action');
view = new TopicCardView({
var createTopicCardView = function() {
return new TopicCardView({
model: new Topic({
'id': 'renewables',
'name': 'Renewable Energy',
......@@ -17,9 +14,14 @@ define(['jquery',
'team_count': 34
})
});
};
beforeEach(function () {
spyOn(TopicCardView.prototype, 'action');
});
it('can render itself', function () {
var view = createTopicCardView();
expect(view.$el).toHaveClass('square-card');
expect(view.$el.find('.card-title').text()).toContain('Renewable Energy');
expect(view.$el.find('.card-description').text()).toContain('changes in <ⓡⓔⓝⓔⓦⓐⓑⓛⓔ> ʎƃɹǝuǝ');
......@@ -28,6 +30,7 @@ define(['jquery',
});
it('navigates when action button is clicked', function () {
var view = createTopicCardView();
view.$el.find('.action').trigger('click');
// TODO test actual navigation once implemented
expect(view.action).toHaveBeenCalled();
......
......@@ -3,8 +3,9 @@ define([
'teams/js/collections/team',
'teams/js/collections/team_membership',
'teams/js/views/topic_teams',
'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers) {
'teams/js/spec_helpers/team_spec_helpers',
'common/js/spec_helpers/ajax_helpers'
], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers, AjaxHelpers) {
'use strict';
describe('Topic Teams View', function () {
var createTopicTeamsView = function(options) {
......@@ -21,6 +22,24 @@ define([
}).render();
};
var verifyActions = function(teamsView, options) {
if (!options) {
options = {showActions: true};
}
var expectedTitle = 'Are you having trouble finding a team to join?',
expectedMessage = 'Try browsing all teams or searching team descriptions. 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.showActions) {
expect(title).toBe(expectedTitle);
expect(message).toBe(expectedMessage);
} else {
expect(title).not.toBe(expectedTitle);
expect(message).not.toBe(expectedMessage);
}
};
beforeEach(function () {
setFixtures('<div class="teams-container"></div>');
});
......@@ -39,12 +58,7 @@ define([
expect(footerEl).not.toHaveClass('hidden');
TeamSpecHelpers.verifyCards(teamsView, testTeamData);
expect(teamsView.$('.title').text()).toBe('Are you having trouble finding a team to join?');
expect(teamsView.$('.copy').text()).toBe(
"Try browsing all teams or searching team descriptions. If you " +
"still can't find a team to join, create a new team in this topic."
);
verifyActions(teamsView);
});
it('can browse all teams', function () {
......@@ -74,9 +88,7 @@ define([
it('does not show actions for a user already in a team', function () {
var teamsView = createTopicTeamsView({});
expect(teamsView.$el.text()).not.toContain(
'Are you having trouble finding a team to join?'
);
verifyActions(teamsView, {showActions: false});
});
it('shows actions for a privileged user already in a team', function () {
......@@ -85,9 +97,32 @@ define([
{ privileged: true }
),
teamsView = createTopicTeamsView({ teamMemberships: staffMembership });
expect(teamsView.$el.text()).toContain(
'Are you having trouble finding a team to join?'
verifyActions(teamsView);
});
/*
// TODO: make this ready for prime time
it('refreshes when the team membership changes', function() {
var requests = AjaxHelpers.requests(this),
teamMemberships = TeamSpecHelpers.createMockTeamMemberships([]),
teamsView = createTopicTeamsView({ teamMemberships: teamMemberships });
verifyActions(teamsView, {showActions: true});
teamMemberships.teamEvents.trigger('teams:update', { action: 'create' });
teamsView.render();
AjaxHelpers.expectJsonRequestURL(
requests,
'foo',
{
expand : 'team',
username : 'testUser',
course_id : 'my/course/id',
page : '1',
page_size : '10'
}
);
AjaxHelpers.respondWithJson(requests, {});
verifyActions(teamsView, {showActions: false});
});
*/
});
});
define([
'teams/js/collections/topic', 'teams/js/views/topics'
], function (TopicCollection, TopicsView) {
'backbone', 'teams/js/collections/topic', 'teams/js/views/topics',
'teams/js/spec_helpers/team_spec_helpers'
], function (Backbone, TopicCollection, TopicsView, TeamSpecHelpers) {
'use strict';
describe('TopicsView', function () {
var initialTopics, topicCollection, topicsView,
var initialTopics, topicCollection, createTopicsView,
generateTopics = function (startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
return {
......@@ -15,6 +16,14 @@ define([
});
};
createTopicsView = function() {
return new TopicsView({
teamEvents: TeamSpecHelpers.teamEvents,
el: '.topics-container',
collection: topicCollection
}).render();
};
beforeEach(function () {
setFixtures('<div class="topics-container"></div>');
initialTopics = generateTopics(1, 5);
......@@ -26,13 +35,17 @@ define([
"start": 0,
"results": initialTopics
},
{course_id: 'my/course/id', parse: true}
{
teamEvents: TeamSpecHelpers.teamEvents,
course_id: 'my/course/id',
parse: true
}
);
topicsView = new TopicsView({el: '.topics-container', collection: topicCollection}).render();
});
it('can render the first of many pages', function () {
var footerEl = topicsView.$('.topics-paging-footer'),
var topicsView = createTopicsView(),
footerEl = topicsView.$('.topics-paging-footer'),
topicCards = topicsView.$('.topic-card');
expect(topicsView.$('.topics-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
_.each(initialTopics, function (topic, index) {
......
define([
'backbone',
'underscore',
'teams/js/collections/team',
'teams/js/collections/team_membership',
], function (_, TeamCollection, TeamMembershipCollection) {
], function (Backbone, _, TeamCollection, TeamMembershipCollection) {
'use strict';
var createMockPostResponse, createMockDiscussionResponse, createAnnotatedContentInfo, createMockThreadResponse,
testCourseID = 'course/1',
testUser = 'testUser',
testTeamDiscussionID = "12345",
teamEvents = _.clone(Backbone.Events),
testCountries = [
['', ''],
['US', 'United States'],
......@@ -47,6 +49,7 @@ define([
results: teamData
},
{
teamEvents: teamEvents,
course_id: 'my/course/id',
parse: true
}
......@@ -79,6 +82,7 @@ define([
results: teamMembershipData
},
_.extend(_.extend({}, {
teamEvents: teamEvents,
course_id: 'my/course/id',
parse: true,
url: 'api/teams/team_memberships',
......@@ -225,6 +229,7 @@ define([
};
return {
teamEvents: teamEvents,
testCourseID: testCourseID,
testUser: testUser,
testCountries: testCountries,
......
......@@ -20,6 +20,7 @@
},
initialize: function(options) {
this.teamEvents = options.teamEvents;
this.courseID = options.teamParams.courseID;
this.topicID = options.teamParams.topicID;
this.collection = options.collection;
......@@ -28,7 +29,7 @@
this.countries = options.teamParams.countries;
this.primaryButtonTitle = options.primaryButtonTitle || 'Submit';
_.bindAll(this, 'goBackToTopic', 'createTeam');
_.bindAll(this, 'goBackToTopic', 'createTeam');
this.teamModel = new TeamModel({});
this.teamModel.url = this.teamsUrl;
......@@ -113,6 +114,10 @@
this.teamModel.save(data, { wait: true })
.done(function(result) {
view.teamEvents.trigger('teams:update', {
action: 'create',
team: result
});
Backbone.history.navigate(
'teams/' + view.topicID + '/' + view.teamModel.id,
{trigger: true}
......@@ -175,10 +180,10 @@
if (screenReaderMessage) {
this.$('.screen-reader-message').text(screenReaderMessage);
}
},
},
goBackToTopic: function () {
Backbone.history.navigate('topics/' + this.topicID, {trigger: true});
goBackToTopic: function () {
Backbone.history.navigate('topics/' + this.topicID, {trigger: true});
}
});
});
......
......@@ -5,10 +5,17 @@
function (Backbone, gettext, TeamsView) {
var MyTeamsView = TeamsView.extend({
render: function() {
TeamsView.prototype.render.call(this);
if (this.collection.length === 0) {
this.$el.append('<p>' + gettext('You are not currently a member of any teams.') + '</p>');
var view = this;
if (this.collection.isStale) {
this.$el.html('');
}
this.collection.refresh()
.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>');
}
});
return this;
},
......
;(function (define) {
'use strict';
'use strict';
define(['backbone',
'underscore',
'gettext',
'teams/js/views/team_utils',
'text!teams/templates/team-join.underscore'],
function (Backbone, _, gettext, TeamUtils, teamJoinTemplate) {
return Backbone.View.extend({
define(['backbone',
'underscore',
'gettext',
'teams/js/views/team_utils',
'text!teams/templates/team-join.underscore'],
function (Backbone, _, gettext, TeamUtils, teamJoinTemplate) {
return Backbone.View.extend({
errorMessage: gettext("An error occurred. Try again."),
alreadyMemberMessage: gettext("You already belong to another team."),
teamFullMessage: gettext("This team is full."),
errorMessage: gettext("An error occurred. Try again."),
alreadyMemberMessage: gettext("You already belong to another team."),
teamFullMessage: gettext("This team is full."),
events: {
"click .action-primary": "joinTeam"
},
events: {
"click .action-primary": "joinTeam"
},
initialize: function(options) {
this.template = _.template(teamJoinTemplate);
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.currentUsername = options.currentUsername;
this.teamMembershipsUrl = options.teamMembershipsUrl;
_.bindAll(this, 'render', 'joinTeam', 'getUserTeamInfo');
this.listenTo(this.model, "change", this.render);
},
initialize: function(options) {
this.teamEvents = options.teamEvents;
this.template = _.template(teamJoinTemplate);
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.currentUsername = options.currentUsername;
this.teamMembershipsUrl = options.teamMembershipsUrl;
_.bindAll(this, 'render', 'joinTeam', 'getUserTeamInfo');
this.listenTo(this.model, "change", this.render);
},
render: function() {
var message,
showButton,
teamHasSpace;
render: function() {
var view = this,
message,
showButton,
teamHasSpace;
this.getUserTeamInfo(this.currentUsername, view.maxTeamSize).done(function (info) {
teamHasSpace = info.teamHasSpace;
var view = this;
this.getUserTeamInfo(this.currentUsername, view.maxTeamSize).done(function (info) {
teamHasSpace = info.teamHasSpace;
// if user is the member of current team then we wouldn't show anything
if (!info.memberOfCurrentTeam) {
showButton = !info.alreadyMember && teamHasSpace;
// if user is the member of current team then we wouldn't show anything
if (!info.memberOfCurrentTeam) {
showButton = !info.alreadyMember && teamHasSpace;
if (info.alreadyMember) {
message = info.memberOfCurrentTeam ? '' : view.alreadyMemberMessage;
} else if (!teamHasSpace) {
message = view.teamFullMessage;
}
}
if (info.alreadyMember) {
message = info.memberOfCurrentTeam ? '' : view.alreadyMemberMessage;
} else if (!teamHasSpace) {
message = view.teamFullMessage;
}
}
view.$el.html(view.template({showButton: showButton, message: message}));
});
return view;
},
view.$el.html(view.template({showButton: showButton, message: message}));
});
return view;
},
joinTeam: function () {
var view = this;
$.ajax({
type: 'POST',
url: view.teamMembershipsUrl,
data: {'username': view.currentUsername, 'team_id': view.model.get('id')}
}).done(function (data) {
view.model.fetch()
.done(function() {
view.teamEvents.trigger('teams:update', {
action: 'join',
team: view.model
});
});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
},
joinTeam: function () {
var view = this;
$.ajax({
type: 'POST',
url: view.teamMembershipsUrl,
data: {'username': view.currentUsername, 'team_id': view.model.get('id')}
}).done(function (data) {
view.model.fetch({});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
},
getUserTeamInfo: function (username, maxTeamSize) {
var deferred = $.Deferred();
var info = {
alreadyMember: false,
memberOfCurrentTeam: false,
teamHasSpace: false
};
getUserTeamInfo: function (username, maxTeamSize) {
var deferred = $.Deferred();
var info = {
alreadyMember: false,
memberOfCurrentTeam: false,
teamHasSpace: false
};
info.memberOfCurrentTeam = TeamUtils.isUserMemberOfTeam(this.model.get('membership'), username);
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;
if (info.memberOfCurrentTeam) {
info.alreadyMember = true;
info.memberOfCurrentTeam = true;
deferred.resolve(info);
} else {
if (teamHasSpace) {
var view = this;
$.ajax({
type: 'GET',
url: view.teamMembershipsUrl,
data: {'username': username, 'course_id': view.courseID}
}).done(function (data) {
info.alreadyMember = (data.count > 0);
info.memberOfCurrentTeam = false;
info.teamHasSpace = teamHasSpace;
deferred.resolve(info);
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
deferred.reject();
});
} else {
deferred.resolve(info);
}
}
if (info.memberOfCurrentTeam) {
info.alreadyMember = true;
info.memberOfCurrentTeam = true;
deferred.resolve(info);
} else {
if (teamHasSpace) {
var view = this;
$.ajax({
type: 'GET',
url: view.teamMembershipsUrl,
data: {'username': username, 'course_id': view.courseID}
}).done(function (data) {
info.alreadyMember = (data.count > 0);
info.memberOfCurrentTeam = false;
info.teamHasSpace = teamHasSpace;
deferred.resolve(info);
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
deferred.reject();
});
} else {
deferred.resolve(info);
}
}
return deferred.promise();
}
});
});
return deferred.promise();
}
});
});
}).call(this, define || RequireJS.define);
......@@ -16,7 +16,7 @@
'click .leave-team-link': 'leaveTeam'
},
initialize: function (options) {
this.listenTo(this.model, "change", this.render);
this.teamEvents = options.teamEvents;
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.requestUsername = options.requestUsername;
......@@ -26,13 +26,13 @@
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages);
this.listenTo(this.model, "change", this.render);
},
render: function () {
var memberships = this.model.get('membership'),
discussionTopicID = this.model.get('discussion_topic_id'),
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername);
this.$el.html(_.template(teamTemplate, {
courseID: this.courseID,
discussionTopicID: discussionTopicID,
......@@ -73,10 +73,16 @@
event.preventDefault();
var view = this;
$.ajax({
type: 'DELETE',
url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
type: 'DELETE',
url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
}).done(function (data) {
view.model.fetch({});
view.model.fetch()
.done(function() {
view.teamEvents.trigger('teams:update', {
action: 'leave',
team: view.model
});
});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
......
......@@ -81,9 +81,13 @@
router.route.apply(router, route);
});
// Create an event queue to track team changes
this.teamEvents = _.clone(Backbone.Events);
this.teamMemberships = new TeamMembershipCollection(
this.userInfo.team_memberships_data,
{
teamEvents: this.teamEvents,
url: this.teamMembershipsUrl,
course_id: this.courseID,
username: this.userInfo.username,
......@@ -94,6 +98,7 @@
this.myTeamsView = new MyTeamsView({
router: this.router,
teamEvents: this.teamEvents,
collection: this.teamMemberships,
teamMemberships: this.teamMemberships,
maxTeamSize: this.maxTeamSize,
......@@ -107,12 +112,18 @@
this.topicsCollection = new TopicCollection(
this.topics,
{url: options.topicsUrl, course_id: this.courseID, parse: true}
{
teamEvents: this.teamEvents,
url: options.topicsUrl,
course_id: this.courseID,
parse: true
}
).bootstrap();
this.topicsView = new TopicsView({
collection: this.topicsCollection,
router: this.router
router: this.router,
teamEvents: this.teamEvents,
collection: this.topicsCollection
});
this.mainView = this.tabbedView = new ViewWithHeader({
......@@ -196,6 +207,7 @@
})
}),
main: new TeamEditView({
teamEvents: self.teamEvents,
tagName: 'create-new-team',
teamParams: teamsView.main.teamParams,
primaryButtonTitle: 'Create'
......@@ -220,6 +232,7 @@
this.getTopic(topicID)
.done(function(topic) {
var collection = new TeamCollection([], {
teamEvents: self.teamEvents,
course_id: self.courseID,
topic_id: topicID,
url: self.teamsUrl,
......@@ -229,7 +242,7 @@
collection.goTo(1)
.done(function() {
var teamsView = new TopicTeamsView({
router: router,
router: self.router,
topic: topic,
collection: collection,
teamMemberships: self.teamMemberships,
......@@ -278,25 +291,26 @@
self.getTopic(topicID).done(function(topic) {
self.getTeam(teamID, true).done(function(team) {
var view = new TeamProfileView({
teamEvents: self.teamEvents,
router: self.router,
courseID: courseID,
model: team,
maxTeamSize: self.maxTeamSize,
isPrivileged: self.userInfo.privileged,
requestUsername: self.userInfo.username,
countries: self.countries,
languages: self.languages,
teamMembershipDetailUrl: self.teamMembershipDetailUrl
});
var teamJoinView = new TeamJoinView({
teamEvents: self.teamEvents,
courseID: courseID,
model: team,
maxTeamSize: self.maxTeamSize,
isPrivileged: self.userInfo.privileged,
requestUsername: self.userInfo.username,
countries: self.countries,
languages: self.languages,
teamMembershipDetailUrl: self.teamMembershipDetailUrl
});
var teamJoinView = new TeamJoinView(
{
courseID: courseID,
model: team,
teamsUrl: self.teamsUrl,
maxTeamSize: self.maxTeamSize,
currentUsername: self.userInfo.username,
teamMembershipsUrl: self.teamMembershipsUrl
}
);
model: team,
teamsUrl: self.teamsUrl,
maxTeamSize: self.maxTeamSize,
currentUsername: self.userInfo.username,
teamMembershipsUrl: self.teamMembershipsUrl
});
deferred.resolve(
self.createViewWithHeader(
{
......
......@@ -17,20 +17,26 @@
},
render: function() {
TeamsView.prototype.render.call(this);
if (this.teamMemberships.canUserCreateTeam()) {
var message = interpolate_text(
_.escape(gettext("Try {browse_span_start}browsing all teams{span_end} or {search_span_start}searching team descriptions{span_end}. 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>'
}
);
this.$el.append(_.template(teamActionsTemplate, {message: message}));
}
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(
_.escape(gettext("Try {browse_span_start}browsing all teams{span_end} or {search_span_start}searching team descriptions{span_end}. 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;
},
......
;(function (define) {
'use strict';
define([
'gettext',
'teams/js/views/topic_card',
'common/js/components/views/paginated_view'
], function (TopicCardView, PaginatedView) {
], function (gettext, TopicCardView, PaginatedView) {
var TopicsView = PaginatedView.extend({
type: 'topics',
......@@ -18,6 +19,16 @@
srInfo: this.srInfo
});
PaginatedView.prototype.initialize.call(this);
},
render: function() {
var self = this;
this.collection.refresh()
.done(function() {
self.collection.isStale = false;
PaginatedView.prototype.render.call(self);
});
return this;
}
});
return TopicsView;
......
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