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);
this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
// Only allow the topic field for course threads, as standalone threads
// cannot be moved.
if (this.context === 'course') {
this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this;
},
......@@ -53,13 +58,14 @@
var title = this.$('.edit-post-title').val(),
threadType = this.$(".post-type-input:checked").val(),
body = this.$('.edit-post-body textarea').val(),
commentableId = this.topicView.getCurrentTopicId(),
postData = {
title: title,
thread_type: threadType,
body: body,
commentable_id: commentableId
body: body
};
if (this.topicView) {
postData.commentable_id = this.topicView.getCurrentTopicId();
}
return DiscussionUtil.safeAjax({
$elem: this.submitBtn,
......@@ -75,7 +81,9 @@
this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html('');
postData.courseware_title = this.topicView.getFullTopicName();
if (this.topicView) {
postData.courseware_title = this.topicView.getFullTopicName();
}
this.model.set(postData).unset('abbreviatedBody');
this.trigger('thread:updated');
if (this.threadType !== threadType) {
......
......@@ -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,7 +92,10 @@ if Backbone?
comment.set('thread', @model.get('thread'))
view = new ResponseCommentView(model: comment)
view.render()
@$el.find(".comments .new-comment").before(view.el)
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?
@cancelCommentEdits()
......
<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">
<a href="javascript:void(0)" class="action-button action-more" role="button" aria-haspopup="true" aria-controls="action-menu-<%= contentId %>">
<span class="action-label"><%- gettext("More") %></span>
<span class="action-icon"><i class="icon fa fa-ellipsis-h"></i></span>
</a>
<div class="actions-dropdown" id="action-menu-<%= contentType %>" aria-expanded="false">
<ul class="actions-dropdown-list">
<% _.each(secondaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>
</ul>
<% 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">
<a href="javascript:void(0)" class="action-button action-more" role="button" aria-haspopup="true" aria-controls="action-menu-<%= contentId %>">
<span class="action-label"><%- gettext("More") %></span>
<span class="action-icon"><i class="icon fa fa-ellipsis-h"></i></span>
</a>
<div class="actions-dropdown" id="action-menu-<%= contentType %>" aria-expanded="false">
<ul class="actions-dropdown-list">
<% _.each(secondaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>
</ul>
</div>
</div>
</div>
</li>
</ul>
</li>
</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,16 +12,16 @@
</a>
<ol class="comments">
<li class="new-comment">
<% if (create_sub_comment) { %>
<form class="comment-form" data-id="<%- wmdId %>">
<ul class="discussion-errors"></ul>
<label class="sr" for="add-new-comment"><%- gettext("Add a comment") %></label>
<div class="comment-body" id="add-new-comment" data-id="<%- wmdId %>"
data-placeholder="<%- gettext('Add a comment') %>"></div>
<div class="comment-post-control">
<a class="discussion-submit-comment control-button" href="#"><%- gettext("Submit") %></a>
</div>
</form>
<% } %>
<% 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>
<div class="comment-body" id="add-new-comment" data-id="<%- wmdId %>"
data-placeholder="<%- gettext('Add a comment') %>"></div>
<div class="comment-post-control">
<a class="discussion-submit-comment control-button" href="#"><%- gettext("Submit") %></a>
</div>
</form>
<% } %>
</li>
</ol>
......@@ -40,19 +40,22 @@
<span class="post-label-closed"><i class="icon fa fa-lock"></i><%- gettext("Closed") %></span>
</div>
</div>
<div class="post-header-actions post-extended-content">
<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'post',
primaryActions: ['vote', 'follow'],
secondaryActions: ['pin', 'edit', 'delete', 'report', 'close']
}
)
%>
</div>
<% if (!readOnly) { %>
<div class="post-header-actions post-extended-content">
<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'post',
primaryActions: ['vote', 'follow'],
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"/>
<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>
<% 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,26 +56,67 @@ define([
expectContent('This is the new Teams tab.');
});
it('can switch tabs', function () {
teamsTabView.$('a.nav-item[data-url="browse"]').click();
expectContent('test description');
teamsTabView.$('a.nav-item[data-url="teams"]').click();
expectContent('This is the new Teams tab.');
});
describe('Navigation', function () {
it('can switch tabs', function () {
teamsTabView.$('a.nav-item[data-url="browse"]').click();
expectContent('test description');
teamsTabView.$('a.nav-item[data-url="teams"]').click();
expectContent('This is the new Teams tab.');
});
it('displays and focuses an error message when trying to navigate to a nonexistent 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 route', function () {
teamsTabView.router.navigate('test', {trigger: true});
expectError('The page "test" could not be found.');
expectFocus(teamsTabView.$('.warning'));
it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () {
var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('topics/no_such_topic', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/no_such_topic,test/course/id', null);
AjaxHelpers.respondWithError(requests, 404);
expectError('The topic "no_such_topic" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
it('displays and focuses an error message when trying to navigate to a nonexistent team', function () {
var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('teams/test_topic/no_such_team', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team', null);
AjaxHelpers.respondWithError(requests, 404);
expectError('The team "no_such_team" could not be found.');
expectFocus(teamsTabView.$('.warning'));
});
});
it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () {
var requests = AjaxHelpers.requests(this);
teamsTabView.router.navigate('topics/test', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/test,course_id', null);
AjaxHelpers.respondWithError(requests, 404);
expectError('The topic "test" could not be found.');
expectFocus(teamsTabView.$('.warning'));
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
};
});
......@@ -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