Commit 63da1907 by Andy Armstrong

Show inline discussion component on Team view

TNL-2515
parent df936837
...@@ -10,10 +10,11 @@ if Backbone? ...@@ -10,10 +10,11 @@ if Backbone?
"click .discussion-paginator a": "navigateToPage" "click .discussion-paginator a": "navigateToPage"
page_re: /\?discussion_page=(\d+)/ page_re: /\?discussion_page=(\d+)/
initialize: -> initialize: (options) ->
@toggleDiscussionBtn = @$(".discussion-show") @toggleDiscussionBtn = @$(".discussion-show")
# Set the page if it was set in the URL. This is used to allow deep linking to pages # 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) match = @page_re.exec(window.location.href)
@context = options.context or "course" # allowed values are "course" or "standalone"
if match if match
@page = parseInt(match[1]) @page = parseInt(match[1])
else else
...@@ -105,6 +106,7 @@ if Backbone? ...@@ -105,6 +106,7 @@ if Backbone?
el: @$("article#thread_#{thread.id}"), el: @$("article#thread_#{thread.id}"),
model: thread, model: thread,
mode: "inline", mode: "inline",
context: @context,
course_settings: @course_settings, course_settings: @course_settings,
topicId: discussionId topicId: discussionId
) )
...@@ -141,6 +143,7 @@ if Backbone? ...@@ -141,6 +143,7 @@ if Backbone?
el: article, el: article,
model: thread, model: thread,
mode: "inline", mode: "inline",
context: @context,
course_settings: @course_settings, course_settings: @course_settings,
topicId: @$el.data("discussion-id") topicId: @$el.data("discussion-id")
) )
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
this.course_settings = options.course_settings; this.course_settings = options.course_settings;
this.threadType = this.model.get('thread_type'); this.threadType = this.model.get('thread_type');
this.topicId = this.model.get('commentable_id'); this.topicId = this.model.get('commentable_id');
this.context = options.context || 'course';
_.bindAll(this); _.bindAll(this);
return this; return this;
}, },
...@@ -31,11 +32,15 @@ ...@@ -31,11 +32,15 @@
threadTypeTemplate = _.template($("#thread-type-template").html()); threadTypeTemplate = _.template($("#thread-type-template").html());
this.addField(threadTypeTemplate({form_id: formId})); this.addField(threadTypeTemplate({form_id: formId}));
this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true); this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true);
this.topicView = new DiscussionTopicMenuView({ // Only allow the topic field for course threads, as standalone threads
topicId: this.topicId, // cannot be moved.
course_settings: this.course_settings if (this.context === 'course') {
}); this.topicView = new DiscussionTopicMenuView({
this.addField(this.topicView.render()); topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body'); DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this; return this;
}, },
...@@ -53,13 +58,14 @@ ...@@ -53,13 +58,14 @@
var title = this.$('.edit-post-title').val(), var title = this.$('.edit-post-title').val(),
threadType = this.$(".post-type-input:checked").val(), threadType = this.$(".post-type-input:checked").val(),
body = this.$('.edit-post-body textarea').val(), body = this.$('.edit-post-body textarea').val(),
commentableId = this.topicView.getCurrentTopicId(),
postData = { postData = {
title: title, title: title,
thread_type: threadType, thread_type: threadType,
body: body, body: body
commentable_id: commentableId
}; };
if (this.topicView) {
postData.commentable_id = this.topicView.getCurrentTopicId();
}
return DiscussionUtil.safeAjax({ return DiscussionUtil.safeAjax({
$elem: this.submitBtn, $elem: this.submitBtn,
...@@ -75,7 +81,9 @@ ...@@ -75,7 +81,9 @@
this.$('.edit-post-title').val('').attr('prev-text', ''); this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', ''); this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html(''); 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.model.set(postData).unset('abbreviatedBody');
this.trigger('thread:updated'); this.trigger('thread:updated');
if (this.threadType !== threadType) { if (this.threadType !== threadType) {
......
...@@ -19,6 +19,7 @@ if Backbone? ...@@ -19,6 +19,7 @@ if Backbone?
initialize: (options) -> initialize: (options) ->
super() super()
@mode = options.mode or "inline" # allowed values are "tab" or "inline" @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"] if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode) throw new Error("invalid mode: " + @mode)
...@@ -300,6 +301,7 @@ if Backbone? ...@@ -300,6 +301,7 @@ if Backbone?
container: @$('.thread-content-wrapper') container: @$('.thread-content-wrapper')
model: @model model: @model
mode: @mode mode: @mode
context: @context
course_settings: @options.course_settings course_settings: @options.course_settings
) )
@editView.bind "thread:updated thread:cancel_edit", @closeEditView @editView.bind "thread:updated thread:cancel_edit", @closeEditView
......
...@@ -87,7 +87,6 @@ if Backbone? ...@@ -87,7 +87,6 @@ if Backbone?
url: url url: url
type: "POST" type: "POST"
dataType: 'json' dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data: data:
thread_type: thread_type thread_type: thread_type
title: title title: title
......
...@@ -389,7 +389,7 @@ class InlineDiscussionPage(PageObject): ...@@ -389,7 +389,7 @@ class InlineDiscussionPage(PageObject):
def __init__(self, browser, discussion_id): def __init__(self, browser, discussion_id):
super(InlineDiscussionPage, self).__init__(browser) super(InlineDiscussionPage, self).__init__(browser)
self._discussion_selector = ( 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 discussion_id=discussion_id
) )
) )
...@@ -418,6 +418,10 @@ class InlineDiscussionPage(PageObject): ...@@ -418,6 +418,10 @@ class InlineDiscussionPage(PageObject):
def get_num_displayed_threads(self): def get_num_displayed_threads(self):
return len(self._find_within(".discussion-thread")) 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): def element_exists(self, selector):
return self.q(css=self._discussion_selector + " " + selector).present return self.q(css=self._discussion_selector + " " + selector).present
......
...@@ -4,6 +4,7 @@ Teams pages. ...@@ -4,6 +4,7 @@ Teams pages.
""" """
from .course_page import CoursePage from .course_page import CoursePage
from .discussion import InlineDiscussionPage
from ..common.paging import PaginatedUIMixin from ..common.paging import PaginatedUIMixin
from .fields import FieldsMixin from .fields import FieldsMixin
...@@ -179,3 +180,50 @@ class CreateTeamPage(CoursePage, FieldsMixin): ...@@ -179,3 +180,50 @@ class CreateTeamPage(CoursePage, FieldsMixin):
"""Click on cancel team button""" """Click on cancel team button"""
self.q(css='.create-team .action-cancel').first.click() self.q(css='.create-team .action-cancel').first.click()
self.wait_for_ajax() 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. ...@@ -4,14 +4,19 @@ Acceptance tests for the teams feature.
import json import json
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from uuid import uuid4
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
from ...pages.lms.teams import TeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage
from ...fixtures import LMS_BASE_URL from ...fixtures import LMS_BASE_URL
from ...fixtures.course import CourseFixture 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.auto_auth import AutoAuthPage
from ...pages.lms.course_info import CourseInfoPage 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): class TeamsTabBase(UniqueCourseTest):
...@@ -26,6 +31,33 @@ class TeamsTabBase(UniqueCourseTest): ...@@ -26,6 +31,33 @@ class TeamsTabBase(UniqueCourseTest):
"""Create `num_topics` test topics.""" """Create `num_topics` test topics."""
return [{u"description": str(i), u"name": str(i), u"id": i} for i in xrange(num_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): 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. Sets team configuration on the course and calls auto-auth on the user.
...@@ -272,33 +304,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -272,33 +304,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic)
self.topics_page = BrowseTopicsPage(self.browser, self.course_id) 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): def verify_page_header(self):
"""Verify that the page header correctly reflects the current topic's name and description.""" """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']) self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name'])
...@@ -380,7 +385,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -380,7 +385,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And I should see a button to add a team And I should see a button to add a team
And I should not see a pagination footer 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.browse_teams_page.visit()
self.verify_page_header() self.verify_page_header()
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1-10 out of 10 total') self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1-10 out of 10 total')
...@@ -403,7 +408,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -403,7 +408,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And when I click on the previous page button And when I click on the previous page button
Then I should see that I am on the first page of results 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.browse_teams_page.visit()
self.verify_page_header() self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True) self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True)
...@@ -425,7 +430,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -425,7 +430,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
When I input the first page When I input the first page
Then I should see that I am on the first page of results 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.browse_teams_page.visit()
self.verify_page_header() self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True) self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
...@@ -445,7 +450,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -445,7 +450,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And I should see the team for that topic And I should see the team for that topic
And I should see that the team card shows my membership 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.browse_teams_page.visit()
self.verify_page_header() self.verify_page_header()
self.verify_teams(teams) self.verify_teams(teams)
...@@ -615,23 +620,18 @@ class CreateTeamTest(TeamsTabBase): ...@@ -615,23 +620,18 @@ class CreateTeamTest(TeamsTabBase):
Then I should see the Create Team header and form Then I should see the Create Team header and form
When I fill all the fields present with appropriate data When I fill all the fields present with appropriate data
And I click Create button 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.verify_and_navigate_to_create_team_page()
self.fill_create_form() self.fill_create_form()
self.create_team_page.submit_form() self.create_team_page.submit_form()
self.assertTrue(self.browse_teams_page.is_browser_on_page()) # Verify that the page is shown for the new team
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1 out of 1 total') team_page = TeamPage(self.browser, self.course_id)
# Verify the newly created team content. team_page.wait_for_page()
team_card = self.browse_teams_page.team_cards.results[0] self.assertEqual(team_page.team_name, self.team_name)
self.assertEqual(team_card.find_element_by_css_selector('.card-title').text, self.team_name) self.assertEqual(team_page.team_description, 'The Avengers are a fictional team of superheroes.')
self.assertEqual(
team_card.find_element_by_css_selector('.card-description').text,
'The Avengers are a fictional team of superheroes.'
)
def test_user_can_cancel_the_team_creation(self): def test_user_can_cancel_the_team_creation(self):
""" """
...@@ -649,3 +649,47 @@ class CreateTeamTest(TeamsTabBase): ...@@ -649,3 +649,47 @@ class CreateTeamTest(TeamsTabBase):
self.assertTrue(self.browse_teams_page.is_browser_on_page()) 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') 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): ...@@ -339,6 +339,11 @@ def single_thread(request, course_key, discussion_id, thread_id):
raise Http404 raise Http404
raise 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 # verify that the thread belongs to the requesting student's cohort
if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: if is_commentable_cohorted(course_key, discussion_id) and not is_moderator:
user_group_id = get_cohort_id(request.user, course_key) user_group_id = get_cohort_id(request.user, course_key)
......
...@@ -16,6 +16,10 @@ ...@@ -16,6 +16,10 @@
country: '', country: '',
language: '', language: '',
membership: [] membership: []
},
initialize: function(options) {
this.url = options.url;
} }
}); });
return Team; return Team;
......
...@@ -10,6 +10,10 @@ ...@@ -10,6 +10,10 @@
description: '', description: '',
team_count: 0, team_count: 0,
id: '' id: ''
},
initialize: function(options) {
this.url = options.url;
} }
}); });
return Topic; return Topic;
......
...@@ -102,10 +102,10 @@ define([ ...@@ -102,10 +102,10 @@ define([
teamEditView.$('.create-team.form-actions .action-primary').click(); teamEditView.$('.create-team.form-actions .action-primary').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', teamsUrl, teamsData); 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(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 () { it('shows validation error message when field is empty', function () {
......
...@@ -2,17 +2,17 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"], ...@@ -2,17 +2,17 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
function($, Backbone, TeamsTabFactory) { function($, Backbone, TeamsTabFactory) {
'use strict'; 'use strict';
describe("Teams tab", function() { describe("Teams Tab Factory", function() {
var teamsTab; var teamsTab;
beforeEach(function() { beforeEach(function() {
setFixtures('<section class="teams-content"></section>'); setFixtures('<section class="teams-content"></section>');
teamsTab = new TeamsTabFactory({ teamsTab = new TeamsTabFactory({
topics: {results: []}, topics: {results: []},
topics_url: '', topicsUrl: '',
teams_url: '', teamsUrl: '',
maxTeamSize: 9999, 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([ ...@@ -2,7 +2,7 @@ define([
'backbone', 'teams/js/collections/team', 'teams/js/views/teams' 'backbone', 'teams/js/collections/team', 'teams/js/views/teams'
], function (Backbone, TeamCollection, TeamsView) { ], function (Backbone, TeamCollection, TeamsView) {
'use strict'; 'use strict';
describe('TeamsView', function () { describe('Teams View', function () {
var teamsView, teamCollection, initialTeams, var teamsView, teamCollection, initialTeams,
createTeams = function (startIndex, stopIndex) { createTeams = function (startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) { return _.map(_.range(startIndex, stopIndex + 1), function (i) {
......
...@@ -2,8 +2,9 @@ define([ ...@@ -2,8 +2,9 @@ define([
'jquery', 'jquery',
'backbone', 'backbone',
'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/ajax_helpers',
'teams/js/views/teams_tab' 'teams/js/views/teams_tab',
], function ($, Backbone, AjaxHelpers, TeamsTabView) { 'URI'
], function ($, Backbone, AjaxHelpers, TeamsTabView, URI) {
'use strict'; 'use strict';
describe('TeamsTab', function () { describe('TeamsTab', function () {
...@@ -33,14 +34,14 @@ define([ ...@@ -33,14 +34,14 @@ define([
results: [{ results: [{
description: 'test description', description: 'test description',
name: 'test topic', name: 'test topic',
id: 'test_id', id: 'test_topic',
team_count: 0 team_count: 0
}] }]
}, },
topic_url: 'api/topics/topic_id,course_id', topicsUrl: 'api/topics/',
topics_url: 'topics_url', topicUrl: 'api/topics/topic_id,test/course/id',
teams_url: 'teams_url', teamsUrl: 'api/teams/',
course_id: 'test/course/id' courseID: 'test/course/id'
}).render(); }).render();
Backbone.history.start(); Backbone.history.start();
spyOn($.fn, 'focus'); spyOn($.fn, 'focus');
...@@ -55,26 +56,37 @@ define([ ...@@ -55,26 +56,37 @@ define([
expectContent('This is the new Teams tab.'); expectContent('This is the new Teams tab.');
}); });
it('can switch tabs', function () { describe('Navigation', function () {
teamsTabView.$('a.nav-item[data-url="browse"]').click(); it('can switch tabs', function () {
expectContent('test description'); teamsTabView.$('a.nav-item[data-url="browse"]').click();
teamsTabView.$('a.nav-item[data-url="teams"]').click(); expectContent('test description');
expectContent('This is the new Teams tab.'); 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 () { it('displays and focuses an error message when trying to navigate to a nonexistent page', function () {
teamsTabView.router.navigate('test', {trigger: true}); teamsTabView.router.navigate('no_such_page', {trigger: true});
expectError('The page "test" could not be found.'); expectError('The page "no_such_page" could not be found.');
expectFocus(teamsTabView.$('.warning')); 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 () { it('displays and focuses an error message when trying to navigate to a nonexistent team', function () {
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('topics/test', {trigger: true}); teamsTabView.router.navigate('teams/test_topic/no_such_team', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/test,course_id', null); AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team', null);
AjaxHelpers.respondWithError(requests, 404); AjaxHelpers.respondWithError(requests, 404);
expectError('The topic "test" could not be found.'); expectError('The team "no_such_team" could not be found.');
expectFocus(teamsTabView.$('.warning')); 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 @@ ...@@ -78,6 +78,11 @@
{span_start: '<span class="sr">', team_name: this.model.get('name'), span_end: '</span>'}, {span_start: '<span class="sr">', team_name: this.model.get('name'), span_end: '</span>'},
true 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; 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 @@ ...@@ -10,8 +10,10 @@
type: 'teams', type: 'teams',
initialize: function (options) { initialize: function (options) {
this.topic = options.topic;
this.itemViewClass = TeamCardView.extend({ this.itemViewClass = TeamCardView.extend({
router: options.router, router: options.router,
topic: options.topic,
maxTeamSize: options.maxTeamSize maxTeamSize: options.maxTeamSize
}); });
PaginatedView.prototype.initialize.call(this); 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 @@ ...@@ -7,8 +7,11 @@
<%block name="bodyclass">view-teams is-in-course course</%block> <%block name="bodyclass">view-teams is-in-course course</%block>
<%block name="pagetitle">${_("Teams")}</%block> <%block name="pagetitle">${_("Teams")}</%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='style-course-vendor'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
<%include file="../discussion/_js_head_dependencies.html" />
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='teams'" /> <%include file="/courseware/course_navigation.html" args="active_page='teams'" />
...@@ -21,16 +24,25 @@ ...@@ -21,16 +24,25 @@
</div> </div>
<%block name="js_extra"> <%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"> <%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) }, topics: ${ json.dumps(topics, cls=EscapedEdxJSONEncoder) },
topic_url: '${ topic_url }', topicUrl: '${ topic_url }',
topics_url: '${ topics_url }', topicsUrl: '${ topics_url }',
teams_url: '${ teams_url }', teamsUrl: '${ teams_url }',
maxTeamSize: ${ course.teams_max_size }, maxTeamSize: ${ course.teams_max_size },
course_id: '${ unicode(course.id) }',
languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) }, languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) },
countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) } countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) }
}); });
</%static:require_module> </%static:require_module>
</%block> </%block>
<%include file="../discussion/_underscore_templates.html" />
...@@ -39,6 +39,7 @@ lib_paths: ...@@ -39,6 +39,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jquery.min.js - 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-ui.min.js
- xmodule_js/common_static/js/vendor/jquery.cookie.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/flot/jquery.flot.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
...@@ -66,8 +67,10 @@ lib_paths: ...@@ -66,8 +67,10 @@ lib_paths:
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
- js - js
- coffee/src
- common/js - common/js
- teams/js - teams/js
- xmodule_js/common_static/coffee
# Paths to spec (test) JavaScript files # Paths to spec (test) JavaScript files
spec_paths: spec_paths:
......
...@@ -64,6 +64,7 @@ ...@@ -64,6 +64,7 @@
'logger': 'empty:', 'logger': 'empty:',
'utility': 'empty:', 'utility': 'empty:',
'URI': 'empty:', 'URI': 'empty:',
'DiscussionModuleView': 'empty:'
}, },
/** /**
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
defineDependency("Logger", "logger"); defineDependency("Logger", "logger");
defineDependency("URI", "URI"); defineDependency("URI", "URI");
defineDependency("Backbone", "backbone"); defineDependency("Backbone", "backbone");
// utility.js adds two functions to the window object, but does not return anything // utility.js adds two functions to the window object, but does not return anything
defineDependency("isExternal", "utility", true); 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