Show inline discussion component on Team view

TNL-2515
This commit is contained in:
Andy Armstrong
2015-07-21 17:39:44 -04:00
parent df936837ae
commit 63da1907eb
34 changed files with 1492 additions and 604 deletions

View File

@@ -339,6 +339,11 @@ def single_thread(request, course_key, discussion_id, thread_id):
raise Http404
raise
# Verify that the student has access to this thread if belongs to a course discussion module
thread_context = getattr(thread, "context", "course")
if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id):
raise Http404
# verify that the thread belongs to the requesting student's cohort
if is_commentable_cohorted(course_key, discussion_id) and not is_moderator:
user_group_id = get_cohort_id(request.user, course_key)

View File

@@ -16,6 +16,10 @@
country: '',
language: '',
membership: []
},
initialize: function(options) {
this.url = options.url;
}
});
return Team;

View File

@@ -10,6 +10,10 @@
description: '',
team_count: 0,
id: ''
},
initialize: function(options) {
this.url = options.url;
}
});
return Topic;

View File

@@ -102,10 +102,10 @@ define([
teamEditView.$('.create-team.form-actions .action-primary').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', teamsUrl, teamsData);
AjaxHelpers.respondWithJson(requests, teamsData);
AjaxHelpers.respondWithJson(requests, _.extend(_.extend({}, teamsData), { id: '123'}));
expect(teamEditView.$('.create-team.wrapper-msg .copy').text().trim().length).toBe(0);
expect(Backbone.history.navigate.calls[0].args).toContain('topics/awesomeness');
expect(Backbone.history.navigate.calls[0].args).toContain('teams/awesomeness/123');
});
it('shows validation error message when field is empty', function () {

View File

@@ -2,17 +2,17 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
function($, Backbone, TeamsTabFactory) {
'use strict';
describe("Teams tab", function() {
describe("Teams Tab Factory", function() {
var teamsTab;
beforeEach(function() {
setFixtures('<section class="teams-content"></section>');
teamsTab = new TeamsTabFactory({
topics: {results: []},
topics_url: '',
teams_url: '',
topicsUrl: '',
teamsUrl: '',
maxTeamSize: 9999,
course_id: 'edX/DemoX/Demo_Course'
courseID: 'edX/DemoX/Demo_Course'
});
});

View File

@@ -1,80 +0,0 @@
define([
'jquery',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'teams/js/views/teams_tab'
], function ($, Backbone, AjaxHelpers, TeamsTabView) {
'use strict';
describe('TeamsTab', function () {
var teamsTabView,
expectContent = function (text) {
expect(teamsTabView.$('.page-content-main').text()).toContain(text);
},
expectHeader = function (text) {
expect(teamsTabView.$('.teams-header').text()).toContain(text);
},
expectError = function (text) {
expect(teamsTabView.$('.warning').text()).toContain(text);
},
expectFocus = function (element) {
expect(element.focus).toHaveBeenCalled();
};
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
teamsTabView = new TeamsTabView({
el: $('.teams-content'),
topics: {
count: 1,
num_pages: 1,
current_page: 1,
start: 0,
results: [{
description: 'test description',
name: 'test topic',
id: 'test_id',
team_count: 0
}]
},
topic_url: 'api/topics/topic_id,course_id',
topics_url: 'topics_url',
teams_url: 'teams_url',
course_id: 'test/course/id'
}).render();
Backbone.history.start();
spyOn($.fn, 'focus');
});
afterEach(function () {
Backbone.history.stop();
});
it('shows the teams tab initially', function () {
expectHeader('See all teams in your course, organized by topic');
expectContent('This is the new Teams tab.');
});
it('can switch tabs', function () {
teamsTabView.$('a.nav-item[data-url="browse"]').click();
expectContent('test description');
teamsTabView.$('a.nav-item[data-url="teams"]').click();
expectContent('This is the new Teams tab.');
});
it('displays and focuses an error message when trying to navigate to a nonexistent route', function () {
teamsTabView.router.navigate('test', {trigger: true});
expectError('The page "test" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () {
var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('topics/test', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/test,course_id', null);
AjaxHelpers.respondWithError(requests, 404);
expectError('The topic "test" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
});
});

View File

@@ -0,0 +1,214 @@
define([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/views/team_discussion',
'teams/js/spec_helpers/team_discussion_helpers',
'xmodule_js/common_static/coffee/spec/discussion/discussion_spec_helper'
], function (_, AjaxHelpers, TeamDiscussionView, TeamDiscussionSpecHelper, DiscussionSpecHelper) {
'use strict';
describe('TeamDiscussionView', function() {
var discussionView, createDiscussionView, createPost, expandReplies, postReply;
beforeEach(function() {
setFixtures('<div class="discussion-module""></div>');
$('.discussion-module').data('course-id', TeamDiscussionSpecHelper.testCourseID);
$('.discussion-module').data('discussion-id', TeamDiscussionSpecHelper.testTeamDiscussionID);
$('.discussion-module').data('user-create-comment', true);
$('.discussion-module').data('user-create-subcomment', true);
DiscussionSpecHelper.setUnderscoreFixtures();
});
createDiscussionView = function(requests, threads) {
discussionView = new TeamDiscussionView({
el: '.discussion-module'
});
discussionView.render();
AjaxHelpers.expectRequest(
requests, 'GET',
interpolate(
'/courses/%(courseID)s/discussion/forum/%(discussionID)s/inline?page=1&ajax=1',
{
courseID: TeamDiscussionSpecHelper.testCourseID,
discussionID: TeamDiscussionSpecHelper.testTeamDiscussionID
},
true
)
);
AjaxHelpers.respondWithJson(requests, TeamDiscussionSpecHelper.createMockDiscussionResponse(threads));
return discussionView;
};
createPost = function(requests, view, title, body, threadID) {
title = title || "Test title";
body = body || "Test body";
threadID = threadID || "999";
view.$('.new-post-button').click();
view.$('.js-post-title').val(title);
view.$('.js-post-body textarea').val(body);
view.$('.submit').click();
AjaxHelpers.expectRequest(
requests, 'POST',
interpolate(
'/courses/%(courseID)s/discussion/%(discussionID)s/threads/create?ajax=1',
{
courseID: TeamDiscussionSpecHelper.testCourseID,
discussionID: TeamDiscussionSpecHelper.testTeamDiscussionID
},
true
),
interpolate(
'thread_type=discussion&title=%(title)s&body=%(body)s&anonymous=false&anonymous_to_peers=false&auto_subscribe=true',
{
title: title.replace(/ /g, '+'),
body: body.replace(/ /g, '+')
},
true
)
);
AjaxHelpers.respondWithJson(requests, {
content: TeamDiscussionSpecHelper.createMockPostResponse({
id: threadID,
title: title,
body: body
}),
annotated_content_info: TeamDiscussionSpecHelper.createAnnotatedContentInfo()
});
};
expandReplies = function(requests, view, threadID) {
view.$('.forum-thread-expand').first().click();
AjaxHelpers.expectRequest(
requests, 'GET',
interpolate(
'/courses/%(courseID)s/discussion/forum/%(discussionID)s/threads/%(threadID)s?ajax=1&resp_skip=0&resp_limit=25',
{
courseID: TeamDiscussionSpecHelper.testCourseID,
discussionID: TeamDiscussionSpecHelper.testTeamDiscussionID,
threadID: threadID || "999"
},
true
)
);
AjaxHelpers.respondWithJson(requests, {
content: TeamDiscussionSpecHelper.createMockThreadResponse(),
annotated_content_info: TeamDiscussionSpecHelper.createAnnotatedContentInfo()
});
};
postReply = function(requests, view, reply, threadID) {
var replyForm = view.$('.discussion-reply-new').first();
replyForm.find('.reply-body textarea').val(reply);
replyForm.find('.discussion-submit-post').click();
AjaxHelpers.expectRequest(
requests, 'POST',
interpolate(
'/courses/%(courseID)s/discussion/threads/%(threadID)s/reply?ajax=1',
{
courseID: TeamDiscussionSpecHelper.testCourseID,
threadID: threadID || "999"
},
true
),
'body=' + reply.replace(/ /g, '+')
);
AjaxHelpers.respondWithJson(requests, {
content: TeamDiscussionSpecHelper.createMockThreadResponse({
body: reply,
comments_count: 1
}),
"annotated_content_info": TeamDiscussionSpecHelper.createAnnotatedContentInfo()
});
};
it('can render itself', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests);
expect(view.$('.discussion-thread').length).toEqual(3);
});
it('can create a new post', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests),
testTitle = 'New Post',
testBody = 'New post body',
newThreadElement;
createPost(requests, view, testTitle, testBody);
// Expect the first thread to be the new post
expect(view.$('.discussion-thread').length).toEqual(4);
newThreadElement = view.$('.discussion-thread').first();
expect(newThreadElement.find('.post-header-content h1').text().trim()).toEqual(testTitle);
expect(newThreadElement.find('.post-body').text().trim()).toEqual(testBody);
});
it('can post a reply', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests),
testReply = "Test reply",
testThreadID = "1";
expandReplies(requests, view, testThreadID);
postReply(requests, view, testReply, testThreadID);
expect(view.$('.discussion-response .response-body').text().trim()).toBe(testReply);
});
it('can post a reply to a new post', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests, []),
testReply = "Test reply";
createPost(requests, view);
expandReplies(requests, view);
postReply(requests, view, testReply);
expect(view.$('.discussion-response .response-body').text().trim()).toBe(testReply);
});
it('cannot move an existing thread to a different topic', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests),
postTopicButton, updatedThreadElement,
updatedTitle = 'Updated title',
updatedBody = 'Updated body',
testThreadID = "1";
expandReplies(requests, view, testThreadID);
view.$('.action-more .icon').first().click();
view.$('.action-edit').first().click();
postTopicButton = view.$('.post-topic');
expect(postTopicButton.length).toBe(0);
view.$('.js-post-post-title').val(updatedTitle);
view.$('.js-post-body textarea').val(updatedBody);
view.$('.submit').click();
AjaxHelpers.expectRequest(
requests, 'POST',
interpolate(
'/courses/%(courseID)s/discussion/%(discussionID)s/threads/create?ajax=1',
{
courseID: TeamDiscussionSpecHelper.testCourseID,
discussionID: TeamDiscussionSpecHelper.testTeamDiscussionID
},
true
),
'thread_type=discussion&title=&body=Updated+body&anonymous=false&anonymous_to_peers=false&auto_subscribe=true'
);
AjaxHelpers.respondWithJson(requests, {
content: TeamDiscussionSpecHelper.createMockPostResponse({
id: "999", title: updatedTitle, body: updatedBody
}),
annotated_content_info: TeamDiscussionSpecHelper.createAnnotatedContentInfo()
});
// Expect the thread to have been updated
updatedThreadElement = view.$('.discussion-thread').first();
expect(updatedThreadElement.find('.post-header-content h1').text().trim()).toEqual(updatedTitle);
expect(updatedThreadElement.find('.post-body').text().trim()).toEqual(updatedBody);
});
it('cannot move a new thread to a different topic', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests),
postTopicButton;
createPost(requests, view);
expandReplies(requests, view);
view.$('.action-more .icon').first().click();
view.$('.action-edit').first().click();
expect(view.$('.post-topic').length).toBe(0);
});
});
});

View File

@@ -0,0 +1,50 @@
define([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/views/team_profile', 'teams/js/spec_helpers/team_discussion_helpers',
'xmodule_js/common_static/coffee/spec/discussion/discussion_spec_helper'
], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamDiscussionSpecHelper, DiscussionSpecHelper) {
'use strict';
describe('TeamProfileView', function () {
var discussionView, createTeamProfileView;
beforeEach(function () {
DiscussionSpecHelper.setUnderscoreFixtures();
});
createTeamProfileView = function(requests) {
var model = new TeamModel(
{
id: "test-team",
name: "Test Team",
discussion_topic_id: TeamDiscussionSpecHelper.testTeamDiscussionID
},
{ parse: true }
);
discussionView = new TeamProfileView({
courseID: TeamDiscussionSpecHelper.testCourseID,
model: model
});
discussionView.render();
AjaxHelpers.expectRequest(
requests,
'GET',
interpolate(
'/courses/%(courseID)s/discussion/forum/%(topicID)s/inline?page=1&ajax=1',
{
courseID: TeamDiscussionSpecHelper.testCourseID,
topicID: TeamDiscussionSpecHelper.testTeamDiscussionID
},
true
)
);
AjaxHelpers.respondWithJson(requests, TeamDiscussionSpecHelper.createMockDiscussionResponse());
return discussionView;
};
it('can render itself', function () {
var requests = AjaxHelpers.requests(this),
view = createTeamProfileView(requests);
expect(view.$('.discussion-thread').length).toEqual(3);
});
});
});

View File

@@ -2,7 +2,7 @@ define([
'backbone', 'teams/js/collections/team', 'teams/js/views/teams'
], function (Backbone, TeamCollection, TeamsView) {
'use strict';
describe('TeamsView', function () {
describe('Teams View', function () {
var teamsView, teamCollection, initialTeams,
createTeams = function (startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) {

View File

@@ -0,0 +1,92 @@
define([
'jquery',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'teams/js/views/teams_tab',
'URI'
], function ($, Backbone, AjaxHelpers, TeamsTabView, URI) {
'use strict';
describe('TeamsTab', function () {
var teamsTabView,
expectContent = function (text) {
expect(teamsTabView.$('.page-content-main').text()).toContain(text);
},
expectHeader = function (text) {
expect(teamsTabView.$('.teams-header').text()).toContain(text);
},
expectError = function (text) {
expect(teamsTabView.$('.warning').text()).toContain(text);
},
expectFocus = function (element) {
expect(element.focus).toHaveBeenCalled();
};
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
teamsTabView = new TeamsTabView({
el: $('.teams-content'),
topics: {
count: 1,
num_pages: 1,
current_page: 1,
start: 0,
results: [{
description: 'test description',
name: 'test topic',
id: 'test_topic',
team_count: 0
}]
},
topicsUrl: 'api/topics/',
topicUrl: 'api/topics/topic_id,test/course/id',
teamsUrl: 'api/teams/',
courseID: 'test/course/id'
}).render();
Backbone.history.start();
spyOn($.fn, 'focus');
});
afterEach(function () {
Backbone.history.stop();
});
it('shows the teams tab initially', function () {
expectHeader('See all teams in your course, organized by topic');
expectContent('This is the new Teams tab.');
});
describe('Navigation', function () {
it('can switch tabs', function () {
teamsTabView.$('a.nav-item[data-url="browse"]').click();
expectContent('test description');
teamsTabView.$('a.nav-item[data-url="teams"]').click();
expectContent('This is the new Teams tab.');
});
it('displays and focuses an error message when trying to navigate to a nonexistent page', function () {
teamsTabView.router.navigate('no_such_page', {trigger: true});
expectError('The page "no_such_page" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () {
var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('topics/no_such_topic', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/no_such_topic,test/course/id', null);
AjaxHelpers.respondWithError(requests, 404);
expectError('The topic "no_such_topic" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
it('displays and focuses an error message when trying to navigate to a nonexistent team', function () {
var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('teams/test_topic/no_such_team', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team', null);
AjaxHelpers.respondWithError(requests, 404);
expectError('The team "no_such_team" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
});
});
});

View File

@@ -0,0 +1,133 @@
define([
'underscore', 'common/js/spec_helpers/ajax_helpers'
], function (_, AjaxHelpers) {
'use strict';
var createMockPostResponse, createMockDiscussionResponse, createAnnotatedContentInfo, createMockThreadResponse,
testCourseID = 'course/1',
testUser = 'testUser',
testTeamDiscussionID = "12345";
createMockPostResponse = function(options) {
return _.extend(
{
username: testUser,
course_id: testCourseID,
commentable_id: testTeamDiscussionID,
type: 'thread',
body: "",
anonymous_to_peers: false,
unread_comments_count: 0,
updated_at: '2015-07-29T18:44:56Z',
group_name: 'Default Group',
pinned: false,
votes: {count: 0, down_count: 0, point: 0, up_count: 0},
user_id: "9",
abuse_flaggers: [],
closed: false,
at_position_list: [],
read: false,
anonymous: false,
created_at: "2015-07-29T18:44:56Z",
thread_type: 'discussion',
comments_count: 0,
group_id: 1,
endorsed: false
},
options || {}
);
};
createMockDiscussionResponse = function(threads) {
if (_.isUndefined(threads)) {
threads = [
createMockPostResponse({ id: "1", title: "First Post"}),
createMockPostResponse({ id: "2", title: "Second Post"}),
createMockPostResponse({ id: "3", title: "Third Post"})
];
}
return {
"num_pages": 1,
"page": 1,
"discussion_data": threads,
"user_info": {
"username": testUser,
"follower_ids": [],
"default_sort_key": "date",
"downvoted_ids": [],
"subscribed_thread_ids": [],
"upvoted_ids": [],
"external_id": "9",
"id": "9",
"subscribed_user_ids": [],
"subscribed_commentable_ids": []
},
"annotated_content_info": {
},
"roles": {"Moderator": [], "Administrator": [], "Community TA": []},
"course_settings": {
"is_cohorted": false,
"allow_anonymous_to_peers": false,
"allow_anonymous": true,
"category_map": {"subcategories": {}, "children": [], "entries": {}},
"cohorts": []
}
};
};
createAnnotatedContentInfo = function() {
return {
voted: '',
subscribed: true,
ability: {
can_reply: true,
editable: true,
can_openclose: true,
can_delete: true,
can_vote: true
}
};
};
createMockThreadResponse = function(options) {
return _.extend(
{
username: testUser,
course_id: testCourseID,
commentable_id: testTeamDiscussionID,
children: [],
comments_count: 0,
anonymous_to_peers: false,
unread_comments_count: 0,
updated_at: "2015-08-04T21:44:28Z",
resp_skip: 0,
id: "55c1323c56c02ce921000001",
pinned: false,
votes: {"count": 0, "down_count": 0, "point": 0, "up_count": 0},
resp_limit: 25,
abuse_flaggers: [],
closed: false,
resp_total: 1,
at_position_list: [],
type: "thread",
read: true,
anonymous: false,
user_id: "5",
created_at: "2015-08-04T21:44:28Z",
thread_type: "discussion",
context: "standalone",
endorsed: false
},
options || {}
);
};
return {
testCourseID: testCourseID,
testUser: testUser,
testTeamDiscussionID: testTeamDiscussionID,
createMockPostResponse: createMockPostResponse,
createMockDiscussionResponse: createMockDiscussionResponse,
createAnnotatedContentInfo: createAnnotatedContentInfo,
createMockThreadResponse: createMockThreadResponse
};
});

View File

@@ -1,192 +1,191 @@
;(function (define) {
'use strict';
'use strict';
define(['backbone',
'underscore',
'gettext',
'js/views/fields',
'teams/js/models/team',
'text!teams/templates/edit-team.underscore'],
function (Backbone, _, gettext, FieldViews, TeamModel, edit_team_template) {
return Backbone.View.extend({
define(['backbone',
'underscore',
'gettext',
'js/views/fields',
'teams/js/models/team',
'text!teams/templates/edit-team.underscore'],
function (Backbone, _, gettext, FieldViews, TeamModel, edit_team_template) {
return Backbone.View.extend({
maxTeamNameLength: 255,
maxTeamDescriptionLength: 300,
maxTeamNameLength: 255,
maxTeamDescriptionLength: 300,
events: {
"click .action-primary": "createTeam",
"click .action-cancel": "goBackToTopic"
},
events: {
'click .action-primary': 'createTeam',
'click .action-cancel': 'goBackToTopic'
},
initialize: function(options) {
this.courseId = options.teamParams.courseId;
this.teamsUrl = options.teamParams.teamsUrl;
this.topicId = options.teamParams.topicId;
this.languages = options.teamParams.languages;
this.countries = options.teamParams.countries;
this.primaryButtonTitle = options.primaryButtonTitle || 'Submit';
initialize: function(options) {
this.courseId = options.teamParams.courseId;
this.collection = options.collection;
this.teamsUrl = options.teamParams.teamsUrl;
this.topicId = options.teamParams.topicId;
this.languages = options.teamParams.languages;
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;
this.teamModel = new TeamModel({});
this.teamModel.url = this.teamsUrl;
this.teamNameField = new FieldViews.TextFieldView({
model: this.teamModel,
title: gettext("Team Name (Required) *"),
valueAttribute: 'name',
helpMessage: gettext("A name that identifies your team (maximum 255 characters).")
});
this.teamNameField = new FieldViews.TextFieldView({
model: this.teamModel,
title: gettext('Team Name (Required) *'),
valueAttribute: 'name',
helpMessage: gettext('A name that identifies your team (maximum 255 characters).')
});
this.teamDescriptionField = new FieldViews.TextareaFieldView({
model: this.teamModel,
title: gettext("Team Description (Required) *"),
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).")
});
this.teamDescriptionField = new FieldViews.TextareaFieldView({
model: this.teamModel,
title: gettext('Team Description (Required) *'),
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).')
});
this.optionalDescriptionField = new FieldViews.ReadonlyFieldView({
model: this.teamModel,
title: gettext("Optional Characteristics"),
valueAttribute: 'optional_description',
helpMessage: gettext("Help other learners decide whether to join your team by specifying some characteristics for your team. Choose carefully, because fewer people might be interested in joining your team if it seems too restrictive.")
});
this.optionalDescriptionField = new FieldViews.ReadonlyFieldView({
model: this.teamModel,
title: gettext('Optional Characteristics'),
valueAttribute: 'optional_description',
helpMessage: gettext('Help other learners decide whether to join your team by specifying some characteristics for your team. Choose carefully, because fewer people might be interested in joining your team if it seems too restrictive.')
});
this.teamLanguageField = new FieldViews.DropdownFieldView({
model: this.teamModel,
title: gettext("Language"),
valueAttribute: 'language',
required: false,
showMessages: false,
titleIconName: 'fa-comment-o',
options: this.languages,
helpMessage: gettext("The language that team members primarily use to communicate with each other.")
});
this.teamLanguageField = new FieldViews.DropdownFieldView({
model: this.teamModel,
title: gettext('Language'),
valueAttribute: 'language',
required: false,
showMessages: false,
titleIconName: 'fa-comment-o',
options: this.languages,
helpMessage: gettext('The language that team members primarily use to communicate with each other.')
});
this.teamCountryField = new FieldViews.DropdownFieldView({
model: this.teamModel,
title: gettext('Country'),
valueAttribute: 'country',
required: false,
showMessages: false,
titleIconName: 'fa-globe',
options: this.countries,
helpMessage: gettext("The country that team members primarily identify with.")
});
},
this.teamCountryField = new FieldViews.DropdownFieldView({
model: this.teamModel,
title: gettext('Country'),
valueAttribute: 'country',
required: false,
showMessages: false,
titleIconName: 'fa-globe',
options: this.countries,
helpMessage: gettext('The country that team members primarily identify with.')
});
},
render: function() {
this.$el.html(_.template(edit_team_template)({primaryButtonTitle: this.primaryButtonTitle}));
this.set(this.teamNameField, '.team-required-fields');
this.set(this.teamDescriptionField, '.team-required-fields');
this.set(this.optionalDescriptionField, '.team-optional-fields');
this.set(this.teamLanguageField, '.team-optional-fields');
this.set(this.teamCountryField, '.team-optional-fields');
return this;
},
render: function() {
this.$el.html(_.template(edit_team_template)({primaryButtonTitle: this.primaryButtonTitle}));
this.set(this.teamNameField, '.team-required-fields');
this.set(this.teamDescriptionField, '.team-required-fields');
this.set(this.optionalDescriptionField, '.team-optional-fields');
this.set(this.teamLanguageField, '.team-optional-fields');
this.set(this.teamCountryField, '.team-optional-fields');
return this;
},
set: function(view, selector) {
var viewEl = view.$el;
if (this.$(selector).has(viewEl).length) {
view.render().setElement(viewEl);
} else {
this.$(selector).append(view.render().$el);
}
},
set: function(view, selector) {
var viewEl = view.$el;
if (this.$(selector).has(viewEl).length) {
view.render().setElement(viewEl);
} else {
this.$(selector).append(view.render().$el);
}
},
createTeam: function () {
var teamName = this.teamNameField.fieldValue();
var teamDescription = this.teamDescriptionField.fieldValue();
var teamLanguage = this.teamLanguageField.fieldValue();
var teamCountry = this.teamCountryField.fieldValue();
createTeam: function () {
var view = this,
teamLanguage = this.teamLanguageField.fieldValue(),
teamCountry = this.teamCountryField.fieldValue();
var data = {
course_id: this.courseId,
topic_id: this.topicId,
name: teamName,
description: teamDescription,
language: _.isNull(teamLanguage) ? '' : teamLanguage,
country: _.isNull(teamCountry) ? '' : teamCountry
};
var data = {
course_id: this.courseId,
topic_id: this.topicId,
name: this.teamNameField.fieldValue(),
description: this.teamDescriptionField.fieldValue(),
language: _.isNull(teamLanguage) ? '' : teamLanguage,
country: _.isNull(teamCountry) ? '' : teamCountry
};
var validationResult = this.validateTeamData(data);
if (validationResult.status === false) {
this.showMessage(validationResult.message, validationResult.srMessage);
return;
}
var validationResult = this.validateTeamData(data);
if (validationResult.status === false) {
this.showMessage(validationResult.message, validationResult.srMessage);
return;
}
var view = this;
var options = {
wait: true,
success: function () {
view.goBackToTopic();
},
error: function () {
var message = gettext('An error occurred. Please try again.');
view.showMessage(message, message);
}
};
this.teamModel.save(data, options);
},
this.teamModel.save(data, { wait: true })
.done(function(result) {
Backbone.history.navigate(
'teams/' + view.topicId + '/' + view.teamModel.id,
{trigger: true}
);
})
.fail(function() {
var message = gettext('An error occurred. Please try again.');
view.showMessage(message, message);
});
},
validateTeamData: function (data) {
var status = true,
message = gettext("Check the highlighted fields below and try again.");
var srMessages = [];
validateTeamData: function (data) {
var status = true,
message = gettext('Check the highlighted fields below and try again.');
var srMessages = [];
this.teamNameField.unhighlightField();
this.teamDescriptionField.unhighlightField();
this.teamNameField.unhighlightField();
this.teamDescriptionField.unhighlightField();
if (_.isEmpty(data.name.trim()) ) {
status = false;
this.teamNameField.highlightFieldOnError();
srMessages.push(
gettext("Enter team name.")
);
} else if (data.name.length > this.maxTeamNameLength) {
status = false;
this.teamNameField.highlightFieldOnError();
srMessages.push(
gettext("Team name cannot have more than 255 characters.")
);
}
if (_.isEmpty(data.name.trim()) ) {
status = false;
this.teamNameField.highlightFieldOnError();
srMessages.push(
gettext('Enter team name.')
);
} else if (data.name.length > this.maxTeamNameLength) {
status = false;
this.teamNameField.highlightFieldOnError();
srMessages.push(
gettext('Team name cannot have more than 255 characters.')
);
}
if (_.isEmpty(data.description.trim()) ) {
status = false;
this.teamDescriptionField.highlightFieldOnError();
srMessages.push(
gettext("Enter team description.")
);
} else if (data.description.length > this.maxTeamDescriptionLength) {
status = false;
this.teamDescriptionField.highlightFieldOnError();
srMessages.push(
gettext("Team description cannot have more than 300 characters.")
);
}
if (_.isEmpty(data.description.trim()) ) {
status = false;
this.teamDescriptionField.highlightFieldOnError();
srMessages.push(
gettext('Enter team description.')
);
} else if (data.description.length > this.maxTeamDescriptionLength) {
status = false;
this.teamDescriptionField.highlightFieldOnError();
srMessages.push(
gettext('Team description cannot have more than 300 characters.')
);
}
return {
status: status,
message: message,
srMessage: srMessages.join(" ")
};
},
return {
status: status,
message: message,
srMessage: srMessages.join(' ')
};
},
showMessage: function (message, screenReaderMessage) {
this.$('.wrapper-msg').removeClass('is-hidden');
this.$('.msg-content .copy p').text(message);
this.$('.wrapper-msg').focus();
showMessage: function (message, screenReaderMessage) {
this.$('.wrapper-msg').removeClass('is-hidden');
this.$('.msg-content .copy p').text(message);
this.$('.wrapper-msg').focus();
if (screenReaderMessage) {
this.$('.screen-reader-message').text(screenReaderMessage);
}
if (screenReaderMessage) {
this.$('.screen-reader-message').text(screenReaderMessage);
}
},
goBackToTopic: function () {
Backbone.history.navigate("topics/" + this.topicId, {trigger: true});
}
});
});
Backbone.history.navigate('topics/' + this.topicId, {trigger: true});
}
});
});
}).call(this, define || RequireJS.define);

View File

@@ -78,6 +78,11 @@
{span_start: '<span class="sr">', team_name: this.model.get('name'), span_end: '</span>'},
true
);
},
action: function (event) {
var url = 'teams/' + this.topic.get('id') + '/' + this.model.get('id');
event.preventDefault();
this.router.navigate(url, {trigger: true});
}
});
return TeamCardView;

View File

@@ -0,0 +1,27 @@
/**
* View that shows the discussion for a team.
*/
;(function (define) {
'use strict';
define(['backbone', 'underscore', 'gettext', 'DiscussionModuleView'],
function (Backbone, _, gettext, DiscussionModuleView) {
var TeamDiscussionView = Backbone.View.extend({
initialize: function () {
window.$$course_id = this.$el.data("course-id");
this.render();
},
render: function () {
var discussionModuleView = new DiscussionModuleView({
el: this.$el,
context: 'standalone'
});
discussionModuleView.render();
discussionModuleView.loadPage(this.$el);
return this;
}
});
return TeamDiscussionView;
});
}).call(this, define || RequireJS.define);

View File

@@ -0,0 +1,33 @@
/**
* View for an individual team.
*/
;(function (define) {
'use strict';
define(['backbone', 'underscore', 'gettext', 'teams/js/views/team_discussion',
'text!teams/templates/team-profile.underscore'],
function (Backbone, _, gettext, TeamDiscussionView, teamTemplate) {
var TeamProfileView = Backbone.View.extend({
initialize: function (options) {
this.courseID = options.courseID;
this.discussionTopicID = this.model.get('discussion_topic_id');
},
render: function () {
var canPostToTeam = true; // TODO: determine this permission correctly!
this.$el.html(_.template(teamTemplate, {
courseID: this.courseID,
discussionTopicID: this.discussionTopicID,
canCreateComment: canPostToTeam,
canCreateSubComment: canPostToTeam
}));
this.discussionView = new TeamDiscussionView({
el: this.$('.discussion-module')
});
this.discussionView.render();
return this;
}
});
return TeamProfileView;
});
}).call(this, define || RequireJS.define);

View File

@@ -10,8 +10,10 @@
type: 'teams',
initialize: function (options) {
this.topic = options.topic;
this.itemViewClass = TeamCardView.extend({
router: options.router,
topic: options.topic,
maxTeamSize: options.maxTeamSize
});
PaginatedView.prototype.initialize.call(this);

View File

@@ -7,304 +7,379 @@
'js/components/header/views/header',
'js/components/header/models/header',
'js/components/tabbed/views/tabbed_view',
'teams/js/views/topics',
'teams/js/models/topic',
'teams/js/collections/topic',
'teams/js/views/teams',
'teams/js/models/team',
'teams/js/collections/team',
'teams/js/views/topics',
'teams/js/views/team_profile',
'teams/js/views/teams',
'teams/js/views/edit_team',
'text!teams/templates/teams_tab.underscore'],
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView,
TopicsView, TopicModel, TopicCollection, TeamsView, TeamCollection,
TeamEditView, teamsTemplate) {
var ViewWithHeader = Backbone.View.extend({
initialize: function (options) {
this.header = options.header;
this.main = options.main;
},
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView,
TopicModel, TopicCollection, TeamModel, TeamCollection,
TopicsView, TeamProfileView, TeamsView, TeamEditView,
teamsTemplate) {
var ViewWithHeader = Backbone.View.extend({
initialize: function (options) {
this.header = options.header;
this.main = options.main;
},
render: function () {
this.$el.html(_.template(teamsTemplate));
this.$('p.error').hide();
this.header.setElement(this.$('.teams-header')).render();
this.main.setElement(this.$('.page-content')).render();
return this;
}
});
render: function () {
this.$el.html(_.template(teamsTemplate));
this.$('p.error').hide();
this.header.setElement(this.$('.teams-header')).render();
this.main.setElement(this.$('.page-content')).render();
return this;
}
});
var TeamTabView = Backbone.View.extend({
initialize: function(options) {
var TempTabView, router;
this.course_id = options.course_id;
this.topics = options.topics;
this.topic_url = options.topic_url;
this.teams_url = options.teams_url;
this.maxTeamSize = options.maxTeamSize;
this.languages = options.languages;
this.countries = options.countries;
// This slightly tedious approach is necessary
// to use regular expressions within Backbone
// routes, allowing us to capture which tab
// name is being routed to
router = this.router = new Backbone.Router();
_.each([
[':default', _.bind(this.routeNotFound, this)],
['topics/:topic_id(/)', _.bind(this.browseTopic, this)],
['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)],
[new RegExp('^(browse)\/?$'), _.bind(this.goToTab, this)],
[new RegExp('^(teams)\/?$'), _.bind(this.goToTab, this)]
], function (route) {
router.route.apply(router, route);
});
// TODO replace this with actual views!
TempTabView = Backbone.View.extend({
initialize: function (options) {
this.text = options.text;
},
var TeamTabView = Backbone.View.extend({
initialize: function(options) {
var TempTabView, router;
this.courseID = options.courseID;
this.topics = options.topics;
this.topicUrl = options.topicUrl;
this.teamsUrl = options.teamsUrl;
this.maxTeamSize = options.maxTeamSize;
this.languages = options.languages;
this.countries = options.countries;
// This slightly tedious approach is necessary
// to use regular expressions within Backbone
// routes, allowing us to capture which tab
// name is being routed to
router = this.router = new Backbone.Router();
_.each([
[':default', _.bind(this.routeNotFound, this)],
['topics/:topic_id(/)', _.bind(this.browseTopic, this)],
['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)],
['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)],
[new RegExp('^(browse)\/?$'), _.bind(this.goToTab, this)],
[new RegExp('^(teams)\/?$'), _.bind(this.goToTab, this)]
], function (route) {
router.route.apply(router, route);
});
// TODO replace this with actual views!
TempTabView = Backbone.View.extend({
initialize: function (options) {
this.text = options.text;
},
render: function () {
this.$el.text(this.text);
}
});
this.topicsCollection = new TopicCollection(
this.topics,
{url: options.topicsUrl, course_id: this.courseID, parse: true}
).bootstrap();
this.topicsView = new TopicsView({
collection: this.topicsCollection,
router: this.router
});
this.mainView = this.tabbedView = new ViewWithHeader({
header: new HeaderView({
model: new HeaderModel({
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."),
title: gettext("Teams")
})
}),
main: new TabbedView({
tabs: [{
title: gettext('My Teams'),
url: 'teams',
view: new TempTabView({text: 'This is the new Teams tab.'})
}, {
title: gettext('Browse'),
url: 'browse',
view: this.topicsView
}],
router: this.router
})
});
},
render: function () {
this.$el.text(this.text);
}
});
this.topicsCollection = new TopicCollection(
this.topics,
{url: options.topics_url, course_id: this.course_id, parse: true}
).bootstrap();
this.mainView = this.tabbedView = new ViewWithHeader({
header: new HeaderView({
model: new HeaderModel({
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."),
title: gettext("Teams")
})
}),
main: new TabbedView({
tabs: [{
title: gettext('My Teams'),
url: 'teams',
view: new TempTabView({text: 'This is the new Teams tab.'})
}, {
title: gettext('Browse'),
url: 'browse',
view: new TopicsView({
collection: this.topicsCollection,
router: this.router
})
}],
router: this.router
})
});
},
render: function() {
this.mainView.setElement(this.$el).render();
this.hideWarning();
return this;
},
render: function() {
this.mainView.setElement(this.$el).render();
this.hideWarning();
return this;
},
/**
* Render the list of teams for the given topic ID.
*/
browseTopic: function (topicID) {
var self = this;
this.getTeamsView(topicID).done(function (teamsView) {
self.teamsView = self.mainView = teamsView;
self.render();
});
},
/**
* Render the create new team form.
*/
newTeam: function (topicId) {
var self = this;
this.getTeamsView(topicId).done(function (teamsView) {
self.mainView = new ViewWithHeader({
header: new HeaderView({
model: new HeaderModel({
description: gettext("Create a new team if you can't find existing teams to join, or if you would like to learn with friends you know."),
title: gettext("Create a New Team"),
breadcrumbs: [
{
title: teamsView.main.teamParams.topicName,
url: '#topics/' + teamsView.main.teamParams.topicId
}
]
})
}),
main: new TeamEditView({
tagName: 'create-new-team',
teamParams: teamsView.main.teamParams,
primaryButtonTitle: 'Create'
})
});
self.render();
});
},
/**
* Render the create new team form.
*/
newTeam: function (topicId) {
var self = this;
this.getTeamsView(topicId).done(function (teamsView) {
self.mainView = new ViewWithHeader({
header: new HeaderView({
model: new HeaderModel({
description: gettext("Create a new team if you can't find existing teams to join, or if you would like to learn with friends you know."),
title: gettext("Create a New Team"),
breadcrumbs: [
{
title: teamsView.main.teamParams.topicName,
url: '#topics/' + teamsView.main.teamParams.topicId
}
]
})
}),
main: new TeamEditView({
tagName: 'create-new-team',
teamParams: teamsView.main.teamParams,
primaryButtonTitle: 'Create'
})
});
self.render();
});
},
/**
* Render the list of teams for the given topic ID.
*/
browseTopic: function (topicID) {
var self = this;
this.getTeamsView(topicID).done(function (teamsView) {
self.mainView = teamsView;
self.render();
});
},
/**
* Return a promise for the TeamsView for the given topic ID.
*/
getTeamsView: function (topicID) {
// Lazily load the teams-for-topic view in
// order to avoid making an extra AJAX call.
var self = this,
router = this.router,
deferred = $.Deferred();
if (this.teamsCollection && this.teamsCollection.topic_id === topicID) {
deferred.resolve(this.teamsView);
} else {
this.getTopic(topicID)
.done(function(topic) {
var collection = new TeamCollection([], {
course_id: self.courseID,
topic_id: topicID,
url: self.teamsUrl,
per_page: 10
});
this.teamsCollection = collection;
collection.goTo(1)
.done(function() {
var teamsView = new TeamsView({
router: router,
topic: topic,
collection: collection,
maxTeamSize: self.maxTeamSize,
teamParams: {
courseId: self.courseID,
teamsUrl: self.teamsUrl,
topicId: topic.get('id'),
topicName: topic.get('name'),
languages: self.languages,
countries: self.countries
}
});
deferred.resolve(self.createViewWithHeader(teamsView, topic));
});
});
}
return deferred.promise();
},
/**
* Return a promise for the TeamsView for the given
* topic ID.
*/
getTeamsView: function (topicID) {
// Lazily load the teams-for-topic view in
// order to avoid making an extra AJAX call.
if (!_.isUndefined(this.teamsView)
&& this.teamsView.main.collection.topic_id === topicID) {
return this.identityPromise(this.teamsView);
}
var self = this,
teamCollection = new TeamCollection([], {
course_id: this.course_id,
url: this.teams_url,
topic_id: topicID,
per_page: 10
}),
teamPromise = teamCollection.goTo(1).fail(function (xhr) {
if (xhr.status === 400) {
self.topicNotFound(topicID);
}
}),
topicPromise = this.getTopic(topicID).fail(function (xhr) {
if (xhr.status === 404) {
self.topicNotFound(topicID);
}
});
return $.when(topicPromise, teamPromise).pipe(
_.bind(this.constructTeamView, this)
);
},
/**
* Browse to the team with the specified team ID belonging to the specified topic.
*/
browseTeam: function (topicID, teamID) {
var self = this;
this.getBrowseTeamView(topicID, teamID).done(function (browseTeamView) {
self.mainView = browseTeamView;
self.render();
});
},
/**
* Given a topic and the results of the team
* collection's fetch(), return the team list view.
*/
constructTeamView: function (topic, collectionResults) {
var self = this,
headerView = new HeaderView({
model: new HeaderModel({
description: _.escape(topic.get('description')),
title: _.escape(topic.get('name')),
breadcrumbs: [{
title: 'All topics',
url: '#'
}]
}),
events: {
'click nav.breadcrumbs a.nav-item': function (event) {
event.preventDefault();
self.router.navigate('browse', {trigger: true});
}
}
});
return new ViewWithHeader({
header: headerView,
main: new TeamsView({
collection: new TeamCollection(collectionResults[0], {
course_id: this.course_id,
url: this.teams_url,
topic_id: topic.get('id'),
per_page: 10,
parse: true
}),
teamParams: {
courseId: this.course_id,
teamsUrl: this.teams_url,
topicId: topic.get('id'),
topicName: topic.get('name'),
languages: self.languages,
countries: self.countries
},
maxTeamSize: this.maxTeamSize
})
});
},
/**
* Return a promise for the team view for the given team ID.
*/
getBrowseTeamView: function (topicID, teamID) {
var self = this,
deferred = $.Deferred(),
courseID = this.courseID;
self.getTopic(topicID).done(function(topic) {
self.getTeam(teamID).done(function(team) {
var view = new TeamProfileView({
courseID: courseID,
model: team
});
deferred.resolve(self.createViewWithHeader(view, team, topic));
});
});
return deferred.promise();
},
/**
* Get a topic given a topic ID. Returns a jQuery deferred
* promise, since the topic may need to be fetched from the
* server.
* @param topicID the string identifier for the requested topic
* @returns a jQuery deferred promise for the topic.
*/
getTopic: function (topicID) {
// Try finding topic in the current page of the
// topicCollection. Otherwise call the topic endpoint.
var topic = this.topicsCollection.findWhere({'id': topicID}),
self = this;
if (topic) {
return this.identityPromise(topic);
} else {
var TopicModelWithUrl = TopicModel.extend({
url: function () { return self.topic_url.replace('topic_id', this.id); }
});
return (new TopicModelWithUrl({id: topicID })).fetch();
}
},
createViewWithHeader: function (mainView, subject, parentTopic) {
var router = this.router,
breadcrumbs, headerView;
breadcrumbs = [{
title: gettext('All Topics'),
url: '#browse'
}];
if (parentTopic) {
breadcrumbs.push({
title: parentTopic.get('name'),
url: '#topics/' + parentTopic.id
});
}
headerView = new HeaderView({
model: new HeaderModel({
description: subject.get('description'),
title: subject.get('name'),
breadcrumbs: breadcrumbs
}),
events: {
'click nav.breadcrumbs a.nav-item': function (event) {
var url = $(event.currentTarget).attr('href');
event.preventDefault();
router.navigate(url, {trigger: true});
}
}
});
return new ViewWithHeader({
header: headerView,
main: mainView
});
},
/**
* Immediately return a promise for the given
* object.
*/
identityPromise: function (obj) {
return new $.Deferred().resolve(obj).promise();
},
/**
* Get a topic given a topic ID. Returns a jQuery deferred
* promise, since the topic may need to be fetched from the
* server.
* @param topicID the string identifier for the requested topic
* @returns a jQuery deferred promise for the topic.
*/
getTopic: function (topicID) {
// Try finding topic in the current page of the
// topicCollection. Otherwise call the topic endpoint.
var topic = this.topicsCollection.findWhere({'id': topicID}),
self = this,
deferred = $.Deferred();
if (topic) {
deferred.resolve(topic);
} else {
topic = new TopicModel({
id: topicID,
url: self.topicUrl.replace('topic_id', topicID)
});
topic.fetch()
.done(function() {
deferred.resolve(topic);
})
.fail(function() {
self.topicNotFound(topicID);
deferred.reject();
});
}
return deferred.promise();
},
/**
* Set up the tabbed view and switch tabs.
*/
goToTab: function (tab) {
this.mainView = this.tabbedView;
// Note that `render` should be called first so
// that the tabbed view's element is set
// correctly.
this.render();
this.tabbedView.main.setActiveTab(tab);
},
/**
* Get a team given a team ID. Returns a jQuery deferred
* promise, since the team may need to be fetched from the
* server.
* @param teamID the string identifier for the requested team
* @returns {promise} a jQuery deferred promise for the team.
*/
getTeam: function (teamID) {
var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null,
self = this,
deferred = $.Deferred();
if (team) {
deferred.resolve(team);
} else {
team = new TeamModel({
id: teamID,
url: this.teamsUrl + teamID
});
team.fetch()
.done(function() {
deferred.resolve(team);
})
.fail(function() {
self.teamNotFound(teamID);
deferred.reject();
});
}
return deferred.promise();
},
// Error handling
/**
* Set up the tabbed view and switch tabs.
*/
goToTab: function (tab) {
this.mainView = this.tabbedView;
// Note that `render` should be called first so
// that the tabbed view's element is set
// correctly.
this.render();
this.tabbedView.main.setActiveTab(tab);
},
routeNotFound: function (route) {
this.notFoundError(
interpolate(
gettext('The page "%(route)s" could not be found.'),
{route: route},
true
)
);
},
// Error handling
topicNotFound: function (topicID) {
this.notFoundError(
interpolate(
gettext('The topic "%(topic)s" could not be found.'),
{topic: topicID},
true
)
);
},
routeNotFound: function (route) {
this.notFoundError(
interpolate(
gettext('The page "%(route)s" could not be found.'),
{route: route},
true
)
);
},
/**
* Called when the user attempts to navigate to a
* route that doesn't exist. "Redirects" back to
* the main teams tab, and adds an error message.
*/
notFoundError: function (message) {
this.router.navigate('teams', {trigger: true});
this.showWarning(message);
},
topicNotFound: function (topicID) {
this.notFoundError(
interpolate(
gettext('The topic "%(topic)s" could not be found.'),
{topic: topicID},
true
)
);
},
showWarning: function (message) {
var warningEl = this.$('.warning');
warningEl.find('.copy').html('<p>' + message + '</p');
warningEl.toggleClass('is-hidden', false);
warningEl.focus();
},
teamNotFound: function (teamID) {
this.notFoundError(
interpolate(
gettext('The team "%(team)s" could not be found.'),
{team: teamID},
true
)
);
},
hideWarning: function () {
this.$('.warning').toggleClass('is-hidden', true);
}
});
/**
* Called when the user attempts to navigate to a
* route that doesn't exist. "Redirects" back to
* the main teams tab, and adds an error message.
*/
notFoundError: function (message) {
this.router.navigate('teams', {trigger: true});
this.showWarning(message);
},
return TeamTabView;
});
showWarning: function (message) {
var warningEl = this.$('.warning');
warningEl.find('.copy').html('<p>' + message + '</p');
warningEl.toggleClass('is-hidden', false);
warningEl.focus();
},
hideWarning: function () {
this.$('.warning').toggleClass('is-hidden', true);
}
});
return TeamTabView;
});
}).call(this, define || RequireJS.define);

View File

@@ -0,0 +1,7 @@
<div class="team-profile">
<div class="discussion-module" data-course-id="<%= courseID %>" data-discussion-id="<%= discussionTopicID %>"
data-user-create-comment="<%= canCreateComment %>"
data-user-create-subcomment="<%= canCreateSubComment %>">
<a href="#" class="new-post-btn" role="button"><span class="icon fa fa-edit new-post-icon"></span><%= gettext("New Post") %></a>
</div>
</div>

View File

@@ -7,8 +7,11 @@
<%block name="bodyclass">view-teams is-in-course course</%block>
<%block name="pagetitle">${_("Teams")}</%block>
<%block name="headextra">
<%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<%include file="../discussion/_js_head_dependencies.html" />
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='teams'" />
@@ -21,16 +24,25 @@
</div>
<%block name="js_extra">
<%include file="../discussion/_js_body_dependencies.html" />
<%static:js group='discussion'/>
<script type="text/javascript">
RequireJS.define('DiscussionModuleView', [], function() {return window['DiscussionModuleView'];});
</script>
<%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory">
new TeamsTabFactory({
TeamsTabFactory({
courseID: '${ unicode(course.id) }',
topics: ${ json.dumps(topics, cls=EscapedEdxJSONEncoder) },
topic_url: '${ topic_url }',
topics_url: '${ topics_url }',
teams_url: '${ teams_url }',
topicUrl: '${ topic_url }',
topicsUrl: '${ topics_url }',
teamsUrl: '${ teams_url }',
maxTeamSize: ${ course.teams_max_size },
course_id: '${ unicode(course.id) }',
languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) },
countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) }
});
</%static:require_module>
</%block>
<%include file="../discussion/_underscore_templates.html" />

View File

@@ -21,6 +21,7 @@
'jquery.inputnumber': 'xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill',
'jquery.immediateDescendents': 'xmodule_js/common_static/coffee/src/jquery.immediateDescendents',
'jquery.simulate': 'xmodule_js/common_static/js/vendor/jquery.simulate',
'jquery.timeago': 'xmodule_js/common_static/js/vendor/jquery.timeago',
'jquery.url': 'xmodule_js/common_static/js/vendor/url.min',
'datepair': 'xmodule_js/common_static/js/vendor/timepicker/datepair',
'date': 'xmodule_js/common_static/js/vendor/date',
@@ -30,8 +31,8 @@
'backbone': 'xmodule_js/common_static/js/vendor/backbone-min',
'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
'backbone.paginator': 'xmodule_js/common_static/js/vendor/backbone.paginator.min',
'backbone-super': 'js/vendor/backbone-super',
'URI': 'xmodule_js/common_static/js/vendor/URI.min',
"backbone-super": "js/vendor/backbone-super",
'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
'xmodule': 'xmodule_js/src/xmodule',
@@ -57,6 +58,12 @@
'capa/display': 'xmodule_js/src/capa/display',
'string_utils': 'xmodule_js/common_static/js/src/string_utils',
'logger': 'xmodule_js/common_static/js/src/logger',
'Markdown.Converter': 'js/Markdown.Converter',
'Markdown.Editor': 'js/Markdown.Editor',
'Markdown.Sanitizer': 'js/Markdown.Sanitizer',
'_split': 'js/split',
'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer',
'MathJaxProcessor': 'coffee/src/customwmd',
// Manually specify LMS files that are not converted to RequireJS
'history': 'js/vendor/history',
@@ -83,6 +90,9 @@
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
'js/ccx/schedule': 'js/ccx/schedule',
// Discussion classes loaded explicitly until they are converted to use RequireJS
'DiscussionModuleView': 'xmodule_js/common_static/coffee/src/discussion/discussion_module_view',
// edxnotes
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
},
@@ -149,6 +159,10 @@
deps: ['jquery'],
exports: 'jQuery.fn.simulate'
},
'jquery.timeago': {
deps: ['jquery'],
exports: 'jQuery.timeago'
},
'jquery.tinymce': {
deps: ['jquery', 'tinymce'],
exports: 'jQuery.fn.tinymce'
@@ -192,11 +206,32 @@
exports: 'Backbone.Paginator'
},
"backbone-super": {
deps: ["backbone"],
deps: ["backbone"]
},
'youtube': {
exports: 'YT'
},
'Markdown.Converter': {
deps: ['mathjax'],
exports: 'Markdown.Converter'
},
'Markdown.Editor': {
deps: ['Markdown.Converter'],
exports: 'Markdown.Editor'
},
'Markdown.Sanitizer': {
deps: ['Markdown.Converter'],
exports: 'Markdown.Sanitizer'
},
'_split': {
exports: '_split'
},
'MathJaxProcessor': {
deps: [
'Markdown.Converter', 'Markdown.Sanitizer', 'Markdown.Editor', '_split', 'mathjax_delay_renderer'
],
exports: 'MathJaxProcessor'
},
'codemirror': {
exports: 'CodeMirror'
},
@@ -417,7 +452,7 @@
exports: 'edx.verify_student.IntroStepView',
deps: [
'jquery',
'js/verify_student/views/step_view',
'js/verify_student/views/step_view'
]
},
'js/verify_student/views/make_payment_step_view': {
@@ -429,7 +464,7 @@
'jquery.cookie',
'jquery.url',
'string_utils',
'js/verify_student/views/step_view',
'js/verify_student/views/step_view'
]
},
'js/verify_student/views/payment_confirmation_step_view': {
@@ -438,7 +473,7 @@
'jquery',
'underscore',
'gettext',
'js/verify_student/views/step_view',
'js/verify_student/views/step_view'
]
},
'js/verify_student/views/face_photo_step_view': {
@@ -475,14 +510,14 @@
exports: 'edx.verify_student.EnrollmentConfirmationStepView',
deps: [
'jquery',
'js/verify_student/views/step_view',
'js/verify_student/views/step_view'
]
},
'js/verify_student/views/reverify_success_step_view': {
exports: 'edx.verify_student.ReverifySuccessStepView',
deps: [
'jquery',
'js/verify_student/views/step_view',
'js/verify_student/views/step_view'
]
},
'js/verify_student/views/pay_and_verify_view': {
@@ -520,21 +555,169 @@
'annotator_1.2.9': {
exports: 'Annotator',
deps: ['jquery']
},
// Discussions
'xmodule_js/common_static/coffee/src/discussion/utils': {
deps: [
'jquery',
'jquery.timeago',
'underscore',
'backbone',
'gettext',
'MathJaxProcessor',
'URI'
],
exports: 'DiscussionUtil',
init: function() {
// Set global variables that the discussion code is expecting to be defined
window.Backbone = require('backbone');
window.URI = require('URI');
}
},
'xmodule_js/common_static/coffee/src/discussion/content': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'Content'
},
'xmodule_js/common_static/coffee/src/discussion/discussion': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils',
'xmodule_js/common_static/coffee/src/discussion/content'
],
exports: 'Discussion'
},
'xmodule_js/common_static/coffee/src/discussion/discussion_filter': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionFilter'
},
'xmodule_js/common_static/coffee/src/discussion/models/discussion_course_settings': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionCourseSettings'
},
'xmodule_js/common_static/coffee/src/discussion/models/discussion_user': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionUser'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_content_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionContentView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_edit_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionThreadEditView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_list_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionThreadListView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_profile_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionThreadProfileView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_show_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionThreadShowView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionThreadView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_topic_menu_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionTopicMenuView'
},
'xmodule_js/common_static/coffee/src/discussion/views/discussion_user_profile_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionUserProfileView'
},
'xmodule_js/common_static/coffee/src/discussion/views/new_post_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'NewPostView'
},
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_edit_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'ThreadResponseEditView'
},
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_show_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'ThreadResponseShowView'
},
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_view': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'ThreadResponseView'
},
'DiscussionModuleView': {
deps: [
'jquery',
'underscore',
'backbone',
'gettext',
'URI',
'xmodule_js/common_static/coffee/src/discussion/content',
'xmodule_js/common_static/coffee/src/discussion/discussion',
'xmodule_js/common_static/coffee/src/discussion/discussion_filter',
'xmodule_js/common_static/coffee/src/discussion/utils',
'xmodule_js/common_static/coffee/src/discussion/models/discussion_course_settings',
'xmodule_js/common_static/coffee/src/discussion/models/discussion_user',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_content_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_edit_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_list_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_profile_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_show_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_topic_menu_view',
'xmodule_js/common_static/coffee/src/discussion/views/discussion_user_profile_view',
'xmodule_js/common_static/coffee/src/discussion/views/new_post_view',
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_edit_view',
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_show_view',
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_view'
],
exports: 'DiscussionModuleView'
},
'xmodule_js/common_static/coffee/spec/discussion/discussion_spec_helper': {
deps: [
'xmodule_js/common_static/coffee/src/discussion/utils'
],
exports: 'DiscussionSpecHelper'
}
}
});
// TODO: why do these need 'lms/include' at the front but the CMS equivalent logic doesn't?
define([
// Run the LMS tests
'lms/include/teams/js/spec/teams_factory_spec.js',
'lms/include/teams/js/spec/topic_card_spec.js',
'lms/include/teams/js/spec/topic_collection_spec.js',
'lms/include/teams/js/spec/topics_spec.js',
'lms/include/teams/js/spec/teams_spec.js',
'lms/include/teams/js/spec/teams_tab_spec.js',
'lms/include/teams/js/spec/team_actions_spec.js',
'lms/include/teams/js/spec/edit_team_spec.js',
'lms/include/js/spec/components/header/header_spec.js',
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js',
'lms/include/js/spec/components/card/card_spec.js',
@@ -605,7 +788,17 @@
'lms/include/js/spec/discovery/views/refine_sidebar_spec.js',
'lms/include/js/spec/discovery/views/search_form_spec.js',
'lms/include/js/spec/discovery/discovery_factory_spec.js',
'lms/include/js/spec/ccx/schedule_spec.js'
'lms/include/js/spec/ccx/schedule_spec.js',
'lms/include/teams/js/spec/collections/topic_collection_spec.js',
'lms/include/teams/js/spec/edit_team_spec.js',
'lms/include/teams/js/spec/team_actions_spec.js',
'lms/include/teams/js/spec/teams_factory_spec.js',
'lms/include/teams/js/spec/views/team_discussion_spec.js',
'lms/include/teams/js/spec/views/team_profile_spec.js',
'lms/include/teams/js/spec/views/teams_spec.js',
'lms/include/teams/js/spec/views/teams_tab_spec.js',
'lms/include/teams/js/spec/views/topic_card_spec.js',
'lms/include/teams/js/spec/views/topics_spec.js',
]);
}).call(this, requirejs, define);

View File

@@ -39,6 +39,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jquery.min.js
- xmodule_js/common_static/js/vendor/jquery-ui.min.js
- xmodule_js/common_static/js/vendor/jquery.cookie.js
- xmodule_js/common_static/js/vendor/jquery.timeago.js
- xmodule_js/common_static/js/vendor/flot/jquery.flot.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.js
@@ -66,8 +67,10 @@ lib_paths:
# Paths to source JavaScript files
src_paths:
- js
- coffee/src
- common/js
- teams/js
- xmodule_js/common_static/coffee
# Paths to spec (test) JavaScript files
spec_paths:

View File

@@ -64,6 +64,7 @@
'logger': 'empty:',
'utility': 'empty:',
'URI': 'empty:',
'DiscussionModuleView': 'empty:'
},
/**

View File

@@ -23,6 +23,7 @@
defineDependency("Logger", "logger");
defineDependency("URI", "URI");
defineDependency("Backbone", "backbone");
// utility.js adds two functions to the window object, but does not return anything
defineDependency("isExternal", "utility", true);
}