Commit 63da1907 by Andy Armstrong

Show inline discussion component on Team view

TNL-2515
parent df936837
......@@ -10,10 +10,11 @@ if Backbone?
"click .discussion-paginator a": "navigateToPage"
page_re: /\?discussion_page=(\d+)/
initialize: ->
initialize: (options) ->
@toggleDiscussionBtn = @$(".discussion-show")
# Set the page if it was set in the URL. This is used to allow deep linking to pages
match = @page_re.exec(window.location.href)
@context = options.context or "course" # allowed values are "course" or "standalone"
if match
@page = parseInt(match[1])
else
......@@ -105,6 +106,7 @@ if Backbone?
el: @$("article#thread_#{thread.id}"),
model: thread,
mode: "inline",
context: @context,
course_settings: @course_settings,
topicId: discussionId
)
......@@ -141,6 +143,7 @@ if Backbone?
el: article,
model: thread,
mode: "inline",
context: @context,
course_settings: @course_settings,
topicId: @$el.data("discussion-id")
)
......
......@@ -18,6 +18,7 @@
this.course_settings = options.course_settings;
this.threadType = this.model.get('thread_type');
this.topicId = this.model.get('commentable_id');
this.context = options.context || 'course';
_.bindAll(this);
return this;
},
......@@ -31,11 +32,15 @@
threadTypeTemplate = _.template($("#thread-type-template").html());
this.addField(threadTypeTemplate({form_id: formId}));
this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true);
this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
// Only allow the topic field for course threads, as standalone threads
// cannot be moved.
if (this.context === 'course') {
this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this;
},
......@@ -53,13 +58,14 @@
var title = this.$('.edit-post-title').val(),
threadType = this.$(".post-type-input:checked").val(),
body = this.$('.edit-post-body textarea').val(),
commentableId = this.topicView.getCurrentTopicId(),
postData = {
title: title,
thread_type: threadType,
body: body,
commentable_id: commentableId
body: body
};
if (this.topicView) {
postData.commentable_id = this.topicView.getCurrentTopicId();
}
return DiscussionUtil.safeAjax({
$elem: this.submitBtn,
......@@ -75,7 +81,9 @@
this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html('');
postData.courseware_title = this.topicView.getFullTopicName();
if (this.topicView) {
postData.courseware_title = this.topicView.getFullTopicName();
}
this.model.set(postData).unset('abbreviatedBody');
this.trigger('thread:updated');
if (this.threadType !== threadType) {
......
......@@ -19,6 +19,7 @@ if Backbone?
initialize: (options) ->
super()
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
@context = options.context or "course" # allowed values are "course" or "standalone"
if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode)
......@@ -300,6 +301,7 @@ if Backbone?
container: @$('.thread-content-wrapper')
model: @model
mode: @mode
context: @context
course_settings: @options.course_settings
)
@editView.bind "thread:updated thread:cancel_edit", @closeEditView
......
......@@ -87,7 +87,6 @@ if Backbone?
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
thread_type: thread_type
title: title
......
......@@ -389,7 +389,7 @@ class InlineDiscussionPage(PageObject):
def __init__(self, browser, discussion_id):
super(InlineDiscussionPage, self).__init__(browser)
self._discussion_selector = (
"body.courseware .discussion-module[data-discussion-id='{discussion_id}'] ".format(
".discussion-module[data-discussion-id='{discussion_id}'] ".format(
discussion_id=discussion_id
)
)
......@@ -418,6 +418,10 @@ class InlineDiscussionPage(PageObject):
def get_num_displayed_threads(self):
return len(self._find_within(".discussion-thread"))
def has_thread(self, thread_id):
"""Returns true if this page is showing the thread with the specified id."""
return self._find_within('.discussion-thread#thread_{}'.format(thread_id)).present
def element_exists(self, selector):
return self.q(css=self._discussion_selector + " " + selector).present
......
......@@ -4,6 +4,7 @@ Teams pages.
"""
from .course_page import CoursePage
from .discussion import InlineDiscussionPage
from ..common.paging import PaginatedUIMixin
from .fields import FieldsMixin
......@@ -179,3 +180,50 @@ class CreateTeamPage(CoursePage, FieldsMixin):
"""Click on cancel team button"""
self.q(css='.create-team .action-cancel').first.click()
self.wait_for_ajax()
class TeamPage(CoursePage, PaginatedUIMixin):
"""
The page for a specific Team within the Teams tab
"""
def __init__(self, browser, course_id, team=None):
"""
Set up `self.url_path` on instantiation, since it dynamically
reflects the current team.
"""
super(TeamPage, self).__init__(browser, course_id)
self.team = team
if self.team:
self.url_path = "teams/#teams/{topic_id}/{team_id}".format(
topic_id=self.team['topic_id'], team_id=self.team['id']
)
def is_browser_on_page(self):
"""Check if we're on the teams list page for a particular team."""
if self.team:
if not self.url.endswith(self.url_path):
return False
return self.q(css='.team-profile').present
@property
def discussion_id(self):
"""Get the id of the discussion module on the page"""
return self.q(css='div.discussion-module').attrs('data-discussion-id')[0]
@property
def discussion_page(self):
"""Get the discussion as a bok_choy page object"""
if not hasattr(self, '_discussion_page'):
# pylint: disable=attribute-defined-outside-init
self._discussion_page = InlineDiscussionPage(self.browser, self.discussion_id)
return self._discussion_page
@property
def team_name(self):
"""Get the team's name as displayed in the page header"""
return self.q(css='.page-header .page-title')[0].text
@property
def team_description(self):
"""Get the team's description as displayed in the page header"""
return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text
......@@ -4,14 +4,19 @@ Acceptance tests for the teams feature.
import json
from nose.plugins.attrib import attr
from uuid import uuid4
from ..helpers import UniqueCourseTest
from ...pages.lms.teams import TeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage
from ...fixtures import LMS_BASE_URL
from ...fixtures.course import CourseFixture
from ...pages.lms.tab_nav import TabNavPage
from ...fixtures.discussion import (
Thread,
MultipleThreadFixture
)
from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.course_info import CourseInfoPage
from ...pages.lms.tab_nav import TabNavPage
from ...pages.lms.teams import TeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage, TeamPage
class TeamsTabBase(UniqueCourseTest):
......@@ -26,6 +31,33 @@ class TeamsTabBase(UniqueCourseTest):
"""Create `num_topics` test topics."""
return [{u"description": str(i), u"name": str(i), u"id": i} for i in xrange(num_topics)]
def create_teams(self, topic, num_teams):
"""Create `num_teams` teams belonging to `topic`."""
teams = []
for i in xrange(num_teams):
team = {
'course_id': self.course_id,
'topic_id': topic['id'],
'name': 'Team {}'.format(i),
'description': 'Description {}'.format(i)
}
response = self.course_fixture.session.post(
LMS_BASE_URL + '/api/team/v0/teams/',
data=json.dumps(team),
headers=self.course_fixture.headers
)
teams.append(json.loads(response.text))
return teams
def create_membership(self, username, team_id):
"""Assign `username` to `team_id`."""
response = self.course_fixture.session.post(
LMS_BASE_URL + '/api/team/v0/team_membership/',
data=json.dumps({'username': username, 'team_id': team_id}),
headers=self.course_fixture.headers
)
return json.loads(response.text)
def set_team_configuration(self, configuration, enroll_in_course=True, global_staff=False):
"""
Sets team configuration on the course and calls auto-auth on the user.
......@@ -272,33 +304,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic)
self.topics_page = BrowseTopicsPage(self.browser, self.course_id)
def create_teams(self, num_teams):
"""Create `num_teams` teams belonging to `self.topic`."""
teams = []
for i in xrange(num_teams):
team = {
'course_id': self.course_id,
'topic_id': self.topic['id'],
'name': 'Team {}'.format(i),
'description': 'Description {}'.format(i)
}
response = self.course_fixture.session.post(
LMS_BASE_URL + '/api/team/v0/teams/',
data=json.dumps(team),
headers=self.course_fixture.headers
)
teams.append(json.loads(response.text))
return teams
def create_membership(self, username, team_id):
"""Assign `username` to `team_id`."""
response = self.course_fixture.session.post(
LMS_BASE_URL + '/api/team/v0/team_membership/',
data=json.dumps({'username': username, 'team_id': team_id}),
headers=self.course_fixture.headers
)
return json.loads(response.text)
def verify_page_header(self):
"""Verify that the page header correctly reflects the current topic's name and description."""
self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name'])
......@@ -380,7 +385,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And I should see a button to add a team
And I should not see a pagination footer
"""
teams = self.create_teams(self.TEAMS_PAGE_SIZE)
teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE)
self.browse_teams_page.visit()
self.verify_page_header()
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1-10 out of 10 total')
......@@ -403,7 +408,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And when I click on the previous page button
Then I should see that I am on the first page of results
"""
teams = self.create_teams(self.TEAMS_PAGE_SIZE + 1)
teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1)
self.browse_teams_page.visit()
self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True)
......@@ -425,7 +430,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
When I input the first page
Then I should see that I am on the first page of results
"""
teams = self.create_teams(self.TEAMS_PAGE_SIZE + 10)
teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10)
self.browse_teams_page.visit()
self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
......@@ -445,7 +450,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And I should see the team for that topic
And I should see that the team card shows my membership
"""
teams = self.create_teams(1)
teams = self.create_teams(self.topic, 1)
self.browse_teams_page.visit()
self.verify_page_header()
self.verify_teams(teams)
......@@ -615,23 +620,18 @@ class CreateTeamTest(TeamsTabBase):
Then I should see the Create Team header and form
When I fill all the fields present with appropriate data
And I click Create button
Then I should see teams list page with newly created team.
Then I should see the page for my team
"""
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total')
self.verify_and_navigate_to_create_team_page()
self.fill_create_form()
self.create_team_page.submit_form()
self.assertTrue(self.browse_teams_page.is_browser_on_page())
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1 out of 1 total')
# Verify the newly created team content.
team_card = self.browse_teams_page.team_cards.results[0]
self.assertEqual(team_card.find_element_by_css_selector('.card-title').text, self.team_name)
self.assertEqual(
team_card.find_element_by_css_selector('.card-description').text,
'The Avengers are a fictional team of superheroes.'
)
# Verify that the page is shown for the new team
team_page = TeamPage(self.browser, self.course_id)
team_page.wait_for_page()
self.assertEqual(team_page.team_name, self.team_name)
self.assertEqual(team_page.team_description, 'The Avengers are a fictional team of superheroes.')
def test_user_can_cancel_the_team_creation(self):
"""
......@@ -649,3 +649,47 @@ class CreateTeamTest(TeamsTabBase):
self.assertTrue(self.browse_teams_page.is_browser_on_page())
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total')
@attr('shard_5')
class TeamPageTest(TeamsTabBase):
"""Tests for viewing a specific team"""
def setUp(self):
super(TeamPageTest, self).setUp()
self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"}
self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]})
self.team = self.create_teams(self.topic, 1)[0]
self.create_membership(self.user_info['username'], self.team['id'])
self.team_page = TeamPage(self.browser, self.course_id, self.team)
def setup_thread(self):
"""
Set up the discussion thread for the team.
"""
thread = Thread(
id="test_thread_{}".format(uuid4().hex),
commentable_id=self.team['discussion_topic_id'],
body="Dummy text body."
)
thread_fixture = MultipleThreadFixture([thread])
thread_fixture.push()
return thread
def test_discussion_on_team_page(self):
"""
Scenario: Team Page renders a team discussion.
Given I am enrolled in a course with a team configuration, a topic,
and a team belonging to that topic
When a thread exists in the team's discussion
And I visit the Team page for that team
Then I should see a discussion with the correct discussion_id
And I should see the existing thread
"""
thread = self.setup_thread()
self.team_page.visit()
self.assertEqual(self.team_page.discussion_id, self.team['discussion_topic_id'])
discussion = self.team_page.discussion_page
self.assertTrue(discussion.is_browser_on_page())
self.assertTrue(discussion.is_discussion_expanded())
self.assertEqual(discussion.get_num_displayed_threads(), 1)
self.assertTrue(discussion.has_thread(thread['id']))
......@@ -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)
......
......@@ -16,6 +16,10 @@
country: '',
language: '',
membership: []
},
initialize: function(options) {
this.url = options.url;
}
});
return Team;
......
......@@ -10,6 +10,10 @@
description: '',
team_count: 0,
id: ''
},
initialize: function(options) {
this.url = options.url;
}
});
return Topic;
......
......@@ -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 () {
......
......@@ -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'
});
});
......
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);
});
});
});
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);
});
});
});
......@@ -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) {
......
......@@ -2,8 +2,9 @@ define([
'jquery',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'teams/js/views/teams_tab'
], function ($, Backbone, AjaxHelpers, TeamsTabView) {
'teams/js/views/teams_tab',
'URI'
], function ($, Backbone, AjaxHelpers, TeamsTabView, URI) {
'use strict';
describe('TeamsTab', function () {
......@@ -33,14 +34,14 @@ define([
results: [{
description: 'test description',
name: 'test topic',
id: 'test_id',
id: 'test_topic',
team_count: 0
}]
},
topic_url: 'api/topics/topic_id,course_id',
topics_url: 'topics_url',
teams_url: 'teams_url',
course_id: 'test/course/id'
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');
......@@ -55,26 +56,37 @@ define([
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.');
});
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 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 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 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'));
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'));
});
});
});
});
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
};
});
......@@ -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 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 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);
......@@ -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);
......
<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>
......@@ -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" />
......@@ -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:
......
......@@ -64,6 +64,7 @@
'logger': 'empty:',
'utility': 'empty:',
'URI': 'empty:',
'DiscussionModuleView': 'empty:'
},
/**
......
......@@ -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);
}
......
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