Commit 1a251a49 by Andy Armstrong

Merge pull request #9105 from edx/andya/team-discussion-component

Show discussion component on Team view
parents 3570d088 2f7fa6fd
......@@ -67,5 +67,6 @@ class @DiscussionSpecHelper
data-course-name="Fake Course"
data-user-create-comment="true"
data-user-create-subcomment="true"
data-read-only="false"
></div>
""")
......@@ -205,7 +205,7 @@ describe "DiscussionUserProfileView", ->
)
{always: ->}
)
@view.$(".pagination a").first().click()
@view.$(".discussion-pagination a").first().click()
expect(@view.$(".current-page").text()).toEqual("42")
expect(@view.$(".last-page").text()).toEqual("99")
......@@ -216,5 +216,5 @@ describe "DiscussionUserProfileView", ->
params.error()
{always: ->}
)
@view.$(".pagination a").first().click()
@view.$(".discussion-pagination a").first().click()
expect(DiscussionUtil.discussionAlert).toHaveBeenCalled()
......@@ -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")
)
......@@ -152,7 +155,7 @@ if Backbone?
"?discussion_page=#{number}"
params = DiscussionUtil.getPaginationParams(@page, numPages, pageUrl)
pagination = _.template($("#pagination-template").html())(params)
@$('section.pagination').html(pagination)
@$('section.discussion-pagination').html(pagination)
navigateToPage: (event) =>
event.preventDefault()
......
......@@ -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);
// 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('');
if (this.topicView) {
postData.courseware_title = this.topicView.getFullTopicName();
}
this.model.set(postData).unset('abbreviatedBody');
this.trigger('thread:updated');
if (this.threadType !== threadType) {
......
......@@ -13,7 +13,8 @@ if Backbone?
mode: @mode,
flagged: @model.isFlagged(),
author_display: @getAuthorDisplay(),
cid: @model.cid
cid: @model.cid,
readOnly: $('.discussion-module').data('read-only')
},
@model.attributes,
)
......
......@@ -19,9 +19,12 @@ 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)
@readOnly = $(".discussion-module").data('read-only')
# Quick fix to have an actual model when we're receiving new models from
# the server.
@model.collection.on "reset", (collection) =>
......@@ -50,12 +53,15 @@ if Backbone?
renderTemplate: ->
@template = _.template($("#thread-template").html())
templateData = @model.toJSON()
container = $("#discussion-container")
if !container.length
# inline discussion
container = $(".discussion-module")
templateData.can_create_comment = container.data("user-create-comment")
templateData = _.extend(
@model.toJSON(),
readOnly: @readOnly,
can_create_comment: container.data("user-create-comment")
)
@template(templateData)
render: ->
......@@ -300,6 +306,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
......
......@@ -18,7 +18,7 @@ if Backbone?
baseUri = URI(window.location).removeSearch("page")
pageUrlFunc = (page) -> baseUri.clone().addSearch("page", page)
paginationParams = DiscussionUtil.getPaginationParams(@page, @numPages, pageUrlFunc)
@$el.find(".pagination").html(_.template($("#pagination-template").html())(paginationParams))
@$el.find(".discussion-pagination").html(_.template($("#pagination-template").html())(paginationParams))
changePage: (event) ->
event.preventDefault()
......
......@@ -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
......
......@@ -9,7 +9,8 @@ if Backbone?
_.extend(
{
cid: @model.cid,
author_display: @getAuthorDisplay()
author_display: @getAuthorDisplay(),
readOnly: $('.discussion-module').data('read-only')
},
@model.attributes
)
......
......@@ -10,7 +10,8 @@ if Backbone?
{
cid: @model.cid,
author_display: @getAuthorDisplay(),
endorser_display: @getEndorserDisplay()
endorser_display: @getEndorserDisplay(),
readOnly: $('.discussion-module').data('read-only')
},
@model.attributes
)
......
......@@ -13,17 +13,21 @@ if Backbone?
initialize: (options) ->
@collapseComments = options.collapseComments
@createShowView()
@readOnly = $('.discussion-module').data('read-only')
renderTemplate: ->
@template = _.template($("#thread-response-template").html())
templateData = @model.toJSON()
templateData.wmdId = @model.id ? (new Date()).getTime()
container = $("#discussion-container")
if !container.length
# inline discussion
container = $(".discussion-module")
templateData.create_sub_comment = container.data("user-create-subcomment")
templateData = _.extend(
@model.toJSON(),
wmdId: @model.id ? (new Date()).getTime(),
create_sub_comment: container.data("user-create-subcomment"),
readOnly: @readOnly
)
@template(templateData)
render: ->
......@@ -88,6 +92,9 @@ if Backbone?
comment.set('thread', @model.get('thread'))
view = new ResponseCommentView(model: comment)
view.render()
if @readOnly
@$el.find('.comments').append(view.el)
else
@$el.find(".comments .new-comment").before(view.el)
view.bind "comment:edit", (event) =>
@cancelEdit(event) if @editView?
......
<ul class="<%= contentType %>-actions-list">
<% if (!readOnly) { %>
<ul class="<%= contentType %>-actions-list">
<% _.each(primaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>
<li class="actions-item is-visible">
<div class="more-wrapper">
......@@ -13,4 +14,5 @@
</div>
</div>
</li>
</ul>
</ul>
<% } %>
......@@ -8,6 +8,6 @@
<% }); %>
</section>
<section class="pagination">
<section class="discussion-pagination">
</section>
</section>
......@@ -7,7 +7,8 @@
contentId: cid,
contentType: 'comment',
primaryActions: [],
secondaryActions: ['edit', 'delete', 'report']
secondaryActions: ['edit', 'delete', 'report'],
readOnly: readOnly
}
)
%>
......
......@@ -49,7 +49,8 @@
contentId: cid,
contentType: 'response',
primaryActions: ['vote', thread.get('thread_type') == 'question' ? 'answer' : 'endorse'],
secondaryActions: ['edit', 'delete', 'report']
secondaryActions: ['edit', 'delete', 'report'],
readOnly: readOnly
}
)
%>
......
......@@ -12,7 +12,7 @@
</a>
<ol class="comments">
<li class="new-comment">
<% if (create_sub_comment) { %>
<% if (create_sub_comment && !readOnly) { %>
<form class="comment-form" data-id="<%- wmdId %>">
<ul class="discussion-errors"></ul>
<label class="sr" for="add-new-comment"><%- gettext("Add a comment") %></label>
......
......@@ -40,6 +40,7 @@
<span class="post-label-closed"><i class="icon fa fa-lock"></i><%- gettext("Closed") %></span>
</div>
</div>
<% if (!readOnly) { %>
<div class="post-header-actions post-extended-content">
<%=
_.template(
......@@ -48,11 +49,13 @@
contentId: cid,
contentType: 'post',
primaryActions: ['vote', 'follow'],
secondaryActions: ['pin', 'edit', 'delete', 'report', 'close']
secondaryActions: ['pin', 'edit', 'delete', 'report', 'close'],
readOnly: readOnly
}
)
%>
</div>
<% } %>
</header>
<div class="post-body"><%- body %></div>
......
......@@ -8,18 +8,20 @@
</div>
<div class="post-extended-content">
<div class="response-count"/>
<% if (!readOnly) { %>
<div class="add-response">
<button class="button add-response-btn">
<i class="icon fa fa-reply"></i>
<span class="add-response-btn-text"><%- gettext("Add a Response") %></span>
</button>
</div>
<% } %>
<ol class="responses js-response-list"/>
<div class="response-pagination"/>
<div class="post-status-closed bottom-post-status" style="display: none">
<%- gettext("This thread is closed.") %>
</div>
<% if (can_create_comment) { %>
<% if (can_create_comment && !readOnly) { %>
<form class="discussion-reply-new" data-id="<%- id %>">
<h4><%- gettext("Post a response:") %></h4>
<ul class="discussion-errors"></ul>
......
......@@ -4,4 +4,4 @@
<article class="discussion-thread" id="thread_<%= thread.id %>"/>
<% }); %>
</section>
<section class="pagination"/>
<section class="discussion-pagination"/>
......@@ -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
......@@ -1193,7 +1193,11 @@ class TeamsPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSe
thread_author = getattr(self, thread_author)
self._setup_mock(
user, mock_request, # user is the person making the request.
{"user_id": str(thread_author.id), "closed": False, "commentable_id": commentable_id}
{
"user_id": str(thread_author.id),
"closed": False, "commentable_id": commentable_id,
"context": "standalone"
}
)
response = self.client.post(
reverse(
......@@ -1203,7 +1207,7 @@ class TeamsPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSe
"thread_id": "dummy"
}
),
data={"body": "foo", "title": "foo"}
data={"body": "foo", "title": "foo", "commentable_id": commentable_id}
)
self.assertEqual(response.status_code, status_code)
......
......@@ -250,11 +250,13 @@ def update_thread(request, course_id, thread_id):
if "thread_type" in request.POST:
thread.thread_type = request.POST["thread_type"]
if "commentable_id" in request.POST:
commentable_id = request.POST["commentable_id"]
course = get_course_with_access(request.user, 'load', course_key)
if discussion_category_id_access(course, request.user, request.POST.get("commentable_id")):
thread.commentable_id = request.POST["commentable_id"]
else:
thread_context = getattr(thread, "context", "course")
if thread_context == "course" and not discussion_category_id_access(course, request.user, commentable_id):
return JsonError(_("Topic doesn't exist"))
else:
thread.commentable_id = commentable_id
thread.save()
if request.is_ajax():
......
......@@ -176,6 +176,7 @@ def make_mock_request_impl(
thread_id=thread_id,
num_children=num_thread_responses,
group_id=group_id,
commentable_id=commentable_id
)
elif "/users/" in url:
data = {
......@@ -336,8 +337,8 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
@ddt.data(
# old mongo with cache
(ModuleStoreEnum.Type.mongo, 1, 7, 5, 14, 8),
(ModuleStoreEnum.Type.mongo, 50, 7, 5, 14, 8),
(ModuleStoreEnum.Type.mongo, 1, 6, 4, 14, 8),
(ModuleStoreEnum.Type.mongo, 50, 6, 4, 14, 8),
# split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, 1, 3, 3, 14, 8),
(ModuleStoreEnum.Type.split, 50, 3, 3, 14, 8),
......@@ -668,6 +669,40 @@ class SingleThreadContentGroupTestCase(ContentGroupTestCase):
self.assert_can_access(self.non_cohorted_user, self.beta_module.discussion_id, thread_id, False)
def test_course_context_respected(self, mock_request):
"""
Verify that course threads go through discussion_category_id_access method.
"""
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl(
course=self.course, text="dummy content", thread_id=thread_id
)
# Beta user does not have access to alpha_module.
self.assert_can_access(self.beta_user, self.alpha_module.discussion_id, thread_id, False)
def test_standalone_context_respected(self, mock_request):
"""
Verify that standalone threads don't go through discussion_category_id_access method.
"""
# For this rather pathological test, we are assigning the alpha module discussion_id (commentable_id)
# to a team so that we can verify that standalone threads don't go through discussion_category_id_access.
thread_id = "test_thread_id"
CourseTeamFactory(
name="A team",
course_id=self.course.id,
topic_id='topic_id',
discussion_topic_id=self.alpha_module.discussion_id
)
mock_request.side_effect = make_mock_request_impl(
course=self.course, text="dummy content", thread_id=thread_id,
commentable_id=self.alpha_module.discussion_id
)
# If a thread returns context other than "course", the access check is not done, and the beta user
# can see the alpha discussion module.
self.assert_can_access(self.beta_user, self.alpha_module.discussion_id, thread_id, True)
@patch('lms.lib.comment_client.utils.requests.request')
class InlineDiscussionContextTestCase(ModuleStoreTestCase):
......
......@@ -320,10 +320,6 @@ def single_thread(request, course_key, discussion_id, thread_id):
user_info = cc_user.to_dict()
is_moderator = has_permission(request.user, "see_all_cohorts", course_key)
# Verify that the student has access to this thread if belongs to a discussion module
if discussion_id not in utils.get_discussion_categories_ids(course, request.user):
raise Http404
# Currently, the front end always loads responses via AJAX, even for this
# page; it would be a nice optimization to avoid that extra round trip to
# the comments service.
......@@ -339,6 +335,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)
......
......@@ -22,6 +22,7 @@ from openedx.core.djangoapps.course_groups.tests.helpers import config_course_co
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.util.testing import ContentGroupTestCase
from student.roles import CourseStaffRole
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_MIXED_TOY_MODULESTORE
from xmodule.modulestore.django import modulestore
......@@ -80,6 +81,8 @@ class AccessUtilsTestCase(ModuleStoreTestCase):
self.community_ta_role.users.add(self.community_ta1)
self.community_ta2 = UserFactory(username='community_ta2', email='community_ta2@edx.org')
self.community_ta_role.users.add(self.community_ta2)
self.course_staff = UserFactory(username='course_staff', email='course_staff@edx.org')
CourseStaffRole(self.course_id).add_users(self.course_staff)
def test_get_role_ids(self):
ret = utils.get_role_ids(self.course_id)
......@@ -89,6 +92,7 @@ class AccessUtilsTestCase(ModuleStoreTestCase):
def test_has_discussion_privileges(self):
self.assertFalse(utils.has_discussion_privileges(self.student1, self.course_id))
self.assertFalse(utils.has_discussion_privileges(self.student2, self.course_id))
self.assertFalse(utils.has_discussion_privileges(self.course_staff, self.course_id))
self.assertTrue(utils.has_discussion_privileges(self.moderator, self.course_id))
self.assertTrue(utils.has_discussion_privileges(self.community_ta1, self.course_id))
self.assertTrue(utils.has_discussion_privileges(self.community_ta2, self.course_id))
......
......@@ -351,6 +351,7 @@ def get_discussion_category_map(course, user, cohorted_if_in_list=False, exclude
def discussion_category_id_access(course, user, discussion_id):
"""
Returns True iff the given discussion_id is accessible for user in course.
Assumes that the commentable identified by discussion_id has a null or 'course' context.
Uses the discussion id cache if available, falling back to
get_discussion_categories_ids if there is no cache.
"""
......
......@@ -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,6 +56,7 @@ define([
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');
......@@ -62,19 +64,59 @@ define([
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.');
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/test', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/test,course_id', null);
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 "test" could not be found.');
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'));
});
});
describe('Discussion privileges', function () {
it('allows privileged access to any team', function () {
teamsTabView.$el.data('privileged', true);
// Note: using `undefined` here to ensure that we
// don't even look at the team when the user is
// privileged
expect(teamsTabView.readOnlyDiscussion(undefined)).toBe(false);
});
it('allows access to a team which an unprivileged user is a member of', function () {
teamsTabView.$el.data('privileged', false).data('username', 'test-user');
expect(teamsTabView.readOnlyDiscussion({
attributes: {
membership: [{
user: {
username: 'test-user'
}
}]
}
})).toBe(false);
});
it('does not allow access if the user is neither privileged nor a team member', function () {
teamsTabView.$el.data('privileged', false).data('username', 'test-user');
expect(teamsTabView.readOnlyDiscussion({
attributes: { membership: [] }
})).toBe(true);
});
});
});
});
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
};
});
;(function (define) {
'use strict';
'use strict';
define(['backbone',
define(['backbone',
'underscore',
'gettext',
'js/views/fields',
......@@ -14,55 +14,56 @@ define(['backbone',
maxTeamDescriptionLength: 300,
events: {
"click .action-primary": "createTeam",
"click .action-cancel": "goBackToTopic"
'click .action-primary': 'createTeam',
'click .action-cancel': 'goBackToTopic'
},
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.teamNameField = new FieldViews.TextFieldView({
model: this.teamModel,
title: gettext("Team Name (Required) *"),
title: gettext('Team Name (Required) *'),
valueAttribute: 'name',
helpMessage: gettext("A name that identifies your team (maximum 255 characters).")
helpMessage: gettext('A name that identifies your team (maximum 255 characters).')
});
this.teamDescriptionField = new FieldViews.TextareaFieldView({
model: this.teamModel,
title: gettext("Team Description (Required) *"),
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).")
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"),
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.")
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"),
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.")
helpMessage: gettext('The language that team members primarily use to communicate with each other.')
});
this.teamCountryField = new FieldViews.DropdownFieldView({
......@@ -73,7 +74,7 @@ define(['backbone',
showMessages: false,
titleIconName: 'fa-globe',
options: this.countries,
helpMessage: gettext("The country that team members primarily identify with.")
helpMessage: gettext('The country that team members primarily identify with.')
});
},
......@@ -97,16 +98,15 @@ define(['backbone',
},
createTeam: function () {
var teamName = this.teamNameField.fieldValue();
var teamDescription = this.teamDescriptionField.fieldValue();
var teamLanguage = this.teamLanguageField.fieldValue();
var teamCountry = this.teamCountryField.fieldValue();
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,
name: this.teamNameField.fieldValue(),
description: this.teamDescriptionField.fieldValue(),
language: _.isNull(teamLanguage) ? '' : teamLanguage,
country: _.isNull(teamCountry) ? '' : teamCountry
};
......@@ -117,23 +117,22 @@ define(['backbone',
return;
}
var view = this;
var options = {
wait: true,
success: function () {
view.goBackToTopic();
},
error: function () {
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);
}
};
this.teamModel.save(data, options);
});
},
validateTeamData: function (data) {
var status = true,
message = gettext("Check the highlighted fields below and try again.");
message = gettext('Check the highlighted fields below and try again.');
var srMessages = [];
this.teamNameField.unhighlightField();
......@@ -143,13 +142,13 @@ define(['backbone',
status = false;
this.teamNameField.highlightFieldOnError();
srMessages.push(
gettext("Enter team name.")
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.")
gettext('Team name cannot have more than 255 characters.')
);
}
......@@ -157,20 +156,20 @@ define(['backbone',
status = false;
this.teamDescriptionField.highlightFieldOnError();
srMessages.push(
gettext("Enter team description.")
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.")
gettext('Team description cannot have more than 300 characters.')
);
}
return {
status: status,
message: message,
srMessage: srMessages.join(" ")
srMessage: srMessages.join(' ')
};
},
......@@ -185,7 +184,7 @@ define(['backbone',
},
goBackToTopic: function () {
Backbone.history.navigate("topics/" + this.topicId, {trigger: true});
Backbone.history.navigate('topics/' + this.topicId, {trigger: true});
}
});
});
......
......@@ -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');
this.readOnly = options.readOnly;
},
render: function () {
this.$el.html(_.template(teamTemplate, {
courseID: this.courseID,
discussionTopicID: this.discussionTopicID,
readOnly: this.readOnly
}));
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-read-only="<%= readOnly %>"
data-user-create-comment="<%= !readOnly %>"
data-user-create-subcomment="<%= !readOnly %>">
<% if ( !readOnly) { %>
<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,30 +7,42 @@
<%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'" />
<div class="container">
<div class="teams-wrapper">
<section class="teams-content">
<section class="teams-content" data-username=${json.dumps(username, cls=EscapedEdxJSONEncoder)} data-privileged="${json.dumps(privileged)}">
</section>
</div>
</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" />
......@@ -467,7 +467,7 @@ class TestCreateTeamAPI(TeamAPITestCase):
# Verify that the creating user gets added to the team.
self.assertEqual(len(team_membership), 1)
member = team_membership[0]['user']
self.assertEqual(member['id'], creator)
self.assertEqual(member['username'], creator)
self.assertEqual(team, {
'name': 'Fully specified team',
......@@ -688,7 +688,7 @@ class TestListMembershipAPI(TeamAPITestCase):
membership = self.get_membership_list(status, {'team_id': self.test_team_1.team_id}, user=user)
if status == 200:
self.assertEqual(membership['count'], 1)
self.assertEqual(membership['results'][0]['user']['id'], self.users['student_enrolled'].username)
self.assertEqual(membership['results'][0]['user']['username'], self.users['student_enrolled'].username)
@ddt.data(
(None, 401, False),
......@@ -705,7 +705,7 @@ class TestListMembershipAPI(TeamAPITestCase):
if status == 200:
if has_content:
self.assertEqual(membership['count'], 1)
self.assertEqual(membership['results'][0]['team']['id'], self.test_team_1.team_id)
self.assertEqual(membership['results'][0]['team']['team_id'], self.test_team_1.team_id)
else:
self.assertEqual(membership['count'], 0)
......@@ -754,8 +754,8 @@ class TestCreateMembershipAPI(TeamAPITestCase):
user=user
)
if status == 200:
self.assertEqual(membership['user']['id'], self.users['student_enrolled_not_on_team'].username)
self.assertEqual(membership['team']['id'], self.test_team_1.team_id)
self.assertEqual(membership['user']['username'], self.users['student_enrolled_not_on_team'].username)
self.assertEqual(membership['team']['team_id'], self.test_team_1.team_id)
memberships = self.get_membership_list(200, {'team_id': self.test_team_1.team_id})
self.assertEqual(memberships['count'], 2)
......
......@@ -90,6 +90,7 @@ class TeamsDashboardView(View):
instance=topics_page,
context={'course_id': course.id, 'sort_order': sort_order}
)
user = request.user
context = {
"course": course,
"topics": topics_serializer.data,
......@@ -100,6 +101,8 @@ class TeamsDashboardView(View):
"teams_url": reverse('teams_list', request=request),
"languages": settings.ALL_LANGUAGES,
"countries": list(countries),
"username": user.username,
"privileged": has_discussion_privileges(user, course_key)
}
return render_to_response("teams/teams.html", context)
......
......@@ -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);
}
......
......@@ -753,10 +753,10 @@ body.discussion {
}
section.discussion {
margin-top: ($baseline*1.5);
clear: both;
padding-top: $baseline;
.threads {
margin-top: $baseline;
}
.discussion-thread {
......@@ -936,7 +936,7 @@ body.discussion {
color: $white;
}
section.pagination {
section.discussion-pagination {
margin-top: ($baseline*1.5);
nav.discussion-paginator {
......
......@@ -3,7 +3,7 @@
from django.utils.translation import ugettext as _
%>
<div class="discussion-module" data-discussion-id="${discussion_id | h}" data-user-create-comment="${can_create_comment}" data-user-create-subcomment="${can_create_subcomment}">
<div class="discussion-module" data-discussion-id="${discussion_id | h}" data-user-create-comment="${can_create_comment}" data-user-create-subcomment="${can_create_subcomment}" data-read-only="false">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}" role="button"><span class="show-hide-discussion-icon"></span><span class="button-text">${_("Show Discussion")}</span></a>
% if can_create_thread:
<a href="#" class="new-post-btn" role="button"><span class="icon fa fa-edit new-post-icon"></span>${_("New Post")}</a>
......
......@@ -31,6 +31,7 @@ from django.core.urlresolvers import reverse
data-user-info="${user_info}"
data-user-create-comment="${can_create_comment}"
data-user-create-subcomment="${can_create_subcomment}"
data-read-only="false"
data-threads="${threads}"
data-thread-pages="${thread_pages}"
data-content-info="${annotated_content_info}"
......
......@@ -31,7 +31,6 @@ class PaginationSerializer(pagination.PaginationSerializer):
class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
"""Serializes arbitrary models in a collapsed format, with just an id and url."""
id = serializers.CharField(read_only=True) # pylint: disable=invalid-name
url = serializers.HyperlinkedIdentityField(view_name='')
def __init__(self, model_class, view_name, id_source='id', lookup_field=None, *args, **kwargs):
......@@ -42,7 +41,8 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
view_name (string): Name of the Django view used to lookup the
model.
id_source (string): Optional name of the id field on the model.
Defaults to 'id'.
Defaults to 'id'. Also used as the property name of the field
in the serialized representation.
lookup_field (string): Optional name of the model field used to
lookup the model in the view. Defaults to the value of
id_source.
......@@ -54,7 +54,7 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
super(CollapsedReferenceSerializer, self).__init__(*args, **kwargs)
self.fields['id'].source = id_source
self.fields[id_source] = serializers.CharField(read_only=True, source=id_source)
self.fields['url'].view_name = view_name
self.fields['url'].lookup_field = lookup_field
......@@ -63,4 +63,4 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
model is set dynamically in __init__.
"""
fields = ("id", "url")
fields = ("url",)
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