Merge pull request #12782 from edx/ekolpakov/coffee_to_js_discussions
Hackathon project: burn coffee
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
!view/discussion_thread_edit_view_spec.js
|
||||
!view/discussion_topic_menu_view_spec.js
|
||||
@@ -1,115 +0,0 @@
|
||||
describe 'All Content', ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
|
||||
describe 'Staff and TA Content', ->
|
||||
beforeEach ->
|
||||
DiscussionUtil.loadRoles({"Moderator": [567], "Administrator": [567], "Community TA": [567]})
|
||||
|
||||
it 'anonymous thread should not include login role label', ->
|
||||
anon_content = new Content
|
||||
anon_content.initialize
|
||||
expect(anon_content.get 'staff_authored').toBe false
|
||||
expect(anon_content.get 'community_ta_authored').toBe false
|
||||
|
||||
it 'general thread should include login role label', ->
|
||||
anon_content = new Content { user_id: '567' }
|
||||
anon_content.initialize
|
||||
expect(anon_content.get 'staff_authored').toBe true
|
||||
expect(anon_content.get 'community_ta_authored').toBe true
|
||||
|
||||
describe 'Content', ->
|
||||
beforeEach ->
|
||||
@content = new Content {
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
body: 'this is some content',
|
||||
abuse_flaggers: ['123']
|
||||
}
|
||||
|
||||
it 'should exist', ->
|
||||
expect(Content).toBeDefined()
|
||||
|
||||
it 'is initialized correctly', ->
|
||||
@content.initialize
|
||||
expect(Content.contents['01234567']).toEqual @content
|
||||
expect(@content.get 'id').toEqual '01234567'
|
||||
expect(@content.get 'user_url').toEqual '/courses/edX/999/test/discussion/forum/users/567'
|
||||
expect(@content.get 'children').toEqual []
|
||||
expect(@content.get 'comments').toEqual(jasmine.any(Comments))
|
||||
|
||||
it 'can update info', ->
|
||||
@content.updateInfo {
|
||||
ability: {'can_edit': true},
|
||||
voted: true,
|
||||
subscribed: true
|
||||
}
|
||||
expect(@content.get 'ability').toEqual {'can_edit': true}
|
||||
expect(@content.get 'voted').toEqual true
|
||||
expect(@content.get 'subscribed').toEqual true
|
||||
|
||||
it 'can be flagged for abuse', ->
|
||||
@content.flagAbuse()
|
||||
expect(@content.get 'abuse_flaggers').toEqual ['123', '567']
|
||||
|
||||
it 'can be unflagged for abuse', ->
|
||||
temp_array = []
|
||||
temp_array.push(window.user.get('id'))
|
||||
@content.set("abuse_flaggers",temp_array)
|
||||
@content.unflagAbuse()
|
||||
expect(@content.get 'abuse_flaggers').toEqual []
|
||||
|
||||
describe 'Comments', ->
|
||||
beforeEach ->
|
||||
@comment1 = new Comment {id: '123'}
|
||||
@comment2 = new Comment {id: '345'}
|
||||
|
||||
it 'can contain multiple comments', ->
|
||||
myComments = new Comments
|
||||
expect(myComments.length).toEqual 0
|
||||
myComments.add @comment1
|
||||
expect(myComments.length).toEqual 1
|
||||
myComments.add @comment2
|
||||
expect(myComments.length).toEqual 2
|
||||
|
||||
it 'returns results to the find method', ->
|
||||
myComments = new Comments
|
||||
myComments.add @comment1
|
||||
expect(myComments.find('123')).toBe @comment1
|
||||
|
||||
it 'can be endorsed', ->
|
||||
|
||||
DiscussionUtil.loadRoles(
|
||||
{"Moderator": [111], "Administrator": [222], "Community TA": [333]}
|
||||
)
|
||||
@discussionThread = new Thread({id: 1, thread_type: "discussion", user_id: 99})
|
||||
@discussionResponse = new Comment({id: 1, thread: @discussionThread})
|
||||
@questionThread = new Thread({id: 1, thread_type: "question", user_id: 99})
|
||||
@questionResponse = new Comment({id: 1, thread: @questionThread})
|
||||
|
||||
# mod
|
||||
window.user = new DiscussionUser({id: 111})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(true)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# admin
|
||||
window.user = new DiscussionUser({id: 222})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(true)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# TA
|
||||
window.user = new DiscussionUser({id: 333})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(true)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# thread author
|
||||
window.user = new DiscussionUser({id: 99})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(false)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# anyone else
|
||||
window.user = new DiscussionUser({id: 999})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(false)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(false)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
class @DiscussionSpecHelper
|
||||
# This is sad. We should avoid dependence on global vars.
|
||||
@setUpGlobals = ->
|
||||
DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
|
||||
window.$$course_id = "edX/999/test"
|
||||
window.user = new DiscussionUser({username: "test_user", id: "567", upvoted_ids: []})
|
||||
DiscussionUtil.setUser(window.user)
|
||||
|
||||
@makeTA = () ->
|
||||
DiscussionUtil.roleIds["Community TA"].push(parseInt(DiscussionUtil.getUser().id))
|
||||
|
||||
@makeModerator = () ->
|
||||
DiscussionUtil.roleIds["Moderator"].push(parseInt(DiscussionUtil.getUser().id))
|
||||
|
||||
@makeAjaxSpy = (fakeAjax) ->
|
||||
spyOn($, "ajax").and.callFake(
|
||||
(params) ->
|
||||
fakeAjax(params)
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
@makeEventSpy = () ->
|
||||
jasmine.createSpyObj('event', ['preventDefault', 'target'])
|
||||
|
||||
@makeCourseSettings = (is_cohorted=true) ->
|
||||
new DiscussionCourseSettings(
|
||||
category_map:
|
||||
children: ['Test Topic', 'Other Topic']
|
||||
entries:
|
||||
'Test Topic':
|
||||
is_cohorted: is_cohorted
|
||||
id: 'test_topic'
|
||||
'Other Topic':
|
||||
is_cohorted: is_cohorted
|
||||
id: 'other_topic'
|
||||
is_cohorted: is_cohorted
|
||||
)
|
||||
|
||||
@setUnderscoreFixtures = ->
|
||||
templateNames = [
|
||||
'thread', 'thread-show', 'thread-edit',
|
||||
'thread-response', 'thread-response-show', 'thread-response-edit',
|
||||
'response-comment-show', 'response-comment-edit',
|
||||
'thread-list-item', 'discussion-home', 'search-alert',
|
||||
'new-post', 'thread-type', 'new-post-menu-entry',
|
||||
'new-post-menu-category', 'topic', 'post-user-display',
|
||||
'inline-discussion', 'pagination', 'user-profile', 'profile-thread'
|
||||
]
|
||||
templateNamesNoTrailingTemplate = [
|
||||
'forum-action-endorse', 'forum-action-answer', 'forum-action-follow',
|
||||
'forum-action-vote', 'forum-action-report', 'forum-action-pin',
|
||||
'forum-action-close', 'forum-action-edit', 'forum-action-delete',
|
||||
'forum-actions',
|
||||
]
|
||||
|
||||
for templateName in templateNames
|
||||
templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore')
|
||||
appendSetFixtures($('<script>', { id: templateName + '-template', type: 'text/template' })
|
||||
.text(templateFixture))
|
||||
for templateName in templateNamesNoTrailingTemplate
|
||||
templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore')
|
||||
appendSetFixtures($('<script>', { id: templateName, type: 'text/template' })
|
||||
.text(templateFixture))
|
||||
appendSetFixtures("""
|
||||
<div id="fixture-element"></div>
|
||||
<div id="discussion-container"
|
||||
data-course-name="Fake Course"
|
||||
data-user-create-comment="true"
|
||||
data-user-create-subcomment="true"
|
||||
data-read-only="false"
|
||||
></div>
|
||||
""")
|
||||
@@ -1,48 +0,0 @@
|
||||
describe 'DiscussionUtil', ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
|
||||
describe "updateWithUndo", ->
|
||||
|
||||
it "calls through to safeAjax with correct params, and reverts the model in case of failure", ->
|
||||
deferred = $.Deferred()
|
||||
spyOn($, "ajax").and.returnValue(deferred)
|
||||
spyOn(DiscussionUtil, "safeAjax").and.callThrough()
|
||||
|
||||
model = new Backbone.Model({hello: false, number: 42})
|
||||
updates = {hello: "world"}
|
||||
|
||||
# the ajax request should fire and the model should be updated
|
||||
res = DiscussionUtil.updateWithUndo(model, updates, {foo: "bar"}, "error message")
|
||||
expect(DiscussionUtil.safeAjax).toHaveBeenCalled()
|
||||
expect(model.attributes).toEqual({hello: "world", number: 42})
|
||||
|
||||
# the error message callback should be set up correctly
|
||||
spyOn(DiscussionUtil, "discussionAlert")
|
||||
DiscussionUtil.safeAjax.calls.mostRecent().args[0].error()
|
||||
expect(DiscussionUtil.discussionAlert).toHaveBeenCalledWith("Sorry", "error message")
|
||||
|
||||
# if the ajax call ends in failure, the model state should be reverted
|
||||
deferred.reject()
|
||||
expect(model.attributes).toEqual({hello: false, number: 42})
|
||||
|
||||
it "rolls back the changes if the associated element is disabled", ->
|
||||
spyOn(DiscussionUtil, "safeAjax").and.callThrough()
|
||||
|
||||
model = new Backbone.Model({hello: false, number: 42})
|
||||
updates = {hello: "world"}
|
||||
|
||||
# This is the element that is disabled/enabled while the ajax request is
|
||||
# in progress
|
||||
$elem = jasmine.createSpyObj('$elem', ['attr'])
|
||||
$elem.attr.and.returnValue(true)
|
||||
|
||||
res = DiscussionUtil.updateWithUndo(model, updates, {foo: "bar", $elem:$elem}, "error message")
|
||||
|
||||
expect($elem.attr).toHaveBeenCalledWith("disabled")
|
||||
expect(DiscussionUtil.safeAjax).toHaveBeenCalled()
|
||||
expect(model.attributes).toEqual({hello: false, number: 42})
|
||||
|
||||
failed = false
|
||||
res.fail(() => failed = true)
|
||||
expect(failed).toBe(true);
|
||||
@@ -1,43 +0,0 @@
|
||||
describe "DiscussionContentView", ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
@threadData = {
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
body: 'this is a thread',
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
abuse_flaggers: ['123'],
|
||||
votes: {up_count: '42'},
|
||||
type: "thread",
|
||||
roles: []
|
||||
}
|
||||
@thread = new Thread(@threadData)
|
||||
@view = new DiscussionContentView({ model: @thread })
|
||||
@view.setElement($('#fixture-element'))
|
||||
@view.render()
|
||||
|
||||
it 'defines the tag', ->
|
||||
expect($('#jasmine-fixtures')).toExist
|
||||
expect(@view.tagName).toBeDefined
|
||||
expect(@view.el.tagName.toLowerCase()).toBe 'div'
|
||||
|
||||
it "defines the class", ->
|
||||
# spyOn @content, 'initialize'
|
||||
expect(@view.model).toBeDefined();
|
||||
|
||||
it 'is tied to the model', ->
|
||||
expect(@view.model).toBeDefined();
|
||||
|
||||
it 'can be flagged for abuse', ->
|
||||
@thread.flagAbuse()
|
||||
expect(@thread.get 'abuse_flaggers').toEqual ['123', '567']
|
||||
|
||||
it 'can be unflagged for abuse', ->
|
||||
temp_array = []
|
||||
temp_array.push(window.user.get('id'))
|
||||
@thread.set("abuse_flaggers",temp_array)
|
||||
@thread.unflagAbuse()
|
||||
expect(@thread.get 'abuse_flaggers').toEqual []
|
||||
@@ -1,602 +0,0 @@
|
||||
describe "DiscussionThreadListView", ->
|
||||
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
appendSetFixtures("""
|
||||
<script type="text/template" id="thread-list-template">
|
||||
<div class="forum-nav-header">
|
||||
<button type="button" class="forum-nav-browse" id="forum-nav-browse" aria-haspopup="true">
|
||||
<span class="icon fa fa-bars" aria-hidden="true"></span>
|
||||
<span class="sr">Discussion topics; currently listing: </span>
|
||||
<span class="forum-nav-browse-current">All Discussions</span>
|
||||
▾
|
||||
</button>
|
||||
<form class="forum-nav-search">
|
||||
<label>
|
||||
<span class="sr">Search all posts</span>
|
||||
<input class="forum-nav-search-input" id="forum-nav-search" type="text" placeholder="Search all posts">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div class="forum-nav-browse-menu-wrapper" style="display: none">
|
||||
<form class="forum-nav-browse-filter">
|
||||
<label>
|
||||
<span class="sr">Filter Topics</span>
|
||||
<input type="text" class="forum-nav-browse-filter-input" placeholder="filter topics">
|
||||
</label>
|
||||
</form>
|
||||
<ul class="forum-nav-browse-menu">
|
||||
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-all">
|
||||
<a href="#" class="forum-nav-browse-title">All Discussions</a>
|
||||
</li>
|
||||
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-following">
|
||||
<a href="#" class="forum-nav-browse-title"><span class="icon fa fa-star" aria-hidden="true"></span>Posts I'm Following</a>
|
||||
</li>
|
||||
<li class="forum-nav-browse-menu-item">
|
||||
<a href="#" class="forum-nav-browse-title">Parent</a>
|
||||
<ul class="forum-nav-browse-submenu">
|
||||
<li class="forum-nav-browse-menu-item">
|
||||
<a href="#" class="forum-nav-browse-title">Target</a>
|
||||
<ul class="forum-nav-browse-submenu">
|
||||
<li
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id="child"
|
||||
data-cohorted="false"
|
||||
>
|
||||
<a href="#" class="forum-nav-browse-title">Child</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id="sibling"
|
||||
data-cohorted="false"
|
||||
>
|
||||
<a href="#" class="forum-nav-browse-title">Sibling</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id="other"
|
||||
data-cohorted="true"
|
||||
>
|
||||
<a href="#" class="forum-nav-browse-title">Other Category</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="forum-nav-thread-list-wrapper" id="sort-filter-wrapper" tabindex="-1">
|
||||
<div class="forum-nav-refine-bar">
|
||||
<label class="forum-nav-filter-main">
|
||||
<select class="forum-nav-filter-main-control">
|
||||
<option value="all">Show all</option>
|
||||
<option value="unread">Unread</option>
|
||||
<option value="unanswered">Unanswered</option>
|
||||
<option value="flagged">Flagged</option>
|
||||
</select>
|
||||
</label>
|
||||
<% if (isCohorted && isPrivilegedUser) { %>
|
||||
<label class="forum-nav-filter-cohort">
|
||||
<span class="sr">Cohort:</span>
|
||||
<select class="forum-nav-filter-cohort-control">
|
||||
<option value="">in all cohorts</option>
|
||||
<option value="1">Cohort1</option>
|
||||
<option value="2">Cohort2</option>
|
||||
</select>
|
||||
</label>
|
||||
<% } %>
|
||||
<label class="forum-nav-sort">
|
||||
<select class="forum-nav-sort-control">
|
||||
<option value="activity">by recent activity</option>
|
||||
<option value="comments">by most activity</option>
|
||||
<option value="votes">by most votes</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-alerts"></div>
|
||||
<ul class="forum-nav-thread-list"></ul>
|
||||
</script>
|
||||
""")
|
||||
@threads = [
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "1",
|
||||
title: "Thread1",
|
||||
votes: {up_count: '20'},
|
||||
pinned: true,
|
||||
comments_count: 1,
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
}),
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "2",
|
||||
title: "Thread2",
|
||||
votes: {up_count: '42'},
|
||||
comments_count: 2,
|
||||
created_at: '2013-04-03T20:07:39Z',
|
||||
}),
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "3",
|
||||
title: "Thread3",
|
||||
votes: {up_count: '12'},
|
||||
comments_count: 3,
|
||||
created_at: '2013-04-03T20:06:39Z',
|
||||
}),
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "4",
|
||||
title: "Thread4",
|
||||
votes: {up_count: '25'},
|
||||
comments_count: 0,
|
||||
pinned: true,
|
||||
created_at: '2013-04-03T20:05:39Z',
|
||||
}),
|
||||
]
|
||||
deferred = $.Deferred()
|
||||
spyOn($, "ajax").and.returnValue(deferred);
|
||||
|
||||
@discussion = new Discussion([])
|
||||
@view = new DiscussionThreadListView(
|
||||
collection: @discussion,
|
||||
el: $("#fixture-element"),
|
||||
courseSettings: new DiscussionCourseSettings({is_cohorted: true})
|
||||
)
|
||||
@view.render()
|
||||
|
||||
setupAjax = (callback) ->
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
if callback
|
||||
callback(params)
|
||||
params.success({discussion_data: [], page: 1, num_pages: 1})
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
renderSingleThreadWithProps = (props) ->
|
||||
makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render()
|
||||
|
||||
makeView = (discussion) ->
|
||||
return new DiscussionThreadListView(
|
||||
el: $("#fixture-element"),
|
||||
collection: discussion,
|
||||
courseSettings: new DiscussionCourseSettings({is_cohorted: true})
|
||||
)
|
||||
|
||||
expectFilter = (filterVal) ->
|
||||
$.ajax.and.callFake((params) ->
|
||||
_.each(["unread", "unanswered", "flagged"], (paramName)->
|
||||
if paramName == filterVal
|
||||
expect(params.data[paramName]).toEqual(true)
|
||||
else
|
||||
expect(params.data[paramName]).toBeUndefined()
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
describe "should filter correctly", ->
|
||||
_.each(["all", "unread", "unanswered", "flagged"], (filterVal) ->
|
||||
it "for #{filterVal}", ->
|
||||
expectFilter(filterVal)
|
||||
@view.$(".forum-nav-filter-main-control").val(filterVal).change()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
)
|
||||
|
||||
describe "cohort selector", ->
|
||||
it "should not be visible to students", ->
|
||||
expect(@view.$(".forum-nav-filter-cohort-control:visible")).not.toExist()
|
||||
|
||||
it "should allow moderators to select visibility", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
@view.render()
|
||||
expectedGroupId = null
|
||||
setupAjax((params) => expect(params.data.group_id).toEqual(expectedGroupId))
|
||||
_.each(
|
||||
[
|
||||
{val: "", expectedGroupId: undefined},
|
||||
{val: "1", expectedGroupId: "1"},
|
||||
{val: "2", expectedGroupId: "2"}
|
||||
],
|
||||
(optionInfo) =>
|
||||
expectedGroupId = optionInfo.expectedGroupId
|
||||
@view.$(".forum-nav-filter-cohort-control").val(optionInfo.val).change()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
$.ajax.calls.reset()
|
||||
)
|
||||
|
||||
it "search should clear filter", ->
|
||||
expectFilter(null)
|
||||
@view.$(".forum-nav-filter-main-control").val("flagged")
|
||||
@view.searchFor("foobar")
|
||||
expect(@view.$(".forum-nav-filter-main-control").val()).toEqual("all")
|
||||
|
||||
checkThreadsOrdering = (view, sort_order, type) ->
|
||||
expect(view.$el.find(".forum-nav-thread").children().length).toEqual(4)
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text()).toEqual(sort_order[0])
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(2) .forum-nav-thread-title").text()).toEqual(sort_order[1])
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(3) .forum-nav-thread-title").text()).toEqual(sort_order[2])
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(4) .forum-nav-thread-title").text()).toEqual(sort_order[3])
|
||||
expect(view.$el.find(".forum-nav-sort-control").val()).toEqual(type)
|
||||
|
||||
describe "thread rendering should be correct", ->
|
||||
checkRender = (threads, type, sort_order) ->
|
||||
discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: type})
|
||||
view = makeView(discussion)
|
||||
view.render()
|
||||
checkThreadsOrdering(view, sort_order, type)
|
||||
expect(view.$el.find(".forum-nav-thread-comments-count:visible").length).toEqual(if type == "votes" then 0 else 4)
|
||||
expect(view.$el.find(".forum-nav-thread-votes-count:visible").length).toEqual(if type == "votes" then 4 else 0)
|
||||
if type == "votes"
|
||||
expect(
|
||||
_.map(
|
||||
view.$el.find(".forum-nav-thread-votes-count"),
|
||||
(element) -> $(element).text().trim()
|
||||
)
|
||||
).toEqual(["+25 votes", "+20 votes", "+42 votes", "+12 votes"])
|
||||
|
||||
it "with sort preference activity", ->
|
||||
checkRender(@threads, "activity", ["Thread1", "Thread2", "Thread3", "Thread4"])
|
||||
|
||||
it "with sort preference votes", ->
|
||||
checkRender(@threads, "votes", ["Thread4", "Thread1", "Thread2", "Thread3"])
|
||||
|
||||
it "with sort preference comments", ->
|
||||
checkRender(@threads, "comments", ["Thread1", "Thread4", "Thread3", "Thread2"])
|
||||
|
||||
describe "Sort change should be correct", ->
|
||||
changeSorting = (threads, selected_type, new_type, sort_order) ->
|
||||
discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: selected_type})
|
||||
view = makeView(discussion)
|
||||
view.render()
|
||||
sortControl = view.$el.find(".forum-nav-sort-control")
|
||||
expect(sortControl.val()).toEqual(selected_type)
|
||||
sorted_threads = []
|
||||
if new_type == 'activity'
|
||||
sorted_threads = [threads[0], threads[3], threads[1], threads[2]]
|
||||
else if new_type == 'comments'
|
||||
sorted_threads = [threads[0], threads[3], threads[2], threads[1]]
|
||||
else if new_type == 'votes'
|
||||
sorted_threads = [threads[3], threads[0], threads[1], threads[2]]
|
||||
$.ajax.and.callFake((params) =>
|
||||
params.success(
|
||||
{"discussion_data":sorted_threads, page:1, num_pages:1}
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
sortControl.val(new_type).change()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
checkThreadsOrdering(view, sort_order, new_type)
|
||||
|
||||
it "with sort preference activity", ->
|
||||
changeSorting(@threads, "comments", "activity", ["Thread1", "Thread4", "Thread3", "Thread2"])
|
||||
|
||||
it "with sort preference votes", ->
|
||||
changeSorting(@threads, "activity", "votes", ["Thread4", "Thread1", "Thread2", "Thread3"])
|
||||
|
||||
it "with sort preference comments", ->
|
||||
changeSorting(@threads, "votes", "comments", ["Thread1", "Thread4", "Thread3", "Thread2"])
|
||||
|
||||
describe "search alerts", ->
|
||||
|
||||
testAlertMessages = (expectedMessages) ->
|
||||
expect($(".search-alert .message").map( ->
|
||||
$(@).html()
|
||||
).get()).toEqual(expectedMessages)
|
||||
|
||||
it "renders and removes search alerts", ->
|
||||
testAlertMessages []
|
||||
foo = @view.addSearchAlert("foo")
|
||||
testAlertMessages ["foo"]
|
||||
bar = @view.addSearchAlert("bar")
|
||||
testAlertMessages ["foo", "bar"]
|
||||
@view.removeSearchAlert(foo.cid)
|
||||
testAlertMessages ["bar"]
|
||||
@view.removeSearchAlert(bar.cid)
|
||||
testAlertMessages []
|
||||
|
||||
it "clears all search alerts", ->
|
||||
@view.addSearchAlert("foo")
|
||||
@view.addSearchAlert("bar")
|
||||
@view.addSearchAlert("baz")
|
||||
testAlertMessages ["foo", "bar", "baz"]
|
||||
@view.clearSearchAlerts()
|
||||
testAlertMessages []
|
||||
|
||||
describe "search spell correction", ->
|
||||
|
||||
beforeEach ->
|
||||
spyOn(@view, "searchForUser")
|
||||
|
||||
testCorrection = (view, correctedText) ->
|
||||
spyOn(view, "addSearchAlert")
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
params.success(
|
||||
{discussion_data: [], page: 42, num_pages: 99, corrected_text: correctedText}, 'success'
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
view.searchFor("dummy")
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
|
||||
it "adds a search alert when an alternate term was searched", ->
|
||||
testCorrection(@view, "foo")
|
||||
expect(@view.addSearchAlert.calls.count()).toEqual(1)
|
||||
expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/foo/)
|
||||
|
||||
it "does not add a search alert when no alternate term was searched", ->
|
||||
testCorrection(@view, null)
|
||||
expect(@view.addSearchAlert.calls.count()).toEqual(1)
|
||||
expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/no threads matched/i)
|
||||
|
||||
it "clears search alerts when a new search is performed", ->
|
||||
spyOn(@view, "clearSearchAlerts")
|
||||
spyOn(DiscussionUtil, "safeAjax")
|
||||
@view.searchFor("dummy")
|
||||
expect(@view.clearSearchAlerts).toHaveBeenCalled()
|
||||
|
||||
it "clears search alerts when the underlying collection changes", ->
|
||||
spyOn(@view, "clearSearchAlerts")
|
||||
spyOn(@view, "renderThread")
|
||||
@view.collection.trigger("change", new Thread({id: 1}))
|
||||
expect(@view.clearSearchAlerts).toHaveBeenCalled()
|
||||
|
||||
describe "Search events", ->
|
||||
|
||||
it "perform search when enter pressed inside search textfield", ->
|
||||
setupAjax()
|
||||
spyOn(@view, "searchFor")
|
||||
@view.$el.find(".forum-nav-search-input").trigger($.Event("keydown", {which: 13}))
|
||||
expect(@view.searchFor).toHaveBeenCalled()
|
||||
|
||||
it "perform search when search icon is clicked", ->
|
||||
setupAjax()
|
||||
spyOn(@view, "searchFor")
|
||||
@view.$el.find(".fa-search").click()
|
||||
expect(@view.searchFor).toHaveBeenCalled()
|
||||
|
||||
describe "username search", ->
|
||||
|
||||
it "makes correct ajax calls", ->
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
expect(params.data.username).toEqual("testing-username")
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("users"))
|
||||
params.success(
|
||||
{users: []}, 'success'
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
@view.searchForUser("testing-username")
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
|
||||
setAjaxResults = (threadSuccess, userResult) ->
|
||||
# threadSuccess is a boolean indicating whether the thread search ajax call should succeed
|
||||
# userResult is the value that should be returned as data from the username search ajax call
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
if params.data.text and threadSuccess
|
||||
params.success(
|
||||
{discussion_data: [], page: 42, num_pages: 99, corrected_text: "dummy"},
|
||||
"success"
|
||||
)
|
||||
else if params.data.username
|
||||
params.success(
|
||||
{users: userResult},
|
||||
"success"
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
it "gets called after a thread search succeeds", ->
|
||||
spyOn(@view, "searchForUser").and.callThrough()
|
||||
setAjaxResults(true, [])
|
||||
@view.searchFor("gizmo")
|
||||
expect(@view.searchForUser).toHaveBeenCalled()
|
||||
expect($.ajax.calls.mostRecent().args[0].data.username).toEqual("gizmo")
|
||||
|
||||
it "does not get called after a thread search fails", ->
|
||||
spyOn(@view, "searchForUser").and.callThrough()
|
||||
setAjaxResults(false, [])
|
||||
@view.searchFor("gizmo")
|
||||
expect(@view.searchForUser).not.toHaveBeenCalled()
|
||||
|
||||
it "adds a search alert when an username was matched", ->
|
||||
spyOn(@view, "addSearchAlert")
|
||||
setAjaxResults(true, [{username: "gizmo", id: "1"}])
|
||||
@view.searchForUser("dummy")
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect(@view.addSearchAlert).toHaveBeenCalled()
|
||||
expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/gizmo/)
|
||||
|
||||
it "does not add a search alert when no username was matched", ->
|
||||
spyOn(@view, "addSearchAlert")
|
||||
setAjaxResults(true, [])
|
||||
@view.searchForUser("dummy")
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect(@view.addSearchAlert).not.toHaveBeenCalled()
|
||||
|
||||
describe "post type renders correctly", ->
|
||||
it "for discussion", ->
|
||||
renderSingleThreadWithProps({thread_type: "discussion"})
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-comments")
|
||||
expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("discussion")
|
||||
|
||||
it "for answered question", ->
|
||||
renderSingleThreadWithProps({thread_type: "question", endorsed: true})
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-check-square-o")
|
||||
expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("answered question")
|
||||
|
||||
it "for unanswered question", ->
|
||||
renderSingleThreadWithProps({thread_type: "question", endorsed: false})
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-question")
|
||||
expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("unanswered question")
|
||||
|
||||
describe "post labels render correctly", ->
|
||||
beforeEach ->
|
||||
@moderatorId = "42"
|
||||
@administratorId = "43"
|
||||
@communityTaId = "44"
|
||||
DiscussionUtil.loadRoles({
|
||||
"Moderator": [parseInt(@moderatorId)],
|
||||
"Administrator": [parseInt(@administratorId)],
|
||||
"Community TA": [parseInt(@communityTaId)],
|
||||
})
|
||||
|
||||
it "for pinned", ->
|
||||
renderSingleThreadWithProps({pinned: true})
|
||||
expect($(".post-label-pinned").length).toEqual(1)
|
||||
|
||||
it "for following", ->
|
||||
renderSingleThreadWithProps({subscribed: true})
|
||||
expect($(".post-label-following").length).toEqual(1)
|
||||
|
||||
it "for moderator", ->
|
||||
renderSingleThreadWithProps({user_id: @moderatorId})
|
||||
expect($(".post-label-by-staff").length).toEqual(1)
|
||||
|
||||
it "for administrator", ->
|
||||
renderSingleThreadWithProps({user_id: @administratorId})
|
||||
expect($(".post-label-by-staff").length).toEqual(1)
|
||||
|
||||
it "for community TA", ->
|
||||
renderSingleThreadWithProps({user_id: @communityTaId})
|
||||
expect($(".post-label-by-community-ta").length).toEqual(1)
|
||||
|
||||
it "when none should be present", ->
|
||||
renderSingleThreadWithProps({})
|
||||
expect($(".forum-nav-thread-labels").length).toEqual(0)
|
||||
|
||||
describe "browse menu", ->
|
||||
afterEach ->
|
||||
# Remove handler added to make browse menu disappear
|
||||
$("body").unbind("click")
|
||||
|
||||
expectBrowseMenuVisible = (isVisible) ->
|
||||
expect($(".forum-nav-browse-menu:visible").length).toEqual(if isVisible then 1 else 0)
|
||||
expect($(".forum-nav-thread-list-wrapper:visible").length).toEqual(if isVisible then 0 else 1)
|
||||
|
||||
it "should not be visible by default", ->
|
||||
expectBrowseMenuVisible(false)
|
||||
|
||||
it "should show when header button is clicked", ->
|
||||
$(".forum-nav-browse").click()
|
||||
expectBrowseMenuVisible(true)
|
||||
|
||||
describe "when shown", ->
|
||||
beforeEach ->
|
||||
$(".forum-nav-browse").click()
|
||||
|
||||
it "should hide when header button is clicked", ->
|
||||
$(".forum-nav-browse").click()
|
||||
expectBrowseMenuVisible(false)
|
||||
|
||||
it "should hide when a click outside the menu occurs", ->
|
||||
$(".forum-nav-search-input").click()
|
||||
expectBrowseMenuVisible(false)
|
||||
|
||||
it "should hide when a search is executed", ->
|
||||
setupAjax()
|
||||
$(".forum-nav-search-input").trigger($.Event("keydown", {which: 13}))
|
||||
expectBrowseMenuVisible(false)
|
||||
|
||||
it "should hide when a category is clicked", ->
|
||||
$(".forum-nav-browse-title")[0].click()
|
||||
expectBrowseMenuVisible(false)
|
||||
|
||||
it "should still be shown when filter input is clicked", ->
|
||||
$(".forum-nav-browse-filter-input").click()
|
||||
expectBrowseMenuVisible(true)
|
||||
|
||||
describe "filtering", ->
|
||||
checkFilter = (filterText, expectedItems) ->
|
||||
$(".forum-nav-browse-filter-input").val(filterText).keyup()
|
||||
visibleItems = $(".forum-nav-browse-title:visible").map(
|
||||
(i, elem) -> $(elem).text()
|
||||
).get()
|
||||
expect(visibleItems).toEqual(expectedItems)
|
||||
|
||||
it "should be case-insensitive", ->
|
||||
checkFilter("other", ["Other Category"])
|
||||
|
||||
it "should match partial words", ->
|
||||
checkFilter("ateg", ["Other Category"])
|
||||
|
||||
it "should show ancestors and descendants of matches", ->
|
||||
checkFilter("Target", ["Parent", "Target", "Child"])
|
||||
|
||||
it "should handle multiple words regardless of order", ->
|
||||
checkFilter("Following Posts", ["Posts I'm Following"])
|
||||
|
||||
it "should handle multiple words in different depths", ->
|
||||
checkFilter("Parent Child", ["Parent", "Target", "Child"])
|
||||
|
||||
describe "selecting an item", ->
|
||||
it "should clear the search box", ->
|
||||
setupAjax()
|
||||
$(".forum-nav-search-input").val("foobar")
|
||||
$(".forum-nav-browse-menu-following .forum-nav-browse-title").click()
|
||||
expect($(".forum-nav-search-input").val()).toEqual("")
|
||||
|
||||
it "should change the button text", ->
|
||||
setupAjax()
|
||||
$(".forum-nav-browse-menu-following .forum-nav-browse-title").click()
|
||||
expect($(".forum-nav-browse-current").text()).toEqual("Posts I'm Following")
|
||||
|
||||
it "should show/hide the cohort selector", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
@view.render()
|
||||
setupAjax()
|
||||
_.each(
|
||||
[
|
||||
{selector: ".forum-nav-browse-menu-all", cohortVisibility: true},
|
||||
{selector: ".forum-nav-browse-menu-following", cohortVisibility: false},
|
||||
{
|
||||
selector: ".forum-nav-browse-menu-item:has(.forum-nav-browse-menu-item .forum-nav-browse-menu-item)",
|
||||
cohortVisibility: false
|
||||
},
|
||||
{selector: "[data-discussion-id=child]", cohortVisibility: false},
|
||||
{selector: "[data-discussion-id=other]", cohortVisibility: true}
|
||||
],
|
||||
(itemInfo) =>
|
||||
@view.$("#{itemInfo.selector} > .forum-nav-browse-title").click()
|
||||
expect(@view.$(".forum-nav-filter-cohort").is(":visible")).toEqual(itemInfo.cohortVisibility)
|
||||
)
|
||||
|
||||
testSelectionRequest = (callback, itemText) ->
|
||||
setupAjax(callback)
|
||||
$(".forum-nav-browse-title:contains(#{itemText})").click()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
|
||||
it "should get all discussions", ->
|
||||
testSelectionRequest(
|
||||
(params) -> expect(params.url.path()).toEqual(DiscussionUtil.urlFor("threads")),
|
||||
"All"
|
||||
)
|
||||
|
||||
it "should get followed threads", ->
|
||||
testSelectionRequest(
|
||||
(params) ->
|
||||
expect(params.url.path()).toEqual(
|
||||
DiscussionUtil.urlFor("followed_threads", window.user.id)
|
||||
)
|
||||
,
|
||||
"Following"
|
||||
)
|
||||
expect($.ajax.calls.mostRecent().args[0].data.group_id).toBeUndefined();
|
||||
|
||||
it "should get threads for the selected leaf", ->
|
||||
testSelectionRequest(
|
||||
(params) ->
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
|
||||
expect(params.data.commentable_ids).toEqual("child")
|
||||
,
|
||||
"Child"
|
||||
)
|
||||
|
||||
it "should get threads for children of the selected intermediate node", ->
|
||||
testSelectionRequest(
|
||||
(params) ->
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
|
||||
expect(params.data.commentable_ids).toEqual("child,sibling")
|
||||
,
|
||||
"Parent"
|
||||
)
|
||||
@@ -1,110 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
describe "DiscussionThreadProfileView", ->
|
||||
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
@threadData = {
|
||||
id: "1",
|
||||
body: "dummy body",
|
||||
discussion: new Discussion()
|
||||
abuse_flaggers: [],
|
||||
commentable_id: 'dummy_discussion',
|
||||
votes: {up_count: "42"},
|
||||
created_at: "2014-09-09T20:11:08Z"
|
||||
}
|
||||
@imageTag = '<img src="https://www.google.com.pk/images/srpr/logo11w.png">'
|
||||
window.MathJax = { Hub: { Queue: -> } }
|
||||
|
||||
makeView = (thread) ->
|
||||
view = new DiscussionThreadProfileView(model: thread)
|
||||
spyConvertMath(view)
|
||||
return view
|
||||
|
||||
makeThread = (threadData) ->
|
||||
thread = new Thread(threadData)
|
||||
thread.discussion = new Discussion()
|
||||
return thread
|
||||
|
||||
spyConvertMath = (view) ->
|
||||
spyOn(view, "convertMath").and.callFake( ->
|
||||
@model.set('markdownBody', @model.get('body'))
|
||||
)
|
||||
|
||||
checkPostWithImages = (numberOfImages, truncatedText, threadData, imageTag) ->
|
||||
expectedHtml = '<p>'
|
||||
threadData.body = '<p>'
|
||||
testText = ''
|
||||
expectedText = ''
|
||||
|
||||
if truncatedText
|
||||
testText = new Array(100).join('test ')
|
||||
expectedText = testText.substring(0, 139)+ '…'
|
||||
else
|
||||
testText = 'Test body'
|
||||
expectedText = 'Test body'
|
||||
|
||||
for i in [0..numberOfImages-1]
|
||||
threadData.body = threadData.body + imageTag
|
||||
if i == 0
|
||||
expectedHtml = expectedHtml + imageTag
|
||||
else
|
||||
expectedHtml = expectedHtml + '<em>image omitted</em>'
|
||||
|
||||
threadData.body = threadData.body + '<em>' + testText + '</em></p>'
|
||||
if numberOfImages > 1
|
||||
expectedHtml = expectedHtml + '<em>' + expectedText + '</em></p><p><em>Some images in this post have been omitted</em></p>'
|
||||
else
|
||||
expectedHtml = expectedHtml + '<em>' + expectedText + '</em></p>'
|
||||
|
||||
view = makeView(makeThread(threadData))
|
||||
view.render()
|
||||
expect(view.$el.find(".post-body").html()).toEqual(expectedHtml)
|
||||
|
||||
checkBody = (truncated, view, threadData) ->
|
||||
view.render()
|
||||
if not truncated
|
||||
expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody"))
|
||||
expect(view.$el.find(".post-body").html()).toEqual(threadData.body)
|
||||
else
|
||||
expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody"))
|
||||
expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body)
|
||||
outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig,"");
|
||||
outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted","")
|
||||
outputHtmlStripped = outputHtmlStripped.replace("image omitted","")
|
||||
inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig,"");
|
||||
expectedOutput = inputHtmlStripped.substring(0, 139)+ '…'
|
||||
expect(outputHtmlStripped).toEqual(expectedOutput)
|
||||
expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0)
|
||||
|
||||
describe "Body markdown should be correct", ->
|
||||
|
||||
it "untruncated text without markdown body", ->
|
||||
@threadData.body = "Test body"
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(false, view, @threadData)
|
||||
|
||||
it "truncated text without markdown body", ->
|
||||
@threadData.body = new Array(100).join("test ")
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(true, view, @threadData)
|
||||
|
||||
it "untruncated text with markdown body", ->
|
||||
@threadData.body = '<p>' + @imageTag + '<em>Google top search engine</em></p>'
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(false, view, @threadData)
|
||||
|
||||
it "truncated text with markdown body", ->
|
||||
testText = new Array(100).join("test ")
|
||||
@threadData.body = '<p>' + @imageTag + @imageTag + '<em>' + testText + '</em></p>'
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(true, view, @threadData)
|
||||
|
||||
for numImages in [1, 2, 10]
|
||||
for truncatedText in [true, false]
|
||||
it "body with #{numImages} images and #{if truncatedText then "truncated" else "untruncated"} text", ->
|
||||
checkPostWithImages(numImages, truncatedText, @threadData, @imageTag)
|
||||
|
||||
it "check the thread retrieve url", ->
|
||||
thread = makeThread(@threadData)
|
||||
expect(thread.urlFor('retrieve')).toBe('/courses/edX/999/test/discussion/forum/dummy_discussion/threads/1')
|
||||
@@ -1,159 +0,0 @@
|
||||
describe "DiscussionThreadShowView", ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
@user = DiscussionUtil.getUser()
|
||||
@threadData = {
|
||||
id: "dummy",
|
||||
user_id: @user.id,
|
||||
username: @user.get('username'),
|
||||
course_id: $$course_id,
|
||||
title: "dummy title",
|
||||
body: "this is a thread",
|
||||
created_at: "2013-04-03T20:08:39Z",
|
||||
abuse_flaggers: [],
|
||||
votes: {up_count: 42},
|
||||
thread_type: "discussion",
|
||||
closed: false,
|
||||
pinned: false,
|
||||
type: "thread" # TODO - silly that this needs to be explicitly set
|
||||
}
|
||||
@thread = new Thread(@threadData)
|
||||
@view = new DiscussionThreadShowView({ model: @thread })
|
||||
@view.setElement($("#fixture-element"))
|
||||
spyOn(@view, "convertMath")
|
||||
|
||||
describe "voting", ->
|
||||
|
||||
it "renders the vote state correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
|
||||
|
||||
it "votes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("click"))
|
||||
|
||||
it "votes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
it "unvotes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("click"))
|
||||
|
||||
it "unvotes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
describe "pinning", ->
|
||||
|
||||
expectPinnedRendered = (view, model) ->
|
||||
pinned = model.get('pinned')
|
||||
button = view.$el.find(".action-pin")
|
||||
expect(button.hasClass("is-checked")).toBe(pinned)
|
||||
expect(button.attr("aria-checked")).toEqual(pinned.toString())
|
||||
|
||||
it "renders the pinned state correctly", ->
|
||||
@view.render()
|
||||
expectPinnedRendered(@view, @thread)
|
||||
@thread.set('pinned', false)
|
||||
@view.render()
|
||||
expectPinnedRendered(@view, @thread)
|
||||
@thread.set('pinned', true)
|
||||
@view.render()
|
||||
expectPinnedRendered(@view, @thread)
|
||||
|
||||
it "exposes the pinning control only to authorized users", ->
|
||||
@thread.updateInfo({ability: {can_openclose: false}})
|
||||
@view.render()
|
||||
expect(@view.$el.find(".action-pin").closest(".is-hidden")).toExist()
|
||||
@thread.updateInfo({ability: {can_openclose: true}})
|
||||
@view.render()
|
||||
expect(@view.$el.find(".action-pin").closest(".is-hidden")).not.toExist()
|
||||
|
||||
it "handles events correctly", ->
|
||||
@view.render()
|
||||
DiscussionViewSpecHelper.checkButtonEvents(@view, "togglePin", ".action-pin")
|
||||
|
||||
describe "labels", ->
|
||||
|
||||
expectOneElement = (view, selector, visible=true) =>
|
||||
view.render()
|
||||
elements = view.$el.find(selector)
|
||||
expect(elements.length).toEqual(1)
|
||||
if visible
|
||||
expect(elements).not.toHaveClass("is-hidden")
|
||||
else
|
||||
expect(elements).toHaveClass("is-hidden")
|
||||
|
||||
it 'displays the closed label when appropriate', ->
|
||||
expectOneElement(@view, '.post-label-closed', false)
|
||||
@thread.set('closed', true)
|
||||
expectOneElement(@view, '.post-label-closed')
|
||||
|
||||
it 'displays the pinned label when appropriate', ->
|
||||
expectOneElement(@view, '.post-label-pinned', false)
|
||||
@thread.set('pinned', true)
|
||||
expectOneElement(@view, '.post-label-pinned')
|
||||
|
||||
it 'displays the reported label when appropriate for a non-staff user', ->
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
# flagged by current user - should be labelled
|
||||
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
# flagged by some other user but not the current one - should not be labelled
|
||||
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
|
||||
it 'displays the reported label when appropriate for a flag moderator', ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
# flagged by current user - should be labelled
|
||||
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
# flagged by some other user but not the current one - should still be labelled
|
||||
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
|
||||
describe "author display", ->
|
||||
|
||||
beforeEach ->
|
||||
@thread.set('user_url', 'test_user_url')
|
||||
|
||||
checkUserLink = (element, is_ta, is_staff) ->
|
||||
expect(element.find('a.username').length).toEqual(1)
|
||||
expect(element.find('a.username').text()).toEqual('test_user')
|
||||
expect(element.find('a.username').attr('href')).toEqual('test_user_url')
|
||||
expect(element.find('.user-label-community-ta').length).toEqual(if is_ta then 1 else 0)
|
||||
expect(element.find('.user-label-staff').length).toEqual(if is_staff then 1 else 0)
|
||||
|
||||
it "renders correctly for a student-authored thread", ->
|
||||
$el = $('#fixture-element').html(@view.getAuthorDisplay())
|
||||
checkUserLink($el, false, false)
|
||||
|
||||
it "renders correctly for a community TA-authored thread", ->
|
||||
@thread.set('community_ta_authored', true)
|
||||
$el = $('#fixture-element').html(@view.getAuthorDisplay())
|
||||
checkUserLink($el, true, false)
|
||||
|
||||
it "renders correctly for a staff-authored thread", ->
|
||||
@thread.set('staff_authored', true)
|
||||
$el = $('#fixture-element').html(@view.getAuthorDisplay())
|
||||
checkUserLink($el, false, true)
|
||||
|
||||
it "renders correctly for an anonymously-authored thread", ->
|
||||
@thread.set('username', null)
|
||||
$el = $('#fixture-element').html(@view.getAuthorDisplay())
|
||||
expect($el.find('a.username').length).toEqual(0)
|
||||
expect($el.text()).toMatch(/^(\s*)anonymous(\s*)$/)
|
||||
|
||||
describe "cohorting", ->
|
||||
it "renders correctly for an uncohorted thread", ->
|
||||
@view.render()
|
||||
expect(@view.$('.group-visibility-label').text().trim()).toEqual(
|
||||
'This post is visible to everyone.'
|
||||
)
|
||||
|
||||
it "renders correctly for a cohorted thread", ->
|
||||
@thread.set('group_id', '1')
|
||||
@thread.set('group_name', 'Mock Cohort')
|
||||
@view.render()
|
||||
expect(@view.$('.group-visibility-label').text().trim()).toEqual(
|
||||
'This post is visible only to Mock Cohort.'
|
||||
)
|
||||
@@ -1,410 +0,0 @@
|
||||
describe "DiscussionThreadView", ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
jasmine.clock().install()
|
||||
@threadData = DiscussionViewSpecHelper.makeThreadWithProps({})
|
||||
@thread = new Thread(@threadData)
|
||||
@discussion = new Discussion(@thread)
|
||||
deferred = $.Deferred();
|
||||
spyOn($, "ajax").and.returnValue(deferred);
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(DiscussionThreadShowView.prototype, "convertMath")
|
||||
spyOn(DiscussionContentView.prototype, "makeWmdEditor")
|
||||
spyOn(DiscussionUtil, "makeWmdEditor")
|
||||
spyOn(DiscussionUtil, "setWmdContent")
|
||||
spyOn(ThreadResponseShowView.prototype, "convertMath")
|
||||
|
||||
afterEach ->
|
||||
$.ajax.calls.reset()
|
||||
jasmine.clock().uninstall()
|
||||
|
||||
renderWithContent = (view, content) ->
|
||||
$.ajax.and.callFake((params) =>
|
||||
params.success(
|
||||
createAjaxResponseJson(content, false),
|
||||
'success'
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
view.render()
|
||||
jasmine.clock().tick(100)
|
||||
|
||||
renderWithTestResponses = (view, count, options) ->
|
||||
renderWithContent(
|
||||
view,
|
||||
_.extend(
|
||||
{
|
||||
resp_total: count,
|
||||
children: if count > 0 then (createTestResponseJson(index) for index in [1..count]) else []
|
||||
},
|
||||
options
|
||||
)
|
||||
)
|
||||
|
||||
createTestResponseJson = (index) ->
|
||||
{
|
||||
user_id: window.user.id,
|
||||
body: "Response " + index,
|
||||
id: "id_" + index,
|
||||
created_at: "2015-01-01T22:20:28Z"
|
||||
}
|
||||
|
||||
assertContentVisible = (view, selector, visible) ->
|
||||
content = view.$el.find(selector)
|
||||
expect(content.length).toBeGreaterThan(0)
|
||||
content.each (i, elem) ->
|
||||
expect($(elem).is(":visible")).toEqual(visible)
|
||||
|
||||
assertExpandedContentVisible = (view, expanded) ->
|
||||
expect(view.$el.hasClass("expanded")).toEqual(expanded)
|
||||
assertContentVisible(view, ".post-extended-content", expanded)
|
||||
assertContentVisible(view, ".forum-thread-expand", not expanded)
|
||||
assertContentVisible(view, ".forum-thread-collapse", expanded)
|
||||
|
||||
assertResponseCountAndPaginationCorrect = (view, countText, displayCountText, buttonText) ->
|
||||
expect(view.$el.find(".response-count").text()).toEqual(countText)
|
||||
if displayCountText
|
||||
expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText)
|
||||
else
|
||||
expect(view.$el.find(".response-display-count").length).toEqual(0)
|
||||
if buttonText
|
||||
expect(view.$el.find(".load-response-button").text()).toEqual(buttonText)
|
||||
else
|
||||
expect(view.$el.find(".load-response-button").length).toEqual(0)
|
||||
|
||||
createAjaxResponseJson = (content, can_act) ->
|
||||
{
|
||||
content: content,
|
||||
annotated_content_info: {
|
||||
ability: {
|
||||
editable: can_act,
|
||||
can_delete: can_act,
|
||||
can_reply: can_act,
|
||||
can_vote: can_act
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
postResponse = (view, index) ->
|
||||
testResponseJson = createTestResponseJson(index)
|
||||
responseText = testResponseJson.body
|
||||
spyOn(view, "getWmdContent").and.returnValue(responseText)
|
||||
$.ajax.and.callFake((params) =>
|
||||
expect(params.type).toEqual("POST")
|
||||
expect(params.data.body).toEqual(responseText)
|
||||
params.success(
|
||||
createAjaxResponseJson(testResponseJson, true),
|
||||
'success'
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
view.$(".discussion-submit-post").click()
|
||||
|
||||
describe "closed and open Threads", ->
|
||||
|
||||
createDiscussionThreadView = (originallyClosed, mode) ->
|
||||
threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed})
|
||||
thread = new Thread(threadData)
|
||||
discussion = new Discussion(thread)
|
||||
view = new DiscussionThreadView(
|
||||
model: thread
|
||||
el: $("#fixture-element")
|
||||
mode: mode
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
)
|
||||
renderWithTestResponses(view, 1)
|
||||
if mode == "inline"
|
||||
view.expand()
|
||||
spyOn(DiscussionUtil, "updateWithUndo").and.callFake(
|
||||
(model, updates, safeAjaxParams, errorMsg) ->
|
||||
model.set(updates)
|
||||
)
|
||||
view
|
||||
|
||||
checkCommentForm = (originallyClosed, mode) ->
|
||||
view = createDiscussionThreadView(originallyClosed, mode)
|
||||
expect(view.$('.comment-form').closest('li').is(":visible")).toBe(not originallyClosed)
|
||||
expect(view.$(".discussion-reply-new").is(":visible")).toBe(not originallyClosed)
|
||||
view.$(".action-close").click()
|
||||
expect(view.$('.comment-form').closest('li').is(":visible")).toBe(originallyClosed)
|
||||
expect(view.$(".discussion-reply-new").is(":visible")).toBe(originallyClosed)
|
||||
|
||||
checkVoteDisplay = (originallyClosed, mode) ->
|
||||
view = createDiscussionThreadView(originallyClosed, mode)
|
||||
expect(view.$('.thread-main-wrapper .action-vote').is(":visible")).toBe(not originallyClosed)
|
||||
expect(view.$('.thread-main-wrapper .display-vote').is(":visible")).toBe(originallyClosed)
|
||||
view.$(".action-close").click()
|
||||
expect(view.$('.action-vote').is(":visible")).toBe(originallyClosed)
|
||||
expect(view.$('.display-vote').is(":visible")).toBe(not originallyClosed)
|
||||
|
||||
_.each(["tab", "inline"], (mode) =>
|
||||
it "Test that in #{mode} mode when a closed thread is opened the comment form is displayed", ->
|
||||
checkCommentForm(true, mode)
|
||||
|
||||
it "Test that in #{mode} mode when a open thread is closed the comment form is hidden", ->
|
||||
checkCommentForm(false, mode)
|
||||
|
||||
it "Test that in #{mode} mode when a closed thread is opened the vote button is displayed and vote count is hidden", ->
|
||||
checkVoteDisplay(true, mode)
|
||||
|
||||
it "Test that in #{mode} mode when a open thread is closed the vote button is hidden and vote count is displayed", ->
|
||||
checkVoteDisplay(false, mode)
|
||||
)
|
||||
|
||||
describe "tab mode", ->
|
||||
beforeEach ->
|
||||
@view = new DiscussionThreadView(
|
||||
model: @thread
|
||||
el: $("#fixture-element")
|
||||
mode: "tab"
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
)
|
||||
|
||||
describe "responses", ->
|
||||
it "can post a first response", ->
|
||||
# Initially render a test post (made by someone else) with zero responses
|
||||
renderWithTestResponses(@view, 0)
|
||||
postResponse(@view, 1)
|
||||
expect(@view.$(".forum-response").length).toBe(1)
|
||||
# At this point, there are 2 DiscussionContentViews, the main post and the response.
|
||||
# Each an .action-edit button, but only 1 (the response) should be available.
|
||||
expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1)
|
||||
expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(1)
|
||||
|
||||
it "can post a second response", ->
|
||||
# Initially render a test post (made by someone else) with a single response (made by the current learner)
|
||||
renderWithTestResponses(@view, 1)
|
||||
expect(@view.$(".forum-response").length).toBe(1)
|
||||
# Post should not be editable, response should be
|
||||
expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1)
|
||||
expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(1)
|
||||
|
||||
# Now make a second response. Prior to TNL-3788, a bug would hide the edit button for the first response
|
||||
postResponse(@view, 2)
|
||||
expect(@view.$(".forum-response").length).toBe(2)
|
||||
# Post should not be editable, responses should be
|
||||
expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1)
|
||||
expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(2)
|
||||
|
||||
describe "response count and pagination", ->
|
||||
it "correctly render for a thread with no responses", ->
|
||||
renderWithTestResponses(@view, 0)
|
||||
assertResponseCountAndPaginationCorrect(@view, "0 responses", null, null)
|
||||
|
||||
it "correctly render for a thread with one response", ->
|
||||
renderWithTestResponses(@view, 1)
|
||||
assertResponseCountAndPaginationCorrect(@view, "1 response", "Showing all responses", null)
|
||||
|
||||
it "correctly render for a thread with one additional page", ->
|
||||
renderWithTestResponses(@view, 1, {resp_total: 2})
|
||||
assertResponseCountAndPaginationCorrect(@view, "2 responses", "Showing first response", "Load all responses")
|
||||
|
||||
it "correctly render for a thread with multiple additional pages", ->
|
||||
renderWithTestResponses(@view, 2, {resp_total: 111})
|
||||
assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses")
|
||||
|
||||
describe "on clicking the load more button", ->
|
||||
beforeEach ->
|
||||
renderWithTestResponses(@view, 1, {resp_total: 5})
|
||||
assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing first response", "Load all responses")
|
||||
|
||||
it "correctly re-render when all threads have loaded", ->
|
||||
renderWithTestResponses(@view, 5, {resp_total: 5})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing all responses", null)
|
||||
|
||||
it "correctly re-render when one page remains", ->
|
||||
renderWithTestResponses(@view, 3, {resp_total: 42})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertResponseCountAndPaginationCorrect(@view, "42 responses", "Showing first 3 responses", "Load all responses")
|
||||
|
||||
it "correctly re-render when multiple pages remain", ->
|
||||
renderWithTestResponses(@view, 3, {resp_total: 111})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 3 responses", "Load next 100 responses")
|
||||
|
||||
describe "inline mode", ->
|
||||
beforeEach ->
|
||||
@view = new DiscussionThreadView(
|
||||
model: @thread
|
||||
el: $("#fixture-element")
|
||||
mode: "inline"
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
)
|
||||
|
||||
describe "render", ->
|
||||
it "shows content that should be visible when collapsed", ->
|
||||
@view.render()
|
||||
assertExpandedContentVisible(@view, false)
|
||||
|
||||
it "does not render any responses by default", ->
|
||||
@view.render()
|
||||
expect($.ajax).not.toHaveBeenCalled()
|
||||
expect(@view.$el.find(".responses li").length).toEqual(0)
|
||||
|
||||
describe "focus", ->
|
||||
it "sends focus to the conversation when opened", (done) ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
@view.render()
|
||||
@view.expand()
|
||||
self = @
|
||||
jasmine.waitUntil(->
|
||||
# This is the implementation of "toBeFocused". However, simply calling that method
|
||||
# with no wait seems to be flaky.
|
||||
article = self.view.$el.find('.discussion-article')
|
||||
return article[0] == article[0].ownerDocument.activeElement
|
||||
).then ->
|
||||
done()
|
||||
|
||||
describe "expand/collapse", ->
|
||||
it "shows/hides appropriate content", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
@view.render()
|
||||
@view.expand()
|
||||
assertExpandedContentVisible(@view, true)
|
||||
@view.collapse()
|
||||
assertExpandedContentVisible(@view, false)
|
||||
|
||||
it "switches between the abbreviated and full body", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
longBody = new Array(100).join("test ")
|
||||
expectedAbbreviation = DiscussionUtil.abbreviateString(longBody, 140)
|
||||
@thread.set("body", longBody)
|
||||
|
||||
@view.render()
|
||||
expect($(".post-body").text()).toEqual(expectedAbbreviation)
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
|
||||
DiscussionThreadShowView.prototype.convertMath.calls.reset()
|
||||
|
||||
@view.expand()
|
||||
expect($(".post-body").text()).toEqual(longBody)
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
|
||||
DiscussionThreadShowView.prototype.convertMath.calls.reset()
|
||||
|
||||
@view.collapse()
|
||||
expect($(".post-body").text()).toEqual(expectedAbbreviation)
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
|
||||
|
||||
it "strips script tags appropriately", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
longMaliciousBody = new Array(100).join("<script>alert('Until they think warm days will never cease');</script>\n")
|
||||
@thread.set("body", longMaliciousBody)
|
||||
maliciousAbbreviation = DiscussionUtil.abbreviateString(@thread.get('body'), 140)
|
||||
|
||||
# The nodes' html should be different than the strings, but
|
||||
# their texts should be the same, indicating that they've been
|
||||
# properly escaped. To be safe, make sure the string "<script"
|
||||
# isn't present, either
|
||||
|
||||
@view.render()
|
||||
expect($(".post-body").html()).not.toEqual(maliciousAbbreviation)
|
||||
expect($(".post-body").text()).toEqual(maliciousAbbreviation)
|
||||
expect($(".post-body").html()).not.toContain("<script")
|
||||
|
||||
@view.expand()
|
||||
expect($(".post-body").html()).not.toEqual(longMaliciousBody)
|
||||
expect($(".post-body").text()).toEqual(longMaliciousBody)
|
||||
expect($(".post-body").html()).not.toContain("<script")
|
||||
|
||||
@view.collapse()
|
||||
expect($(".post-body").html()).not.toEqual(maliciousAbbreviation)
|
||||
expect($(".post-body").text()).toEqual(maliciousAbbreviation)
|
||||
expect($(".post-body").html()).not.toContain("<script")
|
||||
|
||||
it "re-renders the show view correctly when leaving the edit view", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
@view.render()
|
||||
@view.expand()
|
||||
assertExpandedContentVisible(@view, true)
|
||||
@view.edit()
|
||||
assertContentVisible(@view, ".edit-post-body", true)
|
||||
expect(@view.$el.find(".post-actions-list").length).toBe(0)
|
||||
@view.closeEditView(DiscussionSpecHelper.makeEventSpy())
|
||||
expect(@view.$el.find(".edit-post-body").length).toBe(0)
|
||||
assertContentVisible(@view, ".post-actions-list", true)
|
||||
|
||||
describe "for question threads", ->
|
||||
beforeEach ->
|
||||
@thread.set("thread_type", "question")
|
||||
@view = new DiscussionThreadView(
|
||||
model: @thread
|
||||
el: $("#fixture-element")
|
||||
mode: "tab"
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
)
|
||||
|
||||
generateContent = (idStart, idEnd) ->
|
||||
_.map(_.range(idStart, idEnd), (i) -> createTestResponseJson(i))
|
||||
|
||||
renderTestCase = (view, numEndorsed, numNonEndorsed) ->
|
||||
renderWithContent(
|
||||
view,
|
||||
{
|
||||
endorsed_responses: generateContent(0, numEndorsed),
|
||||
non_endorsed_responses: generateContent(numEndorsed, numEndorsed + numNonEndorsed),
|
||||
non_endorsed_resp_total: numNonEndorsed
|
||||
}
|
||||
)
|
||||
expect(view.$(".js-marked-answer-list .discussion-response").length).toEqual(numEndorsed)
|
||||
expect(view.$(".js-response-list .discussion-response").length).toEqual(numNonEndorsed)
|
||||
assertResponseCountAndPaginationCorrect(
|
||||
view,
|
||||
"#{numNonEndorsed} #{if numEndorsed then "other " else ""}#{if numNonEndorsed == 1 then "response" else "responses"}",
|
||||
if numNonEndorsed then "Showing all responses" else null,
|
||||
null
|
||||
)
|
||||
|
||||
_.each({"no": 0, "one": 1, "many": 5}, (numEndorsed, endorsedDesc) ->
|
||||
_.each({"no": 0, "one": 1, "many": 5}, (numNonEndorsed, nonEndorsedDesc) ->
|
||||
it "renders correctly with #{endorsedDesc} marked answer(s) and #{nonEndorsedDesc} response(s)", ->
|
||||
renderTestCase(@view, numEndorsed, numNonEndorsed)
|
||||
)
|
||||
)
|
||||
|
||||
it "handles pagination correctly", ->
|
||||
renderWithContent(
|
||||
@view,
|
||||
{
|
||||
endorsed_responses: generateContent(0, 2),
|
||||
non_endorsed_responses: generateContent(3, 6),
|
||||
non_endorsed_resp_total: 42
|
||||
}
|
||||
)
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
# Add an endorsed response; it should be rendered
|
||||
endorsed_responses: generateContent(0, 3),
|
||||
non_endorsed_responses: generateContent(6, 9),
|
||||
non_endorsed_resp_total: 41
|
||||
})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
expect($(".js-marked-answer-list .discussion-response").length).toEqual(3)
|
||||
expect($(".js-response-list .discussion-response").length).toEqual(6)
|
||||
assertResponseCountAndPaginationCorrect(
|
||||
@view,
|
||||
"41 other responses",
|
||||
"Showing first 6 responses",
|
||||
"Load all responses"
|
||||
)
|
||||
|
||||
describe "post restrictions", ->
|
||||
beforeEach ->
|
||||
@thread.attributes.ability = _.extend(@thread.attributes.ability, {
|
||||
can_report: false
|
||||
can_vote: false
|
||||
})
|
||||
@view = new DiscussionThreadView(
|
||||
model: @thread
|
||||
el: $("#fixture-element")
|
||||
mode: "tab"
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
)
|
||||
|
||||
it "doesn't show report option if can_report ability is disabled", ->
|
||||
@view.render()
|
||||
expect(@view.$el.find(".action-report").closest(".actions-item")).toHaveClass('is-hidden')
|
||||
|
||||
it "doesn't show voting button if can_vote ability is disabled", ->
|
||||
@view.render()
|
||||
expect(@view.$el.find(".action-vote").closest(".actions-item")).toHaveClass('is-hidden')
|
||||
@@ -1,221 +0,0 @@
|
||||
describe "DiscussionUserProfileView", ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
spyOn(DiscussionThreadProfileView.prototype, "render")
|
||||
|
||||
makeThreads = (numThreads) ->
|
||||
_.map(_.range(numThreads), (i) -> {id: i.toString(), body: "dummy body"})
|
||||
|
||||
makeView = (threads, page, numPages) ->
|
||||
new DiscussionUserProfileView(
|
||||
collection: threads
|
||||
page: page
|
||||
numPages: numPages
|
||||
)
|
||||
|
||||
describe "thread rendering should be correct", ->
|
||||
checkRender = (numThreads) ->
|
||||
threads = makeThreads(numThreads)
|
||||
view = makeView(threads, 1, 1)
|
||||
expect(view.$(".discussion").children().length).toEqual(numThreads)
|
||||
_.each(threads, (thread) -> expect(view.$("#thread_#{thread.id}").length).toEqual(1))
|
||||
|
||||
it "with no threads", ->
|
||||
checkRender(0)
|
||||
|
||||
it "with one thread", ->
|
||||
checkRender(1)
|
||||
|
||||
it "with several threads", ->
|
||||
checkRender(5)
|
||||
|
||||
describe "pagination rendering should be correct", ->
|
||||
baseUri = URI(window.location)
|
||||
|
||||
pageInfo = (page) -> {url: baseUri.clone().addSearch("page", page).toString(), number: page}
|
||||
|
||||
checkRender = (params) ->
|
||||
view = makeView([], params.page, params.numPages)
|
||||
paginator = view.$(".discussion-paginator")
|
||||
expect(paginator.find(".current-page").text()).toEqual(params["page"].toString())
|
||||
expect(paginator.find(".first-page").length).toBe(if params["first"] then 1 else 0);
|
||||
expect(paginator.find(".previous-page").length).toBe(if params["previous"] then 1 else 0);
|
||||
expect(paginator.find(".previous-ellipses").length).toBe(if params["leftdots"] then 1 else 0);
|
||||
expect(paginator.find(".next-page").length).toBe(if params["next"] then 1 else 0);
|
||||
expect(paginator.find(".next-ellipses").length).toBe(if params["rightdots"] then 1 else 0);
|
||||
expect(paginator.find(".last-page").length).toBe(if params["last"] then 1 else 0);
|
||||
|
||||
get_page_number = (element) => parseInt($(element).text())
|
||||
expect(_.map(paginator.find(".lower-page a"), get_page_number)).toEqual(params["lowPages"])
|
||||
expect(_.map(paginator.find(".higher-page a"), get_page_number)).toEqual(params["highPages"])
|
||||
|
||||
it "for one page", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 1
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for first page of three (max with no last)", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 3
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: [2, 3]
|
||||
rightdots: false
|
||||
last: null
|
||||
next: 2
|
||||
)
|
||||
|
||||
it "for first page of four (has last but no dots)", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 4
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: [2, 3]
|
||||
rightdots: false
|
||||
last: 4
|
||||
next: 2
|
||||
)
|
||||
|
||||
it "for first page of five (has dots)", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 5
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: [2, 3]
|
||||
rightdots: true
|
||||
last: 5
|
||||
next: 2
|
||||
)
|
||||
|
||||
it "for last page of three (max with no first)", ->
|
||||
checkRender(
|
||||
page: 3
|
||||
numPages: 3
|
||||
previous: 2
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: [1, 2]
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for last page of four (has first but no dots)", ->
|
||||
checkRender(
|
||||
page: 4
|
||||
numPages: 4
|
||||
previous: 3
|
||||
first: 1
|
||||
leftdots: false
|
||||
lowPages: [2, 3]
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for last page of five (has dots)", ->
|
||||
checkRender(
|
||||
page: 5
|
||||
numPages: 5
|
||||
previous: 4
|
||||
first: 1
|
||||
leftdots: true
|
||||
lowPages: [3, 4]
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for middle page of five (max with no first/last)", ->
|
||||
checkRender(
|
||||
page: 3
|
||||
numPages: 5
|
||||
previous: 2
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: [1, 2]
|
||||
highPages: [4, 5]
|
||||
rightdots: false
|
||||
last: null
|
||||
next: 4
|
||||
)
|
||||
|
||||
it "for middle page of seven (has first/last but no dots)", ->
|
||||
checkRender(
|
||||
page: 4
|
||||
numPages: 7
|
||||
previous: 3
|
||||
first: 1
|
||||
leftdots: false
|
||||
lowPages: [2, 3]
|
||||
highPages: [5, 6]
|
||||
rightdots: false
|
||||
last: 7
|
||||
next: 5
|
||||
)
|
||||
|
||||
it "for middle page of nine (has dots)", ->
|
||||
checkRender(
|
||||
page: 5
|
||||
numPages: 9
|
||||
previous: 4
|
||||
first: 1
|
||||
leftdots: true
|
||||
lowPages: [3, 4]
|
||||
highPages: [6, 7]
|
||||
rightdots: true
|
||||
last: 9
|
||||
next: 6
|
||||
)
|
||||
|
||||
describe "pagination interaction", ->
|
||||
beforeEach ->
|
||||
@view = makeView(makeThreads(3), 1, 2)
|
||||
deferred = $.Deferred();
|
||||
spyOn($, "ajax").and.returnValue(deferred);
|
||||
|
||||
it "causes updated rendering", ->
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
params.success(
|
||||
discussion_data: [{id: "on_page_42", body: "dummy body"}]
|
||||
page: 42
|
||||
num_pages: 99
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
@view.$(".discussion-pagination a").first().click()
|
||||
expect(@view.$(".current-page").text()).toEqual("42")
|
||||
expect(@view.$(".last-page").text()).toEqual("99")
|
||||
|
||||
it "handles AJAX errors", ->
|
||||
spyOn(DiscussionUtil, "discussionAlert")
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
params.error()
|
||||
{always: ->}
|
||||
)
|
||||
@view.$(".discussion-pagination a").first().click()
|
||||
expect(DiscussionUtil.discussionAlert).toHaveBeenCalled()
|
||||
@@ -1,98 +0,0 @@
|
||||
class @DiscussionViewSpecHelper
|
||||
@makeThreadWithProps = (props) ->
|
||||
# Minimal set of properties necessary for rendering
|
||||
thread = {
|
||||
id: "dummy_id",
|
||||
thread_type: "discussion",
|
||||
pinned: false,
|
||||
endorsed: false,
|
||||
votes: {up_count: '0'},
|
||||
read: false,
|
||||
unread_comments_count: 0,
|
||||
comments_count: 0,
|
||||
abuse_flaggers: [],
|
||||
body: "",
|
||||
title: "dummy title",
|
||||
created_at: "2014-08-18T01:02:03Z"
|
||||
ability: {
|
||||
can_delete: false,
|
||||
can_reply: true,
|
||||
can_vote: false,
|
||||
editable: false,
|
||||
}
|
||||
}
|
||||
$.extend(thread, props)
|
||||
|
||||
@checkVoteClasses = (view) ->
|
||||
view.render()
|
||||
display_button = view.$el.find(".display-vote")
|
||||
expect(display_button.hasClass("is-hidden")).toBe(true)
|
||||
action_button = view.$el.find(".action-vote")
|
||||
# Check that inline css is not applied to the ".action-vote"
|
||||
expect(action_button).not.toHaveAttr('style','display: inline; ');
|
||||
|
||||
@expectVoteRendered = (view, model, user) ->
|
||||
button = view.$el.find(".action-vote")
|
||||
expect(button.hasClass("is-checked")).toBe(user.voted(model))
|
||||
expect(button.attr("aria-checked")).toEqual(user.voted(model).toString())
|
||||
expect(button.find(".vote-count").text()).toMatch("^#{model.get('votes').up_count} Votes?$")
|
||||
expect(button.find(".sr.js-sr-vote-count").text()).toMatch("^there are currently #{model.get('votes').up_count} votes?$")
|
||||
|
||||
@checkRenderVote = (view, model) ->
|
||||
view.render()
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user)
|
||||
window.user.vote(model)
|
||||
view.render()
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user)
|
||||
window.user.unvote(model)
|
||||
view.render()
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user)
|
||||
|
||||
triggerVoteEvent = (view, event, expectedUrl) ->
|
||||
deferred = $.Deferred()
|
||||
spyOn($, "ajax").and.callFake((params) =>
|
||||
expect(params.url.toString()).toEqual(expectedUrl)
|
||||
return deferred
|
||||
)
|
||||
view.render()
|
||||
view.$el.find(".action-vote").trigger(event)
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
deferred.resolve()
|
||||
|
||||
@checkUpvote = (view, model, user, event) ->
|
||||
expect(model.id in user.get('upvoted_ids')).toBe(false)
|
||||
initialVoteCount = model.get('votes').up_count
|
||||
triggerVoteEvent(view, event, DiscussionUtil.urlFor("upvote_#{model.get('type')}", model.id) + "?ajax=1")
|
||||
expect(model.id in user.get('upvoted_ids')).toBe(true)
|
||||
expect(model.get('votes').up_count).toEqual(initialVoteCount + 1)
|
||||
|
||||
@checkUnvote = (view, model, user, event) ->
|
||||
user.vote(model)
|
||||
expect(model.id in user.get('upvoted_ids')).toBe(true)
|
||||
initialVoteCount = model.get('votes').up_count
|
||||
triggerVoteEvent(view, event, DiscussionUtil.urlFor("undo_vote_for_#{model.get('type')}", model.id) + "?ajax=1")
|
||||
expect(user.get('upvoted_ids')).toEqual([])
|
||||
expect(model.get('votes').up_count).toEqual(initialVoteCount - 1)
|
||||
|
||||
@checkButtonEvents = (view, viewFunc, buttonSelector) ->
|
||||
spy = spyOn(view, viewFunc)
|
||||
button = view.$el.find(buttonSelector)
|
||||
|
||||
button.click()
|
||||
expect(spy).toHaveBeenCalled()
|
||||
spy.calls.reset()
|
||||
button.trigger($.Event("keydown", {which: 13}))
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
spy.calls.reset()
|
||||
button.trigger($.Event("keydown", {which: 32}))
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
@checkVoteButtonEvents = (view) ->
|
||||
@checkButtonEvents(view, "toggleVote", ".action-vote")
|
||||
|
||||
@setNextResponseContent = (content) ->
|
||||
$.ajax.and.callFake(
|
||||
(params) =>
|
||||
params.success({"content": content})
|
||||
{always: ->}
|
||||
)
|
||||
@@ -1,229 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
describe "NewPostView", ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
window.$$course_id = "edX/999/test"
|
||||
spyOn(DiscussionUtil, "makeWmdEditor").and.callFake(
|
||||
($content, $local, cls_identifier) ->
|
||||
$local("." + cls_identifier).html("<textarea></textarea>")
|
||||
)
|
||||
@discussion = new Discussion([], {pages: 1})
|
||||
|
||||
checkVisibility = (view, expectedVisible, expectedDisabled, render) =>
|
||||
if render
|
||||
view.render()
|
||||
# Can also be undefined if the element does not exist.
|
||||
expect(view.$('.group-selector-wrapper').is(":visible") or false).toEqual(expectedVisible)
|
||||
disabled = view.$(".js-group-select").prop("disabled") or false
|
||||
group_disabled = view.$('.group-selector-wrapper').hasClass('disabled')
|
||||
if expectedVisible and !expectedDisabled
|
||||
expect(disabled).toEqual(false)
|
||||
expect(group_disabled).toEqual(false)
|
||||
else if expectedDisabled
|
||||
expect(disabled).toEqual(true)
|
||||
expect(group_disabled).toEqual(true)
|
||||
|
||||
describe "cohort selector", ->
|
||||
beforeEach ->
|
||||
@course_settings = new DiscussionCourseSettings({
|
||||
"category_map": {
|
||||
"children": ["Topic", "General"],
|
||||
"entries": {
|
||||
"Topic": {"is_cohorted": true, "id": "topic"},
|
||||
"General": {"is_cohorted": false, "id": "general"}
|
||||
}
|
||||
},
|
||||
"allow_anonymous": false,
|
||||
"allow_anonymous_to_peers": false,
|
||||
"is_cohorted": true,
|
||||
"cohorts": [
|
||||
{"id": 1, "name": "Cohort1"},
|
||||
{"id": 2, "name": "Cohort2"}
|
||||
]
|
||||
})
|
||||
@view = new NewPostView(
|
||||
el: $("#fixture-element"),
|
||||
collection: @discussion,
|
||||
course_settings: @course_settings,
|
||||
is_commententable_cohorted: true,
|
||||
mode: "tab"
|
||||
)
|
||||
|
||||
it "is not visible to students", ->
|
||||
checkVisibility(@view, false, false, true)
|
||||
|
||||
it "allows TAs to see the cohort selector", ->
|
||||
DiscussionSpecHelper.makeTA()
|
||||
checkVisibility(@view, true, false, true)
|
||||
|
||||
it "allows moderators to see the cohort selector", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
checkVisibility(@view, true, false, true)
|
||||
|
||||
it "only enables the cohort selector when applicable", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
# We start on the cohorted discussion
|
||||
checkVisibility(@view, true, false, true)
|
||||
# Select the uncohorted topic
|
||||
$('.topic-title:contains(General)').click()
|
||||
# The menu should now be visible but disabled.
|
||||
checkVisibility(@view, true, true, false)
|
||||
# Select the cohorted topic again
|
||||
$('.topic-title:contains(Topic)').click()
|
||||
# It should be visible and enabled once more.
|
||||
checkVisibility(@view, true, false, false)
|
||||
|
||||
it "allows the user to make a cohort selection", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
@view.render()
|
||||
expectedGroupId = null
|
||||
DiscussionSpecHelper.makeAjaxSpy(
|
||||
(params) -> expect(params.data.group_id).toEqual(expectedGroupId)
|
||||
)
|
||||
|
||||
_.each(
|
||||
["1", "2", ""],
|
||||
(groupIdStr) =>
|
||||
expectedGroupId = groupIdStr
|
||||
@view.$(".js-group-select").val(groupIdStr)
|
||||
@view.$(".js-post-title").val("dummy title")
|
||||
@view.$(".js-post-body textarea").val("dummy body")
|
||||
@view.$(".forum-new-post-form").submit()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
$.ajax.calls.reset()
|
||||
)
|
||||
|
||||
describe "always cohort inline discussions ", ->
|
||||
beforeEach ->
|
||||
@course_settings = new DiscussionCourseSettings({
|
||||
"category_map": {
|
||||
"children": [],
|
||||
"entries": {}
|
||||
},
|
||||
"allow_anonymous": false,
|
||||
"allow_anonymous_to_peers": false,
|
||||
"is_cohorted": true,
|
||||
"cohorts": [
|
||||
{"id": 1, "name": "Cohort1"},
|
||||
{"id": 2, "name": "Cohort2"}
|
||||
]
|
||||
})
|
||||
@view = new NewPostView(
|
||||
el: $("#fixture-element"),
|
||||
collection: @discussion,
|
||||
course_settings: @course_settings,
|
||||
mode: "tab"
|
||||
)
|
||||
|
||||
it "disables the cohort menu if it is set false", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
@view.is_commentable_cohorted = false
|
||||
checkVisibility(@view, true, true, true)
|
||||
|
||||
it "enables the cohort menu if it is set true", ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
@view.is_commentable_cohorted = true
|
||||
checkVisibility(@view, true, false, true)
|
||||
|
||||
it "is not visible to students when set false", ->
|
||||
@view.is_commentable_cohorted = false
|
||||
checkVisibility(@view, false, false, true)
|
||||
|
||||
it "is not visible to students when set true", ->
|
||||
@view.is_commentable_cohorted = true
|
||||
checkVisibility(@view, false, false, true)
|
||||
|
||||
describe "cancel post resets form ", ->
|
||||
beforeEach ->
|
||||
@course_settings = new DiscussionCourseSettings({
|
||||
"allow_anonymous_to_peers":true,
|
||||
"allow_anonymous":true,
|
||||
"category_map": {
|
||||
"subcategories": {
|
||||
"Week 1": {
|
||||
"subcategories": {},
|
||||
"children": [
|
||||
"Topic-Level Student-Visible Label"
|
||||
],
|
||||
"entries": {
|
||||
"Topic-Level Student-Visible Label": {
|
||||
"sort_key": null,
|
||||
"is_cohorted": false,
|
||||
"id": "2b3a858d0c884eb4b272dbbe3f2ffddd"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
"General",
|
||||
"Week 1"
|
||||
],
|
||||
"entries": {
|
||||
"General": {
|
||||
"sort_key": "General",
|
||||
"is_cohorted": false,
|
||||
"id": "i4x-waqastest-waqastest-course-waqastest"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
checkPostCancelReset = (mode, discussion, course_settings) ->
|
||||
view = new NewPostView(
|
||||
el: $("#fixture-element"),
|
||||
collection: discussion,
|
||||
course_settings: course_settings,
|
||||
mode: mode
|
||||
)
|
||||
view.render()
|
||||
eventSpy = jasmine.createSpy('eventSpy')
|
||||
view.listenTo(view, "newPost:cancel", eventSpy)
|
||||
view.$(".post-errors").html("<li class='post-error'>Title can't be empty</li>")
|
||||
view.$("label[for$='post-type-question']").click()
|
||||
view.$(".js-post-title").val("Test Title")
|
||||
view.$(".js-post-body textarea").val("Test body")
|
||||
view.$(".wmd-preview p").html("Test body")
|
||||
view.$(".js-follow").prop("checked", false)
|
||||
view.$(".js-anon").prop("checked", true)
|
||||
view.$(".js-anon-peers").prop("checked", true)
|
||||
if mode == "tab"
|
||||
view.$("a[data-discussion-id='2b3a858d0c884eb4b272dbbe3f2ffddd']").click()
|
||||
view.$(".cancel").click()
|
||||
expect(eventSpy).toHaveBeenCalled()
|
||||
expect(view.$(".post-errors").html()).toEqual("");
|
||||
expect($("input[id$='post-type-discussion']")).toBeChecked()
|
||||
expect($("input[id$='post-type-question']")).not.toBeChecked()
|
||||
expect(view.$(".js-post-title").val()).toEqual("");
|
||||
expect(view.$(".js-post-body textarea").val()).toEqual("");
|
||||
expect(view.$(".js-follow")).toBeChecked()
|
||||
expect(view.$(".js-anon")).not.toBeChecked()
|
||||
expect(view.$(".js-anon-peers")).not.toBeChecked()
|
||||
if mode == "tab"
|
||||
expect(view.$(".js-selected-topic").text()).toEqual("General")
|
||||
|
||||
_.each(["tab", "inline"], (mode) =>
|
||||
it "resets the form in #{mode} mode", ->
|
||||
checkPostCancelReset(mode, @discussion, @course_settings)
|
||||
)
|
||||
|
||||
it "posts to the correct URL", ->
|
||||
topicId = "test_topic"
|
||||
spyOn($, "ajax").and.callFake(
|
||||
(params) ->
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("create_thread", topicId))
|
||||
{always: ->}
|
||||
)
|
||||
view = new NewPostView(
|
||||
el: $("#fixture-element"),
|
||||
collection: @discussion,
|
||||
course_settings: new DiscussionCourseSettings({
|
||||
allow_anonymous: false,
|
||||
allow_anonymous_to_peers: false
|
||||
}),
|
||||
mode: "inline",
|
||||
topicId: topicId
|
||||
)
|
||||
view.render()
|
||||
view.$(".forum-new-post-form").submit()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
@@ -1,102 +0,0 @@
|
||||
describe 'ResponseCommentShowView', ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
# set up the container for the response to go in
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
# set up a model for a new Comment
|
||||
@comment = new Comment {
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
body: 'this is a response',
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
abuse_flaggers: ['123']
|
||||
roles: []
|
||||
}
|
||||
@view = new ResponseCommentShowView({ model: @comment })
|
||||
spyOn(@view, "convertMath")
|
||||
|
||||
it 'defines the tag', ->
|
||||
expect($('#jasmine-fixtures')).toExist
|
||||
expect(@view.tagName).toBeDefined
|
||||
expect(@view.el.tagName.toLowerCase()).toBe 'li'
|
||||
|
||||
it 'is tied to the model', ->
|
||||
expect(@view.model).toBeDefined()
|
||||
|
||||
describe 'rendering', ->
|
||||
|
||||
beforeEach ->
|
||||
spyOn(@view, 'renderAttrs')
|
||||
|
||||
it 'can be flagged for abuse', ->
|
||||
@comment.flagAbuse()
|
||||
expect(@comment.get 'abuse_flaggers').toEqual ['123', '567']
|
||||
|
||||
it 'can be unflagged for abuse', ->
|
||||
temp_array = []
|
||||
temp_array.push(window.user.get('id'))
|
||||
@comment.set("abuse_flaggers",temp_array)
|
||||
@comment.unflagAbuse()
|
||||
expect(@comment.get 'abuse_flaggers').toEqual []
|
||||
|
||||
describe '_delete', ->
|
||||
|
||||
it 'triggers on the correct events', ->
|
||||
DiscussionUtil.loadRoles []
|
||||
@comment.updateInfo {ability: {'can_delete': true}}
|
||||
@view.render()
|
||||
DiscussionViewSpecHelper.checkButtonEvents(@view, "_delete", ".action-delete")
|
||||
|
||||
it 'triggers the delete event', ->
|
||||
triggerTarget = jasmine.createSpy()
|
||||
@view.bind "comment:_delete", triggerTarget
|
||||
@view._delete()
|
||||
expect(triggerTarget).toHaveBeenCalled()
|
||||
|
||||
describe 'edit', ->
|
||||
|
||||
it 'triggers on the correct events', ->
|
||||
DiscussionUtil.loadRoles []
|
||||
@comment.updateInfo {ability: {'can_edit': true}}
|
||||
@view.render()
|
||||
DiscussionViewSpecHelper.checkButtonEvents(@view, "edit", ".action-edit")
|
||||
|
||||
it 'triggers comment:edit when the edit button is clicked', ->
|
||||
triggerTarget = jasmine.createSpy()
|
||||
@view.bind "comment:edit", triggerTarget
|
||||
@view.edit()
|
||||
expect(triggerTarget).toHaveBeenCalled()
|
||||
|
||||
describe "labels", ->
|
||||
|
||||
expectOneElement = (view, selector, visible=true) =>
|
||||
view.render()
|
||||
elements = view.$el.find(selector)
|
||||
expect(elements.length).toEqual(1)
|
||||
if visible
|
||||
expect(elements).not.toHaveClass("is-hidden")
|
||||
else
|
||||
expect(elements).toHaveClass("is-hidden")
|
||||
|
||||
it 'displays the reported label when appropriate for a non-staff user', ->
|
||||
@comment.set('abuse_flaggers', [])
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
# flagged by current user - should be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
# flagged by some other user but not the current one - should not be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
|
||||
it 'displays the reported label when appropriate for a flag moderator', ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
@comment.set('abuse_flaggers', [])
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
# flagged by current user - should be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
# flagged by some other user but not the current one - should still be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
@@ -1,155 +0,0 @@
|
||||
describe 'ResponseCommentView', ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
@comment = new Comment {
|
||||
id: '01234567',
|
||||
user_id: user.id,
|
||||
course_id: $$course_id,
|
||||
body: 'this is a response',
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
abuse_flaggers: ['123']
|
||||
roles: ['Student']
|
||||
}
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
@view = new ResponseCommentView({ model: @comment, el: $("#fixture-element") })
|
||||
spyOn(ResponseCommentShowView.prototype, "convertMath")
|
||||
spyOn(DiscussionUtil, "makeWmdEditor")
|
||||
@view.render()
|
||||
|
||||
describe '_delete', ->
|
||||
beforeEach ->
|
||||
@comment.updateInfo {ability: {can_delete: true}}
|
||||
@event = DiscussionSpecHelper.makeEventSpy()
|
||||
spyOn(@comment, "remove")
|
||||
spyOn(@view.$el, "remove")
|
||||
|
||||
setAjaxResult = (isSuccess) ->
|
||||
spyOn($, "ajax").and.callFake(
|
||||
(params) =>
|
||||
(if isSuccess then params.success else params.error) {}
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
it 'requires confirmation before deleting', ->
|
||||
spyOn(window, "confirm").and.returnValue(false)
|
||||
setAjaxResult(true)
|
||||
@view._delete(@event)
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
expect($.ajax).not.toHaveBeenCalled()
|
||||
expect(@comment.remove).not.toHaveBeenCalled()
|
||||
|
||||
it 'removes the deleted comment object', ->
|
||||
setAjaxResult(true)
|
||||
@view._delete(@event)
|
||||
expect(@comment.remove).toHaveBeenCalled()
|
||||
expect(@view.$el.remove).toHaveBeenCalled()
|
||||
|
||||
it 'calls the ajax comment deletion endpoint', ->
|
||||
setAjaxResult(true)
|
||||
@view._delete(@event)
|
||||
expect(@event.preventDefault).toHaveBeenCalled()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect($.ajax.calls.mostRecent().args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/delete')
|
||||
|
||||
it 'handles ajax errors', ->
|
||||
spyOn(DiscussionUtil, "discussionAlert")
|
||||
setAjaxResult(false)
|
||||
@view._delete(@event)
|
||||
expect(@event.preventDefault).toHaveBeenCalled()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect(@comment.remove).not.toHaveBeenCalled()
|
||||
expect(@view.$el.remove).not.toHaveBeenCalled()
|
||||
expect(DiscussionUtil.discussionAlert).toHaveBeenCalled()
|
||||
|
||||
it 'does not delete a comment if the permission is false', ->
|
||||
@comment.updateInfo {ability: {'can_delete': false}}
|
||||
spyOn(window, "confirm")
|
||||
setAjaxResult(true)
|
||||
@view._delete(@event)
|
||||
expect(window.confirm).not.toHaveBeenCalled()
|
||||
expect($.ajax).not.toHaveBeenCalled()
|
||||
expect(@comment.remove).not.toHaveBeenCalled()
|
||||
expect(@view.$el.remove).not.toHaveBeenCalled()
|
||||
|
||||
describe 'renderShowView', ->
|
||||
it 'renders the show view, removes the edit view, and registers event handlers', ->
|
||||
spyOn(@view, "_delete")
|
||||
spyOn(@view, "edit")
|
||||
# Without calling renderEditView first, renderShowView is a no-op
|
||||
@view.renderEditView()
|
||||
@view.renderShowView()
|
||||
@view.showView.trigger "comment:_delete", DiscussionSpecHelper.makeEventSpy()
|
||||
expect(@view._delete).toHaveBeenCalled()
|
||||
@view.showView.trigger "comment:edit", DiscussionSpecHelper.makeEventSpy()
|
||||
expect(@view.edit).toHaveBeenCalled()
|
||||
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).not.toHaveClass("edit-post-form")
|
||||
|
||||
describe 'renderEditView', ->
|
||||
it 'renders the edit view, removes the show view, and registers event handlers', ->
|
||||
spyOn(@view, "update")
|
||||
spyOn(@view, "cancelEdit")
|
||||
@view.renderEditView()
|
||||
@view.editView.trigger "comment:update", DiscussionSpecHelper.makeEventSpy()
|
||||
expect(@view.update).toHaveBeenCalled()
|
||||
@view.editView.trigger "comment:cancel_edit", DiscussionSpecHelper.makeEventSpy()
|
||||
expect(@view.cancelEdit).toHaveBeenCalled()
|
||||
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).toHaveClass("edit-post-form")
|
||||
|
||||
describe 'edit', ->
|
||||
it 'triggers the appropriate event and switches to the edit view', ->
|
||||
spyOn(@view, 'renderEditView')
|
||||
editTarget = jasmine.createSpy()
|
||||
@view.bind "comment:edit", editTarget
|
||||
@view.edit()
|
||||
expect(@view.renderEditView).toHaveBeenCalled()
|
||||
expect(editTarget).toHaveBeenCalled()
|
||||
|
||||
describe 'with edit view displayed', ->
|
||||
beforeEach ->
|
||||
@view.renderEditView()
|
||||
|
||||
describe 'cancelEdit', ->
|
||||
it 'triggers the appropriate event and switches to the show view', ->
|
||||
spyOn(@view, 'renderShowView')
|
||||
cancelEditTarget = jasmine.createSpy()
|
||||
@view.bind "comment:cancel_edit", cancelEditTarget
|
||||
@view.cancelEdit()
|
||||
expect(@view.renderShowView).toHaveBeenCalled()
|
||||
expect(cancelEditTarget).toHaveBeenCalled()
|
||||
|
||||
describe 'update', ->
|
||||
beforeEach ->
|
||||
@updatedBody = "updated body"
|
||||
# Markdown code creates the editor, so we simulate that here
|
||||
@view.$el.find(".edit-comment-body").html($("<textarea></textarea>"))
|
||||
@view.$el.find(".edit-comment-body textarea").val(@updatedBody)
|
||||
spyOn(@view, 'cancelEdit')
|
||||
spyOn($, "ajax").and.callFake(
|
||||
(params) =>
|
||||
if @ajaxSucceed
|
||||
params.success()
|
||||
else
|
||||
params.error({status: 500})
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
it 'calls the update endpoint correctly and displays the show view on success', ->
|
||||
@ajaxSucceed = true
|
||||
@view.update(DiscussionSpecHelper.makeEventSpy())
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect($.ajax.calls.mostRecent().args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update')
|
||||
expect($.ajax.calls.mostRecent().args[0].data.body).toEqual(@updatedBody)
|
||||
expect(@view.model.get("body")).toEqual(@updatedBody)
|
||||
expect(@view.cancelEdit).toHaveBeenCalled()
|
||||
|
||||
it 'handles AJAX errors', ->
|
||||
originalBody = @comment.get("body")
|
||||
@ajaxSucceed = false
|
||||
@view.update(DiscussionSpecHelper.makeEventSpy())
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect($.ajax.calls.mostRecent().args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update')
|
||||
expect($.ajax.calls.mostRecent().args[0].data.body).toEqual(@updatedBody)
|
||||
expect(@view.model.get("body")).toEqual(originalBody)
|
||||
expect(@view.cancelEdit).not.toHaveBeenCalled()
|
||||
expect(@view.$(".edit-comment-form-errors *").length).toEqual(1)
|
||||
@@ -1,230 +0,0 @@
|
||||
describe "ThreadResponseShowView", ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
@user = DiscussionUtil.getUser()
|
||||
@thread = new Thread({"thread_type": "discussion"})
|
||||
@commentData = {
|
||||
id: "dummy",
|
||||
user_id: "567",
|
||||
course_id: "TestOrg/TestCourse/TestRun",
|
||||
body: "this is a comment",
|
||||
created_at: "2013-04-03T20:08:39Z",
|
||||
endorsed: false,
|
||||
abuse_flaggers: [],
|
||||
votes: {up_count: 42},
|
||||
type: "comment"
|
||||
}
|
||||
@comment = new Comment(@commentData)
|
||||
@comment.set("thread", @thread)
|
||||
@view = new ThreadResponseShowView({ model: @comment, $el: $("#fixture-element") })
|
||||
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(ThreadResponseShowView.prototype, "convertMath")
|
||||
|
||||
@view.render()
|
||||
|
||||
describe "voting", ->
|
||||
|
||||
it "renders the vote state correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
|
||||
|
||||
it "check the vote classes after renders", ->
|
||||
DiscussionViewSpecHelper.checkVoteClasses(@view)
|
||||
|
||||
it "votes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @comment, @user, $.Event("click"))
|
||||
|
||||
it "votes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @comment, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
it "unvotes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @user, $.Event("click"))
|
||||
|
||||
it "unvotes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
it "renders endorsement correctly for a marked answer in a question thread", ->
|
||||
endorsement = {
|
||||
"username": "test_endorser",
|
||||
"user_id": "test_id",
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@thread.set("thread_type", "question")
|
||||
@comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text().replace(/\s+/g, " ")).toMatch(
|
||||
"marked as answer less than a minute ago by " + endorsement.username
|
||||
)
|
||||
expect(@view.$(".posted-details > a").attr('href')).toEqual("/courses/edX/999/test/discussion/forum/users/test_id")
|
||||
|
||||
it "renders anonymous endorsement correctly for a marked answer in a question thread", ->
|
||||
endorsement = {
|
||||
"username": null,
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@thread.set("thread_type", "question")
|
||||
@comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text()).toMatch("marked as answer less than a minute ago")
|
||||
expect(@view.$(".posted-details").text()).not.toMatch("\sby\s")
|
||||
|
||||
it "renders endorsement correctly for an endorsed response in a discussion thread", ->
|
||||
endorsement = {
|
||||
"username": "test_endorser",
|
||||
"user_id": "test_id",
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@thread.set("thread_type", "discussion")
|
||||
@comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text().replace(/\s+/g, " ")).toMatch(
|
||||
"endorsed less than a minute ago by " + endorsement.username
|
||||
)
|
||||
expect(@view.$(".posted-details > a").attr('href')).toEqual("/courses/edX/999/test/discussion/forum/users/test_id")
|
||||
|
||||
it "renders anonymous endorsement correctly for an endorsed response in a discussion thread", ->
|
||||
endorsement = {
|
||||
"username": null,
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@thread.set("thread_type", "discussion")
|
||||
@comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text()).toMatch("endorsed less than a minute ago")
|
||||
expect(@view.$(".posted-details").text()).not.toMatch("\sby\s")
|
||||
|
||||
it "re-renders correctly when endorsement changes", ->
|
||||
spyOn($, "ajax").and.returnValue($.Deferred())
|
||||
DiscussionUtil.loadRoles({"Moderator": [parseInt(window.user.id)]})
|
||||
@thread.set("thread_type", "question")
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text()).not.toMatch("marked as answer")
|
||||
@view.$(".action-answer").click()
|
||||
expect(@view.$(".posted-details").text()).toMatch("marked as answer")
|
||||
@view.$(".action-answer").click()
|
||||
expect(@view.$(".posted-details").text()).not.toMatch("marked as answer")
|
||||
|
||||
it "allows a moderator to mark an answer in a question thread", ->
|
||||
spyOn($, "ajax").and.returnValue($.Deferred())
|
||||
DiscussionUtil.loadRoles({"Moderator": [parseInt(window.user.id)]})
|
||||
@thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": (parseInt(window.user.id) + 1).toString()
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-answer")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden")
|
||||
endorseButton.click()
|
||||
expect(endorseButton).toHaveClass("is-checked")
|
||||
|
||||
it "allows the author of a question thread to mark an answer", ->
|
||||
spyOn($, "ajax").and.returnValue($.Deferred())
|
||||
@thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": window.user.id
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-answer")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden")
|
||||
endorseButton.click()
|
||||
expect(endorseButton).toHaveClass("is-checked")
|
||||
|
||||
it "does not allow the author of a discussion thread to endorse", ->
|
||||
@thread.set({
|
||||
"thread_type": "discussion",
|
||||
"user_id": window.user.id
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-endorse")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
expect(endorseButton.closest(".actions-item")).toHaveClass("is-hidden")
|
||||
|
||||
it "does not allow a student who is not the author of a question thread to mark an answer", ->
|
||||
@thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": (parseInt(window.user.id) + 1).toString()
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-answer")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
expect(endorseButton.closest(".actions-item")).toHaveClass("is-hidden")
|
||||
|
||||
describe "labels", ->
|
||||
|
||||
expectOneElement = (view, selector, visible=true) =>
|
||||
view.render()
|
||||
elements = view.$el.find(selector)
|
||||
expect(elements.length).toEqual(1)
|
||||
if visible
|
||||
expect(elements).not.toHaveClass("is-hidden")
|
||||
else
|
||||
expect(elements).toHaveClass("is-hidden")
|
||||
|
||||
it 'displays the reported label when appropriate for a non-staff user', ->
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
# flagged by current user - should be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
# flagged by some other user but not the current one - should not be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
|
||||
it 'displays the reported label when appropriate for a flag moderator', ->
|
||||
DiscussionSpecHelper.makeModerator()
|
||||
expectOneElement(@view, '.post-label-reported', false)
|
||||
# flagged by current user - should be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
# flagged by some other user but not the current one - should still be labelled
|
||||
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
|
||||
expectOneElement(@view, '.post-label-reported')
|
||||
|
||||
describe "endorser display", ->
|
||||
|
||||
beforeEach ->
|
||||
@comment.set('endorsement', {
|
||||
"username": "test_endorser",
|
||||
"time": new Date().toISOString()
|
||||
})
|
||||
spyOn(DiscussionUtil, 'urlFor').and.returnValue('test_endorser_url')
|
||||
|
||||
checkUserLink = (element, is_ta, is_staff) ->
|
||||
expect(element.find('a.username').length).toEqual(1)
|
||||
expect(element.find('a.username').text()).toEqual('test_endorser')
|
||||
expect(element.find('a.username').attr('href')).toEqual('test_endorser_url')
|
||||
expect(element.find('.user-label-community-ta').length).toEqual(if is_ta then 1 else 0)
|
||||
expect(element.find('.user-label-staff').length).toEqual(if is_staff then 1 else 0)
|
||||
|
||||
it "renders nothing when the response has not been endorsed", ->
|
||||
@comment.set('endorsement', null)
|
||||
expect(@view.getEndorserDisplay()).toBeNull()
|
||||
|
||||
it "renders correctly for a student-endorsed response", ->
|
||||
$el = $('#fixture-element').html(@view.getEndorserDisplay())
|
||||
checkUserLink($el, false, false)
|
||||
|
||||
it "renders correctly for a community TA-endorsed response", ->
|
||||
spyOn(DiscussionUtil, 'isTA').and.returnValue(true)
|
||||
$el = $('#fixture-element').html(@view.getEndorserDisplay())
|
||||
checkUserLink($el, true, false)
|
||||
|
||||
it "renders correctly for a staff-endorsed response", ->
|
||||
spyOn(DiscussionUtil, 'isStaff').and.returnValue(true)
|
||||
$el = $('#fixture-element').html(@view.getEndorserDisplay())
|
||||
checkUserLink($el, false, true)
|
||||
@@ -1,92 +0,0 @@
|
||||
describe 'ThreadResponseView', ->
|
||||
beforeEach ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
@thread = new Thread({"thread_type": "discussion"})
|
||||
@response = new Comment {
|
||||
children: [{}, {}],
|
||||
thread: @thread,
|
||||
}
|
||||
@view = new ThreadResponseView({model: @response, el: $("#fixture-element")})
|
||||
spyOn(ThreadResponseShowView.prototype, "render")
|
||||
spyOn(ResponseCommentView.prototype, "render")
|
||||
|
||||
describe 'closed and open Threads', ->
|
||||
checkCommentForm = (closed) ->
|
||||
thread = new Thread({"thread_type": "discussion", "closed": closed})
|
||||
commentData = {
|
||||
id: "dummy",
|
||||
user_id: "567",
|
||||
course_id: "TestOrg/TestCourse/TestRun",
|
||||
body: "this is a comment",
|
||||
created_at: "2013-04-03T20:08:39Z",
|
||||
abuse_flaggers: [],
|
||||
type: "comment",
|
||||
children: [],
|
||||
thread: thread,
|
||||
}
|
||||
comment = new Comment(commentData)
|
||||
view = new ThreadResponseView({
|
||||
model: comment, el: $("#fixture-element"),
|
||||
})
|
||||
view.render()
|
||||
expect(view.$('.comment-form').closest('li').is(":visible")).toBe(not closed)
|
||||
|
||||
it 'hides comment form when thread is closed', ->
|
||||
checkCommentForm(true)
|
||||
|
||||
it 'show comment form when thread is open', ->
|
||||
checkCommentForm(false)
|
||||
|
||||
describe 'renderComments', ->
|
||||
it 'hides "show comments" link if collapseComments is not set', ->
|
||||
@view.render()
|
||||
expect(@view.$(".comments")).toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).not.toBeVisible()
|
||||
|
||||
it 'hides "show comments" link if collapseComments is set but response has no comments', ->
|
||||
@response = new Comment { children: [], thread: @thread }
|
||||
@view = new ThreadResponseView({
|
||||
model: @response, el: $("#fixture-element"),
|
||||
collapseComments: true
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".comments")).toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).not.toBeVisible()
|
||||
|
||||
it 'hides comments if collapseComments is set and shows them when "show comments" link is clicked', ->
|
||||
@view = new ThreadResponseView({
|
||||
model: @response, el: $("#fixture-element"),
|
||||
collapseComments: true
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".comments")).not.toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).toBeVisible()
|
||||
@view.$(".action-show-comments").click()
|
||||
expect(@view.$(".comments")).toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).not.toBeVisible()
|
||||
|
||||
it 'populates commentViews and binds events', ->
|
||||
# Ensure that edit view is set to test invocation of cancelEdit
|
||||
@view.createEditView()
|
||||
spyOn(@view, 'cancelEdit')
|
||||
spyOn(@view, 'cancelCommentEdits')
|
||||
spyOn(@view, 'hideCommentForm')
|
||||
spyOn(@view, 'showCommentForm')
|
||||
@view.renderComments()
|
||||
expect(@view.commentViews.length).toEqual(2)
|
||||
@view.commentViews[0].trigger "comment:edit", jasmine.createSpyObj("event", ["preventDefault"])
|
||||
expect(@view.cancelEdit).toHaveBeenCalled()
|
||||
expect(@view.cancelCommentEdits).toHaveBeenCalled()
|
||||
expect(@view.hideCommentForm).toHaveBeenCalled()
|
||||
@view.commentViews[0].trigger "comment:cancel_edit"
|
||||
expect(@view.showCommentForm).toHaveBeenCalled()
|
||||
|
||||
describe 'cancelCommentEdits', ->
|
||||
it 'calls cancelEdit on each comment view', ->
|
||||
@view.renderComments()
|
||||
expect(@view.commentViews.length).toEqual(2)
|
||||
_.each(@view.commentViews, (commentView) -> spyOn(commentView, 'cancelEdit'))
|
||||
@view.cancelCommentEdits()
|
||||
_.each(@view.commentViews, (commentView) -> expect(commentView.cancelEdit).toHaveBeenCalled())
|
||||
@@ -1,2 +0,0 @@
|
||||
!views/discussion_thread_edit_view.js
|
||||
!views/discussion_topic_menu_view.js
|
||||
@@ -1,218 +0,0 @@
|
||||
if Backbone?
|
||||
class @Content extends Backbone.Model
|
||||
|
||||
@contents: {}
|
||||
@contentInfos: {}
|
||||
|
||||
template: -> DiscussionUtil.getTemplate('_content')
|
||||
|
||||
actions:
|
||||
editable: '.admin-edit'
|
||||
can_reply: '.discussion-reply'
|
||||
can_delete: '.admin-delete'
|
||||
can_openclose: '.admin-openclose'
|
||||
can_report: '.admin-report'
|
||||
can_vote: '.admin-vote'
|
||||
|
||||
urlMappers: {}
|
||||
|
||||
urlFor: (name) ->
|
||||
@urlMappers[name].apply(@)
|
||||
|
||||
can: (action) ->
|
||||
(@get('ability') || {})[action]
|
||||
|
||||
# Default implementation
|
||||
canBeEndorsed: -> false
|
||||
|
||||
updateInfo: (info) ->
|
||||
if info
|
||||
@set('ability', info.ability)
|
||||
@set('voted', info.voted)
|
||||
@set('subscribed', info.subscribed)
|
||||
|
||||
addComment: (comment, options) ->
|
||||
options ||= {}
|
||||
if not options.silent
|
||||
thread = @get('thread')
|
||||
comments_count = parseInt(thread.get('comments_count'))
|
||||
thread.set('comments_count', comments_count + 1)
|
||||
@get('children').push comment
|
||||
model = new Comment $.extend {}, comment, { thread: @get('thread') }
|
||||
@get('comments').add model
|
||||
@trigger "comment:add"
|
||||
model
|
||||
|
||||
removeComment: (comment) ->
|
||||
thread = @get('thread')
|
||||
comments_count = parseInt(thread.get('comments_count'))
|
||||
thread.set('comments_count', comments_count - 1 - comment.getCommentsCount())
|
||||
@trigger "comment:remove"
|
||||
|
||||
resetComments: (children) ->
|
||||
@set 'children', []
|
||||
@set 'comments', new Comments()
|
||||
for comment in (children || [])
|
||||
@addComment comment, { silent: true }
|
||||
|
||||
initialize: ->
|
||||
Content.addContent @id, @
|
||||
userId = @get('user_id')
|
||||
if userId?
|
||||
@set('staff_authored', DiscussionUtil.isStaff(userId))
|
||||
@set('community_ta_authored', DiscussionUtil.isTA(userId))
|
||||
else
|
||||
@set('staff_authored', false)
|
||||
@set('community_ta_authored', false)
|
||||
if Content.getInfo(@id)
|
||||
@updateInfo(Content.getInfo(@id))
|
||||
@set 'user_url', DiscussionUtil.urlFor('user_profile', userId)
|
||||
@resetComments(@get('children'))
|
||||
|
||||
remove: ->
|
||||
|
||||
if @get('type') == 'comment'
|
||||
@get('thread').removeComment(@)
|
||||
@get('thread').trigger "comment:remove", @
|
||||
else
|
||||
@trigger "thread:remove", @
|
||||
|
||||
@addContent: (id, content) -> @contents[id] = content
|
||||
|
||||
@getContent: (id) -> @contents[id]
|
||||
|
||||
@getInfo: (id) ->
|
||||
@contentInfos[id]
|
||||
|
||||
@loadContentInfos: (infos) ->
|
||||
for id, info of infos
|
||||
if @getContent(id)
|
||||
@getContent(id).updateInfo(info)
|
||||
$.extend @contentInfos, infos
|
||||
|
||||
pinThread: ->
|
||||
pinned = @get("pinned")
|
||||
@set("pinned",pinned)
|
||||
@trigger "change", @
|
||||
|
||||
unPinThread: ->
|
||||
pinned = @get("pinned")
|
||||
@set("pinned",pinned)
|
||||
@trigger "change", @
|
||||
|
||||
flagAbuse: ->
|
||||
temp_array = @get("abuse_flaggers")
|
||||
temp_array.push(window.user.get('id'))
|
||||
@set("abuse_flaggers",temp_array)
|
||||
@trigger "change", @
|
||||
|
||||
unflagAbuse: ->
|
||||
@get("abuse_flaggers").pop(window.user.get('id'))
|
||||
@trigger "change", @
|
||||
|
||||
isFlagged: ->
|
||||
user = DiscussionUtil.getUser()
|
||||
flaggers = @get("abuse_flaggers")
|
||||
user and (user.id in flaggers or (DiscussionUtil.isPrivilegedUser(user.id) and flaggers.length > 0))
|
||||
|
||||
incrementVote: (increment) ->
|
||||
newVotes = _.clone(@get("votes"))
|
||||
newVotes.up_count = newVotes.up_count + increment
|
||||
@set("votes", newVotes)
|
||||
|
||||
vote: ->
|
||||
@incrementVote(1)
|
||||
|
||||
unvote: ->
|
||||
@incrementVote(-1)
|
||||
|
||||
class @Thread extends @Content
|
||||
urlMappers:
|
||||
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @.get('commentable_id'), @id)
|
||||
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
|
||||
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
|
||||
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
|
||||
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
|
||||
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
|
||||
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
|
||||
'_delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
|
||||
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
|
||||
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
|
||||
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
|
||||
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
|
||||
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
|
||||
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
|
||||
|
||||
initialize: ->
|
||||
@set('thread', @)
|
||||
super()
|
||||
|
||||
comment: ->
|
||||
@set("comments_count", parseInt(@get("comments_count")) + 1)
|
||||
|
||||
follow: ->
|
||||
@set('subscribed', true)
|
||||
|
||||
unfollow: ->
|
||||
@set('subscribed', false)
|
||||
|
||||
display_body: ->
|
||||
if @has("highlighted_body")
|
||||
String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
|
||||
else
|
||||
@get("body")
|
||||
|
||||
display_title: ->
|
||||
if @has("highlighted_title")
|
||||
String(@get("highlighted_title")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
|
||||
else
|
||||
@get("title")
|
||||
|
||||
toJSON: ->
|
||||
json_attributes = _.clone(@attributes)
|
||||
_.extend(json_attributes, { title: @display_title(), body: @display_body() })
|
||||
|
||||
created_at_date: ->
|
||||
new Date(@get("created_at"))
|
||||
|
||||
created_at_time: ->
|
||||
new Date(@get("created_at")).getTime()
|
||||
|
||||
hasResponses: ->
|
||||
@get('comments_count') > 0
|
||||
|
||||
class @Comment extends @Content
|
||||
urlMappers:
|
||||
'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id)
|
||||
'unvote': -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
|
||||
'upvote': -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
|
||||
'downvote': -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
|
||||
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
|
||||
'update': -> DiscussionUtil.urlFor('update_comment', @id)
|
||||
'_delete': -> DiscussionUtil.urlFor('delete_comment', @id)
|
||||
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
|
||||
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
|
||||
|
||||
getCommentsCount: ->
|
||||
count = 0
|
||||
@get('comments').each (comment) ->
|
||||
count += comment.getCommentsCount() + 1
|
||||
count
|
||||
|
||||
canBeEndorsed: =>
|
||||
user_id = window.user.get("id")
|
||||
user_id && (
|
||||
DiscussionUtil.isPrivilegedUser(user_id) ||
|
||||
(@get('thread').get('thread_type') == 'question' && @get('thread').get('user_id') == user_id)
|
||||
)
|
||||
|
||||
class @Comments extends Backbone.Collection
|
||||
|
||||
model: Comment
|
||||
|
||||
initialize: ->
|
||||
@bind "add", (item) =>
|
||||
item.collection = @
|
||||
|
||||
find: (id) ->
|
||||
_.first @where(id: id)
|
||||
@@ -1,131 +0,0 @@
|
||||
if Backbone?
|
||||
class @Discussion extends Backbone.Collection
|
||||
model: Thread
|
||||
|
||||
initialize: (models, options={})->
|
||||
@pages = options['pages'] || 1
|
||||
@current_page = 1
|
||||
@sort_preference = options['sort']
|
||||
@bind "add", (item) =>
|
||||
item.discussion = @
|
||||
@setSortComparator(@sort_preference)
|
||||
@on "thread:remove", (thread) =>
|
||||
@remove(thread)
|
||||
|
||||
find: (id) ->
|
||||
_.first @where(id: id)
|
||||
|
||||
hasMorePages: ->
|
||||
@current_page < @pages
|
||||
|
||||
setSortComparator: (sortBy) ->
|
||||
switch sortBy
|
||||
when 'activity' then @comparator = @sortByDateRecentFirst
|
||||
when 'votes' then @comparator = @sortByVotes
|
||||
when 'comments' then @comparator = @sortByComments
|
||||
|
||||
addThread: (thread, options) ->
|
||||
# TODO: Check for existing thread with same ID in a faster way
|
||||
if not @find(thread.id)
|
||||
options ||= {}
|
||||
model = new Thread thread
|
||||
@add model
|
||||
model
|
||||
|
||||
retrieveAnotherPage: (mode, options={}, sort_options={}, error=null)->
|
||||
data = { page: @current_page + 1 }
|
||||
if _.contains(["unread", "unanswered", "flagged"], options.filter)
|
||||
data[options.filter] = true
|
||||
switch mode
|
||||
when 'search'
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
data['text'] = options.search_text
|
||||
when 'commentables'
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
data['commentable_ids'] = options.commentable_ids
|
||||
when 'all'
|
||||
url = DiscussionUtil.urlFor 'threads'
|
||||
when 'followed'
|
||||
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
|
||||
if options['group_id']
|
||||
data['group_id'] = options['group_id']
|
||||
data['sort_key'] = sort_options.sort_key || 'activity'
|
||||
data['sort_order'] = sort_options.sort_order || 'desc'
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$el
|
||||
url: url
|
||||
data: data
|
||||
dataType: 'json'
|
||||
success: (response, textStatus) =>
|
||||
models = @models
|
||||
new_threads = [new Thread(data) for data in response.discussion_data][0]
|
||||
new_collection = _.union(models, new_threads)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@pages = response.num_pages
|
||||
@current_page = response.page
|
||||
@reset new_collection
|
||||
error: error
|
||||
|
||||
sortByDate: (thread) ->
|
||||
#
|
||||
# The comment client asks each thread for a value by which to sort the collection
|
||||
# and calls this sort routine regardless of the order returned from the LMS/comments service
|
||||
# so, this takes advantage of this per-thread value and returns tomorrow's date
|
||||
# for pinned threads, ensuring that they appear first, (which is the intent of pinned threads)
|
||||
#
|
||||
@pinnedThreadsSortComparatorWithDate(thread, true)
|
||||
|
||||
|
||||
sortByDateRecentFirst: (thread) ->
|
||||
#
|
||||
# Same as above
|
||||
# but negative to flip the order (newest first)
|
||||
#
|
||||
@pinnedThreadsSortComparatorWithDate(thread, false)
|
||||
#return String.fromCharCode.apply(String,
|
||||
# _.map(thread.get("created_at").split(""),
|
||||
# ((c) -> return 0xffff - c.charChodeAt()))
|
||||
#)
|
||||
|
||||
sortByVotes: (thread1, thread2) ->
|
||||
thread1_count = parseInt(thread1.get("votes")['up_count'])
|
||||
thread2_count = parseInt(thread2.get("votes")['up_count'])
|
||||
@pinnedThreadsSortComparatorWithCount(thread1, thread2, thread1_count, thread2_count)
|
||||
|
||||
sortByComments: (thread1, thread2) ->
|
||||
thread1_count = parseInt(thread1.get("comments_count"))
|
||||
thread2_count = parseInt(thread2.get("comments_count"))
|
||||
@pinnedThreadsSortComparatorWithCount(thread1, thread2, thread1_count, thread2_count)
|
||||
|
||||
pinnedThreadsSortComparatorWithCount: (thread1, thread2, thread1_count, thread2_count) ->
|
||||
# if threads are pinned they should be displayed on top.
|
||||
# Unpinned will be sorted by their property count
|
||||
if thread1.get('pinned') and not thread2.get('pinned')
|
||||
-1
|
||||
else if thread2.get('pinned') and not thread1.get('pinned')
|
||||
1
|
||||
else
|
||||
if thread1_count > thread2_count
|
||||
-1
|
||||
else if thread2_count > thread1_count
|
||||
1
|
||||
else
|
||||
if thread1.created_at_time() > thread2.created_at_time()
|
||||
-1
|
||||
else
|
||||
1
|
||||
|
||||
pinnedThreadsSortComparatorWithDate: (thread, ascending)->
|
||||
# if threads are pinned they should be displayed on top.
|
||||
# Unpinned will be sorted by their last activity date
|
||||
threadLastActivityAtTime = new Date(thread.get("last_activity_at")).getTime()
|
||||
if thread.get('pinned')
|
||||
#use tomorrow's date
|
||||
today = new Date();
|
||||
preferredDate = new Date(today.getTime() + (24 * 60 * 60 * 1000) + threadLastActivityAtTime);
|
||||
else
|
||||
preferredDate = threadLastActivityAtTime
|
||||
if ascending
|
||||
preferredDate
|
||||
else
|
||||
-(preferredDate)
|
||||
@@ -1,173 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionModuleView extends Backbone.View
|
||||
events:
|
||||
"click .discussion-show": "toggleDiscussion"
|
||||
"keydown .discussion-show":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleDiscussion)
|
||||
"click .new-post-btn": "toggleNewPost"
|
||||
"keydown .new-post-btn":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleNewPost)
|
||||
"click .discussion-paginator a": "navigateToPage"
|
||||
|
||||
page_re: /\?discussion_page=(\d+)/
|
||||
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
|
||||
@page = 1
|
||||
|
||||
toggleNewPost: (event) =>
|
||||
event.preventDefault()
|
||||
if !@newPostForm
|
||||
@toggleDiscussion()
|
||||
@isWaitingOnNewPost = true;
|
||||
return
|
||||
if @showed
|
||||
@newPostForm.slideDown(300)
|
||||
else
|
||||
@newPostForm.show().focus()
|
||||
@toggleDiscussionBtn.addClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html(gettext("Hide Discussion"))
|
||||
@$("section.discussion").slideDown()
|
||||
@showed = true
|
||||
|
||||
hideNewPost: =>
|
||||
@newPostForm.slideUp(300)
|
||||
|
||||
hideDiscussion: =>
|
||||
@$("section.discussion").slideUp()
|
||||
@toggleDiscussionBtn.removeClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html(gettext("Show Discussion"))
|
||||
@showed = false
|
||||
|
||||
toggleDiscussion: (event) =>
|
||||
if @showed
|
||||
@hideDiscussion()
|
||||
else
|
||||
@toggleDiscussionBtn.addClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html(gettext("Hide Discussion"))
|
||||
|
||||
if @retrieved
|
||||
@$("section.discussion").slideDown()
|
||||
@showed = true
|
||||
else
|
||||
$elem = @toggleDiscussionBtn
|
||||
@loadPage(
|
||||
$elem,
|
||||
=>
|
||||
@hideDiscussion()
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the discussion. Please try again.")
|
||||
)
|
||||
)
|
||||
|
||||
loadPage: ($elem, error) =>
|
||||
discussionId = @$el.data("discussion-id")
|
||||
url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + "?page=#{@page}"
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
$loading: $elem
|
||||
takeFocus: true
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: 'json'
|
||||
success: (response, textStatus, jqXHR) => @renderDiscussion($elem, response, textStatus, discussionId)
|
||||
error: error
|
||||
|
||||
renderDiscussion: ($elem, response, textStatus, discussionId) =>
|
||||
$elem.focus()
|
||||
user = new DiscussionUser(response.user_info)
|
||||
window.user = user
|
||||
DiscussionUtil.setUser(user)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
DiscussionUtil.loadRoles(response.roles)
|
||||
|
||||
@course_settings = new DiscussionCourseSettings(response.course_settings)
|
||||
@discussion = new Discussion()
|
||||
@discussion.reset(response.discussion_data, {silent: false})
|
||||
|
||||
$discussion = _.template($("#inline-discussion-template").html())(
|
||||
'threads': response.discussion_data,
|
||||
'discussionId': discussionId
|
||||
)
|
||||
if @$('section.discussion').length
|
||||
@$('section.discussion').replaceWith($discussion)
|
||||
else
|
||||
@$el.append($discussion)
|
||||
|
||||
@newPostForm = this.$el.find('.new-post-article')
|
||||
@threadviews = @discussion.map (thread) =>
|
||||
view = new DiscussionThreadView(
|
||||
el: @$("article#thread_#{thread.id}"),
|
||||
model: thread,
|
||||
mode: "inline",
|
||||
context: @context,
|
||||
course_settings: @course_settings,
|
||||
topicId: discussionId
|
||||
)
|
||||
thread.on "thread:thread_type_updated", ->
|
||||
view.rerender()
|
||||
view.expand()
|
||||
return view
|
||||
_.each @threadviews, (dtv) -> dtv.render()
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
|
||||
@newPostView = new NewPostView(
|
||||
el: @newPostForm,
|
||||
collection: @discussion,
|
||||
course_settings: @course_settings,
|
||||
topicId: discussionId,
|
||||
is_commentable_cohorted: response.is_commentable_cohorted
|
||||
)
|
||||
@newPostView.render()
|
||||
@listenTo( @newPostView, 'newPost:cancel', @hideNewPost )
|
||||
@discussion.on "add", @addThread
|
||||
|
||||
@retrieved = true
|
||||
@showed = true
|
||||
@renderPagination(response.num_pages)
|
||||
|
||||
if @isWaitingOnNewPost
|
||||
@newPostForm.show().focus()
|
||||
|
||||
addThread: (thread, collection, options) =>
|
||||
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
|
||||
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
|
||||
@$('section.discussion > .threads').prepend(article)
|
||||
|
||||
threadView = new DiscussionThreadView(
|
||||
el: article,
|
||||
model: thread,
|
||||
mode: "inline",
|
||||
context: @context,
|
||||
course_settings: @course_settings,
|
||||
topicId: @$el.data("discussion-id")
|
||||
)
|
||||
threadView.render()
|
||||
@threadviews.unshift threadView
|
||||
|
||||
renderPagination: (numPages) =>
|
||||
pageUrl = (number) ->
|
||||
"?discussion_page=#{number}"
|
||||
params = DiscussionUtil.getPaginationParams(@page, numPages, pageUrl)
|
||||
pagination = _.template($("#pagination-template").html())(params)
|
||||
@$('section.discussion-pagination').html(pagination)
|
||||
|
||||
navigateToPage: (event) =>
|
||||
event.preventDefault()
|
||||
window.history.pushState({}, window.document.title, event.target.href)
|
||||
currPage = @page
|
||||
@page = $(event.target).data('page-number')
|
||||
@loadPage(
|
||||
$(event.target),
|
||||
=>
|
||||
@page = currPage
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the threads you requested. Please try again.")
|
||||
)
|
||||
)
|
||||
@@ -1,90 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionRouter extends Backbone.Router
|
||||
routes:
|
||||
"": "allThreads"
|
||||
":forum_name/threads/:thread_id" : "showThread"
|
||||
|
||||
initialize: (options) ->
|
||||
@discussion = options['discussion']
|
||||
@course_settings = options['course_settings']
|
||||
|
||||
@nav = new DiscussionThreadListView(
|
||||
collection: @discussion,
|
||||
el: $(".forum-nav"),
|
||||
courseSettings: @course_settings
|
||||
)
|
||||
@nav.on "thread:selected", @navigateToThread
|
||||
@nav.on "thread:removed", @navigateToAllThreads
|
||||
@nav.on "threads:rendered", @setActiveThread
|
||||
@nav.on "thread:created", @navigateToThread
|
||||
@nav.render()
|
||||
|
||||
@newPost = $('.new-post-article')
|
||||
@newPostView = new NewPostView(
|
||||
el: @newPost,
|
||||
collection: @discussion,
|
||||
course_settings: @course_settings,
|
||||
mode: "tab"
|
||||
)
|
||||
@newPostView.render()
|
||||
@listenTo( @newPostView, 'newPost:cancel', @hideNewPost )
|
||||
$('.new-post-btn').bind "click", @showNewPost
|
||||
$('.new-post-btn').bind "keydown", (event) => DiscussionUtil.activateOnSpace(event, @showNewPost)
|
||||
|
||||
allThreads: ->
|
||||
@nav.updateSidebar()
|
||||
@nav.goHome()
|
||||
|
||||
setActiveThread: =>
|
||||
if @thread
|
||||
@nav.setActiveThread(@thread.get("id"))
|
||||
else
|
||||
@nav.goHome
|
||||
|
||||
showThread: (forum_name, thread_id) ->
|
||||
@thread = @discussion.get(thread_id)
|
||||
@thread.set("unread_comments_count", 0)
|
||||
@thread.set("read", true)
|
||||
@setActiveThread()
|
||||
@showMain()
|
||||
|
||||
showMain: =>
|
||||
if(@main)
|
||||
@main.cleanup()
|
||||
@main.undelegateEvents()
|
||||
unless($(".forum-content").is(":visible"))
|
||||
$(".forum-content").fadeIn()
|
||||
if(@newPost.is(":visible"))
|
||||
@newPost.fadeOut()
|
||||
|
||||
@main = new DiscussionThreadView(
|
||||
el: $(".forum-content"),
|
||||
model: @thread,
|
||||
mode: "tab",
|
||||
course_settings: @course_settings,
|
||||
)
|
||||
@main.render()
|
||||
@main.on "thread:responses:rendered", =>
|
||||
@nav.updateSidebar()
|
||||
@thread.on "thread:thread_type_updated", @showMain
|
||||
|
||||
navigateToThread: (thread_id) =>
|
||||
thread = @discussion.get(thread_id)
|
||||
@navigate("#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true)
|
||||
|
||||
navigateToAllThreads: =>
|
||||
@navigate("", trigger: true)
|
||||
|
||||
showNewPost: (event) =>
|
||||
$('.forum-content').fadeOut(
|
||||
duration: 200
|
||||
complete: =>
|
||||
@newPost.fadeIn(200).focus()
|
||||
)
|
||||
|
||||
hideNewPost: =>
|
||||
@newPost.fadeOut(
|
||||
duration: 200
|
||||
complete: =>
|
||||
$('.forum-content').fadeIn(200).find('.thread-wrapper').focus()
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
if Backbone?
|
||||
DiscussionApp =
|
||||
start: (elem)->
|
||||
# TODO: Perhaps eliminate usage of global variables when possible
|
||||
DiscussionUtil.loadRolesFromContainer()
|
||||
element = $(elem)
|
||||
window.$$course_id = element.data("course-id")
|
||||
window.courseName = element.data("course-name")
|
||||
user_info = element.data("user-info")
|
||||
sort_preference = element.data("sort-preference")
|
||||
threads = element.data("threads")
|
||||
thread_pages = element.data("thread-pages")
|
||||
content_info = element.data("content-info")
|
||||
user = new DiscussionUser(user_info)
|
||||
DiscussionUtil.setUser(user)
|
||||
window.user = user
|
||||
Content.loadContentInfos(content_info)
|
||||
discussion = new Discussion(threads, {pages: thread_pages, sort: sort_preference})
|
||||
course_settings = new DiscussionCourseSettings(element.data("course-settings"))
|
||||
new DiscussionRouter({discussion: discussion, course_settings: course_settings})
|
||||
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
|
||||
DiscussionProfileApp =
|
||||
start: (elem) ->
|
||||
# Roles are not included in user profile page, but they are not used for anything
|
||||
DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
|
||||
element = $(elem)
|
||||
window.$$course_id = element.data("course-id")
|
||||
threads = element.data("threads")
|
||||
user_info = element.data("user-info")
|
||||
window.user = new DiscussionUser(user_info)
|
||||
page = element.data("page")
|
||||
numPages = element.data("num-pages")
|
||||
new DiscussionUserProfileView(el: element, collection: threads, page: page, numPages: numPages)
|
||||
$ ->
|
||||
$("section.discussion").each (index, elem) ->
|
||||
DiscussionApp.start(elem)
|
||||
$("section.discussion-user-threads").each (index, elem) ->
|
||||
DiscussionProfileApp.start(elem)
|
||||
@@ -1,2 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionCourseSettings extends Backbone.Model
|
||||
@@ -1,15 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionUser extends Backbone.Model
|
||||
following: (thread) ->
|
||||
_.include(@get('subscribed_thread_ids'), thread.id)
|
||||
|
||||
voted: (thread) ->
|
||||
_.include(@get('upvoted_ids'), thread.id)
|
||||
|
||||
vote: (thread) ->
|
||||
@get('upvoted_ids').push(thread.id)
|
||||
thread.vote()
|
||||
|
||||
unvote: (thread) ->
|
||||
@set('upvoted_ids', _.without(@get('upvoted_ids'), thread.id))
|
||||
thread.unvote()
|
||||
@@ -1,346 +0,0 @@
|
||||
class @DiscussionUtil
|
||||
|
||||
@wmdEditors: {}
|
||||
|
||||
@getTemplate: (id) ->
|
||||
$("script##{id}").html()
|
||||
|
||||
@setUser: (user) ->
|
||||
@user = user
|
||||
|
||||
@getUser: () ->
|
||||
@user
|
||||
|
||||
@loadRoles: (roles)->
|
||||
@roleIds = roles
|
||||
|
||||
@loadRolesFromContainer: ->
|
||||
@loadRoles($("#discussion-container").data("roles"))
|
||||
|
||||
@isStaff: (user_id) ->
|
||||
user_id ?= @user?.id
|
||||
staff = _.union(@roleIds['Moderator'], @roleIds['Administrator'])
|
||||
_.include(staff, parseInt(user_id))
|
||||
|
||||
@isTA: (user_id) ->
|
||||
user_id ?= @user?.id
|
||||
ta = _.union(@roleIds['Community TA'])
|
||||
_.include(ta, parseInt(user_id))
|
||||
|
||||
@isPrivilegedUser: (user_id) ->
|
||||
@isStaff(user_id) || @isTA(user_id)
|
||||
|
||||
@bulkUpdateContentInfo: (infos) ->
|
||||
for id, info of infos
|
||||
Content.getContent(id).updateInfo(info)
|
||||
|
||||
@generateDiscussionLink: (cls, txt, handler) ->
|
||||
$("<a>").addClass("discussion-link")
|
||||
.attr("href", "javascript:void(0)")
|
||||
.addClass(cls).html(txt)
|
||||
.click -> handler(this)
|
||||
|
||||
@urlFor: (name, param, param1, param2) ->
|
||||
{
|
||||
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
|
||||
unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow"
|
||||
create_thread : "/courses/#{$$course_id}/discussion/#{param}/threads/create"
|
||||
update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update"
|
||||
create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply"
|
||||
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
|
||||
flagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/flagAbuse"
|
||||
unFlagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unFlagAbuse"
|
||||
flagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/flagAbuse"
|
||||
unFlagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unFlagAbuse"
|
||||
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
|
||||
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
|
||||
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
|
||||
un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin"
|
||||
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
|
||||
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
|
||||
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
|
||||
update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update"
|
||||
endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse"
|
||||
create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply"
|
||||
delete_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/delete"
|
||||
upvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/upvote"
|
||||
downvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/downvote"
|
||||
undo_vote_for_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unvote"
|
||||
upload : "/courses/#{$$course_id}/discussion/upload"
|
||||
users : "/courses/#{$$course_id}/discussion/users"
|
||||
search : "/courses/#{$$course_id}/discussion/forum/search"
|
||||
retrieve_discussion : "/courses/#{$$course_id}/discussion/forum/#{param}/inline"
|
||||
retrieve_single_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
|
||||
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
|
||||
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
|
||||
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
|
||||
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
|
||||
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
|
||||
threads : "/courses/#{$$course_id}/discussion/forum"
|
||||
"enable_notifications" : "/notification_prefs/enable/"
|
||||
"disable_notifications" : "/notification_prefs/disable/"
|
||||
"notifications_status" : "/notification_prefs/status/"
|
||||
}[name]
|
||||
|
||||
@ignoreEnterKey: (event) =>
|
||||
if event.which == 13
|
||||
event.preventDefault()
|
||||
|
||||
@activateOnSpace: (event, func) ->
|
||||
if event.which == 32
|
||||
event.preventDefault()
|
||||
func(event)
|
||||
|
||||
@makeFocusTrap: (elem) ->
|
||||
elem.keydown(
|
||||
(event) ->
|
||||
if event.which == 9 # Tab
|
||||
event.preventDefault()
|
||||
)
|
||||
|
||||
@showLoadingIndicator: (element, takeFocus) ->
|
||||
@$_loading = $("<div class='loading-animation' tabindex='0'><span class='sr'>" + gettext("Loading content") + "</span></div>")
|
||||
element.after(@$_loading)
|
||||
if takeFocus
|
||||
@makeFocusTrap(@$_loading)
|
||||
@$_loading.focus()
|
||||
|
||||
@hideLoadingIndicator: () ->
|
||||
@$_loading.remove()
|
||||
|
||||
@discussionAlert: (header, body) ->
|
||||
if $("#discussion-alert").length == 0
|
||||
alertDiv = $("<div class='modal' role='alertdialog' id='discussion-alert' aria-describedby='discussion-alert-message'/>").css("display", "none")
|
||||
alertDiv.html(
|
||||
"<div class='inner-wrapper discussion-alert-wrapper'>" +
|
||||
" <button class='close-modal dismiss' title='" + gettext("Close") + "'><span class='icon fa fa-times' aria-hidden='true'></span></button>" +
|
||||
" <header><h2/><hr/></header>" +
|
||||
" <p id='discussion-alert-message'/>" +
|
||||
" <hr/>" +
|
||||
" <button class='dismiss'>" + gettext("OK") + "</button>" +
|
||||
"</div>"
|
||||
)
|
||||
@makeFocusTrap(alertDiv.find("button"))
|
||||
alertTrigger = $("<a href='#discussion-alert' id='discussion-alert-trigger'/>").css("display", "none")
|
||||
alertTrigger.leanModal({closeButton: "#discussion-alert .dismiss", overlay: 1, top: 200})
|
||||
$("body").append(alertDiv).append(alertTrigger)
|
||||
$("#discussion-alert header h2").html(header)
|
||||
$("#discussion-alert p").html(body)
|
||||
$("#discussion-alert-trigger").click()
|
||||
$("#discussion-alert button").focus()
|
||||
|
||||
@safeAjax: (params) ->
|
||||
$elem = params.$elem
|
||||
|
||||
if $elem and $elem.attr("disabled")
|
||||
deferred = $.Deferred()
|
||||
deferred.reject()
|
||||
return deferred.promise()
|
||||
|
||||
params["url"] = URI(params["url"]).addSearch ajax: 1
|
||||
params["beforeSend"] = =>
|
||||
if $elem
|
||||
$elem.attr("disabled", "disabled")
|
||||
if params["$loading"]
|
||||
if params["loadingCallback"]?
|
||||
params["loadingCallback"].apply(params["$loading"])
|
||||
else
|
||||
@showLoadingIndicator($(params["$loading"]), params["takeFocus"])
|
||||
if !params["error"]
|
||||
params["error"] = =>
|
||||
@discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble processing your request. Please ensure you have copied any unsaved work and then reload the page.")
|
||||
)
|
||||
request = $.ajax(params).always =>
|
||||
if $elem
|
||||
$elem.removeAttr("disabled")
|
||||
if params["$loading"]
|
||||
if params["loadedCallback"]?
|
||||
params["loadedCallback"].apply(params["$loading"])
|
||||
else
|
||||
@hideLoadingIndicator()
|
||||
return request
|
||||
|
||||
@updateWithUndo: (model, updates, safeAjaxParams, errorMsg) ->
|
||||
if errorMsg
|
||||
safeAjaxParams.error = => @discussionAlert(gettext("Sorry"), errorMsg)
|
||||
undo = _.pick(model.attributes, _.keys(updates))
|
||||
model.set(updates)
|
||||
@safeAjax(safeAjaxParams).fail(() -> model.set(undo))
|
||||
|
||||
@bindLocalEvents: ($local, eventsHandler) ->
|
||||
for eventSelector, handler of eventsHandler
|
||||
[event, selector] = eventSelector.split(' ')
|
||||
$local(selector).unbind(event)[event] handler
|
||||
|
||||
@formErrorHandler: (errorsField) ->
|
||||
(xhr, textStatus, error) ->
|
||||
makeErrorElem = (message) ->
|
||||
$("<li>").addClass("post-error").html(message)
|
||||
errorsField.empty().show()
|
||||
if xhr.status == 400
|
||||
response = JSON.parse(xhr.responseText)
|
||||
if response.errors? and response.errors.length > 0
|
||||
for error in response.errors
|
||||
errorsField.append(makeErrorElem(error))
|
||||
else
|
||||
errorsField.append(
|
||||
makeErrorElem(
|
||||
gettext("We had some trouble processing your request. Please try again.")
|
||||
)
|
||||
)
|
||||
|
||||
@clearFormErrors: (errorsField) ->
|
||||
errorsField.empty()
|
||||
|
||||
@postMathJaxProcessor: (text) ->
|
||||
RE_INLINEMATH = /^\$([^\$]*)\$/g
|
||||
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
|
||||
@processEachMathAndCode text, (s, type) ->
|
||||
if type == 'display'
|
||||
s.replace RE_DISPLAYMATH, ($0, $1) ->
|
||||
"\\[" + $1 + "\\]"
|
||||
else if type == 'inline'
|
||||
s.replace RE_INLINEMATH, ($0, $1) ->
|
||||
"\\(" + $1 + "\\)"
|
||||
else
|
||||
s
|
||||
|
||||
@makeWmdEditor: ($content, $local, cls_identifier) ->
|
||||
elem = $local(".#{cls_identifier}")
|
||||
placeholder = elem.data('placeholder')
|
||||
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
|
||||
appended_id = "-#{cls_identifier}-#{id}"
|
||||
imageUploadUrl = @urlFor('upload')
|
||||
_processor = (_this) ->
|
||||
(text) -> _this.postMathJaxProcessor(text)
|
||||
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
|
||||
@wmdEditors["#{cls_identifier}-#{id}"] = editor
|
||||
if placeholder?
|
||||
elem.find("#wmd-input#{appended_id}").attr('placeholder', placeholder)
|
||||
editor
|
||||
|
||||
@getWmdEditor: ($content, $local, cls_identifier) ->
|
||||
elem = $local(".#{cls_identifier}")
|
||||
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
|
||||
@wmdEditors["#{cls_identifier}-#{id}"]
|
||||
|
||||
@getWmdInput: ($content, $local, cls_identifier) ->
|
||||
elem = $local(".#{cls_identifier}")
|
||||
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
|
||||
$local("#wmd-input-#{cls_identifier}-#{id}")
|
||||
|
||||
@getWmdContent: ($content, $local, cls_identifier) ->
|
||||
@getWmdInput($content, $local, cls_identifier).val()
|
||||
|
||||
@setWmdContent: ($content, $local, cls_identifier, text) ->
|
||||
@getWmdInput($content, $local, cls_identifier).val(text)
|
||||
@getWmdEditor($content, $local, cls_identifier).refreshPreview()
|
||||
|
||||
@processEachMathAndCode: (text, processor) ->
|
||||
|
||||
codeArchive = []
|
||||
|
||||
RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m
|
||||
RE_INLINEMATH = /^([^\$]*?)\$([^\$]+?)\$(.*)$/m
|
||||
|
||||
ESCAPED_DOLLAR = '@@ESCAPED_D@@'
|
||||
ESCAPED_BACKSLASH = '@@ESCAPED_B@@'
|
||||
|
||||
processedText = ""
|
||||
|
||||
$div = $("<div>").html(text)
|
||||
|
||||
$div.find("code").each (index, code) ->
|
||||
codeArchive.push $(code).html()
|
||||
$(code).html(codeArchive.length - 1)
|
||||
|
||||
text = $div.html()
|
||||
text = text.replace /\\\$/g, ESCAPED_DOLLAR
|
||||
|
||||
while true
|
||||
if RE_INLINEMATH.test(text)
|
||||
text = text.replace RE_INLINEMATH, ($0, $1, $2, $3) ->
|
||||
processedText += $1 + processor("$" + $2 + "$", 'inline')
|
||||
$3
|
||||
else if RE_DISPLAYMATH.test(text)
|
||||
text = text.replace RE_DISPLAYMATH, ($0, $1, $2, $3) ->
|
||||
#processedText += $1 + processor("$$" + $2 + "$$", 'display')
|
||||
#bug fix, ordering is off
|
||||
processedText = processor("$$" + $2 + "$$", 'display') + processedText
|
||||
processedText = $1 + processedText
|
||||
$3
|
||||
else
|
||||
processedText += text
|
||||
break
|
||||
|
||||
text = processedText
|
||||
text = text.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$')
|
||||
|
||||
text = text.replace /\\\\\\\\/g, ESCAPED_BACKSLASH
|
||||
text = text.replace /\\begin\{([a-z]*\*?)\}([\s\S]*?)\\end\{\1\}/img, ($0, $1, $2) ->
|
||||
processor("\\begin{#{$1}}" + $2 + "\\end{#{$1}}")
|
||||
text = text.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\')
|
||||
|
||||
$div = $("<div>").html(text)
|
||||
cnt = 0
|
||||
$div.find("code").each (index, code) ->
|
||||
$(code).html(processor(codeArchive[cnt], 'code'))
|
||||
cnt += 1
|
||||
|
||||
text = $div.html()
|
||||
|
||||
text
|
||||
|
||||
@unescapeHighlightTag: (text) ->
|
||||
text.replace(/\<\;highlight\>\;/g, "<span class='search-highlight'>")
|
||||
.replace(/\<\;\/highlight\>\;/g, "</span>")
|
||||
|
||||
@stripHighlight: (text) ->
|
||||
text.replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "")
|
||||
.replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, "")
|
||||
|
||||
@stripLatexHighlight: (text) ->
|
||||
@processEachMathAndCode text, @stripHighlight
|
||||
|
||||
@markdownWithHighlight: (text) ->
|
||||
text = text.replace(/^\>\;/gm, ">")
|
||||
converter = Markdown.getMathCompatibleConverter()
|
||||
text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
|
||||
return text.replace(/^>/gm,">")
|
||||
|
||||
@abbreviateString: (text, minLength) ->
|
||||
# Abbreviates a string to at least minLength characters, stopping at word boundaries
|
||||
if text.length<minLength
|
||||
return text
|
||||
else
|
||||
while minLength < text.length && text[minLength] != ' '
|
||||
minLength++
|
||||
return text.substr(0, minLength) + gettext('…')
|
||||
|
||||
@abbreviateHTML: (html, minLength) ->
|
||||
# Abbreviates the html to at least minLength characters, stopping at word boundaries
|
||||
truncated_text = jQuery.truncate(html, {length: minLength, noBreaks: true, ellipsis: gettext('…')})
|
||||
$result = $("<div>" + truncated_text + "</div>")
|
||||
imagesToReplace = $result.find("img:not(:first)")
|
||||
if imagesToReplace.length > 0
|
||||
$result.append("<p><em>Some images in this post have been omitted</em></p>")
|
||||
imagesToReplace.replaceWith("<em>image omitted</em>")
|
||||
$result.html()
|
||||
|
||||
@getPaginationParams: (curPage, numPages, pageUrlFunc) =>
|
||||
delta = 2
|
||||
minPage = Math.max(curPage - delta, 1)
|
||||
maxPage = Math.min(curPage + delta, numPages)
|
||||
pageInfo = (pageNum) -> {number: pageNum, url: pageUrlFunc(pageNum)}
|
||||
params =
|
||||
page: curPage
|
||||
lowPages: _.range(minPage, curPage).map(pageInfo)
|
||||
highPages: _.range(curPage+1, maxPage+1).map(pageInfo)
|
||||
previous: if curPage > 1 then pageInfo(curPage - 1) else null
|
||||
next: if curPage < numPages then pageInfo(curPage + 1) else null
|
||||
leftdots: minPage > 2
|
||||
rightdots: maxPage < numPages-1
|
||||
first: if minPage > 1 then pageInfo(1) else null
|
||||
last: if maxPage < numPages then pageInfo(numPages) else null
|
||||
@@ -1,303 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionContentView extends Backbone.View
|
||||
|
||||
|
||||
events:
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
"keydown .discussion-flag-abuse":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
|
||||
|
||||
attrRenderer:
|
||||
ability: (ability) ->
|
||||
for action, selector of @abilityRenderer
|
||||
if not ability[action]
|
||||
selector.disable.apply(@)
|
||||
else
|
||||
selector.enable.apply(@)
|
||||
|
||||
abilityRenderer:
|
||||
editable:
|
||||
enable: -> @$(".action-edit").closest(".actions-item").removeClass("is-hidden")
|
||||
disable: -> @$(".action-edit").closest(".actions-item").addClass("is-hidden")
|
||||
can_delete:
|
||||
enable: -> @$(".action-delete").closest(".actions-item").removeClass("is-hidden")
|
||||
disable: -> @$(".action-delete").closest(".actions-item").addClass("is-hidden")
|
||||
can_openclose:
|
||||
enable: ->
|
||||
_.each(
|
||||
[".action-close", ".action-pin"],
|
||||
(selector) => @$(selector).closest(".actions-item").removeClass("is-hidden")
|
||||
)
|
||||
disable: ->
|
||||
_.each(
|
||||
[".action-close", ".action-pin"],
|
||||
(selector) => @$(selector).closest(".actions-item").addClass("is-hidden")
|
||||
)
|
||||
can_report:
|
||||
enable: -> @$(".action-report").closest(".actions-item").removeClass("is-hidden")
|
||||
disable: -> @$(".action-report").closest(".actions-item").addClass("is-hidden")
|
||||
can_vote:
|
||||
enable: -> @$(".action-vote").closest(".actions-item").removeClass("is-hidden")
|
||||
disable: -> @$(".action-vote").closest(".actions-item").addClass("is-hidden")
|
||||
|
||||
renderPartialAttrs: ->
|
||||
for attr, value of @model.changedAttributes()
|
||||
if @attrRenderer[attr]
|
||||
@attrRenderer[attr].apply(@, [value])
|
||||
|
||||
renderAttrs: ->
|
||||
for attr, value of @model.attributes
|
||||
if @attrRenderer[attr]
|
||||
@attrRenderer[attr].apply(@, [value])
|
||||
|
||||
makeWmdEditor: (cls_identifier) =>
|
||||
if not @$el.find(".wmd-panel").length
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), cls_identifier
|
||||
|
||||
getWmdEditor: (cls_identifier) =>
|
||||
DiscussionUtil.getWmdEditor @$el, $.proxy(@$, @), cls_identifier
|
||||
|
||||
getWmdContent: (cls_identifier) =>
|
||||
DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), cls_identifier
|
||||
|
||||
setWmdContent: (cls_identifier, text) =>
|
||||
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), cls_identifier, text
|
||||
|
||||
|
||||
initialize: ->
|
||||
@model.bind('change', @renderPartialAttrs, @)
|
||||
@listenTo(@model, "change:endorsed", =>
|
||||
if @model instanceof Comment
|
||||
@trigger("comment:endorse")
|
||||
)
|
||||
|
||||
class @DiscussionContentShowView extends DiscussionContentView
|
||||
events:
|
||||
_.reduce(
|
||||
[
|
||||
[".action-follow", "toggleFollow"],
|
||||
[".action-answer", "toggleEndorse"],
|
||||
[".action-endorse", "toggleEndorse"],
|
||||
[".action-vote", "toggleVote"],
|
||||
[".action-more", "toggleSecondaryActions"],
|
||||
[".action-pin", "togglePin"],
|
||||
[".action-edit", "edit"],
|
||||
[".action-delete", "_delete"],
|
||||
[".action-report", "toggleReport"],
|
||||
[".action-close", "toggleClose"],
|
||||
],
|
||||
(obj, event) =>
|
||||
selector = event[0]
|
||||
funcName = event[1]
|
||||
obj["click #{selector}"] = (event) -> @[funcName](event)
|
||||
obj["keydown #{selector}"] = (event) -> DiscussionUtil.activateOnSpace(event, @[funcName])
|
||||
obj
|
||||
,
|
||||
{}
|
||||
)
|
||||
|
||||
updateButtonState: (selector, checked) =>
|
||||
$button = @$(selector)
|
||||
$button.toggleClass("is-checked", checked)
|
||||
$button.attr("aria-checked", checked)
|
||||
|
||||
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
subscribed: (subscribed) ->
|
||||
@updateButtonState(".action-follow", subscribed)
|
||||
|
||||
endorsed: (endorsed) ->
|
||||
selector = if @model.get("thread").get("thread_type") == "question" then ".action-answer" else ".action-endorse"
|
||||
@updateButtonState(selector, endorsed)
|
||||
$button = @$(selector)
|
||||
$button.closest(".actions-item").toggleClass("is-hidden", not @model.canBeEndorsed())
|
||||
$button.toggleClass("is-checked", endorsed)
|
||||
|
||||
votes: (votes) ->
|
||||
selector = ".action-vote"
|
||||
@updateButtonState(selector, window.user.voted(@model))
|
||||
button = @$el.find(selector)
|
||||
numVotes = votes.up_count
|
||||
button.find(".js-sr-vote-count").html(
|
||||
interpolate(
|
||||
ngettext("there is currently %(numVotes)s vote", "there are currently %(numVotes)s votes", numVotes),
|
||||
{numVotes: numVotes},
|
||||
true
|
||||
)
|
||||
)
|
||||
votesHtml = interpolate(
|
||||
ngettext("%(numVotes)s Vote", "%(numVotes)s Votes", numVotes),
|
||||
{numVotes: numVotes},
|
||||
true
|
||||
)
|
||||
button.find(".vote-count").html(votesHtml)
|
||||
@$el.find('.display-vote .vote-count').html(votesHtml)
|
||||
|
||||
pinned: (pinned) ->
|
||||
@updateButtonState(".action-pin", pinned)
|
||||
@$(".post-label-pinned").toggleClass("is-hidden", not pinned)
|
||||
|
||||
abuse_flaggers: (abuse_flaggers) ->
|
||||
flagged = @model.isFlagged()
|
||||
@updateButtonState(".action-report", flagged)
|
||||
@$(".post-label-reported").toggleClass("is-hidden", not flagged)
|
||||
|
||||
closed: (closed) ->
|
||||
@updateButtonState(".action-close", closed)
|
||||
@$(".post-label-closed").toggleClass("is-hidden", not closed)
|
||||
@$(".display-vote").toggle(closed)
|
||||
})
|
||||
|
||||
toggleSecondaryActions: (event) =>
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@secondaryActionsExpanded = !@secondaryActionsExpanded
|
||||
@$(".action-more").toggleClass("is-expanded", @secondaryActionsExpanded)
|
||||
@$(".actions-dropdown").
|
||||
toggleClass("is-expanded", @secondaryActionsExpanded).
|
||||
attr("aria-expanded", @secondaryActionsExpanded)
|
||||
if @secondaryActionsExpanded
|
||||
if event.type == "keydown"
|
||||
@$(".action-list-item:first").focus()
|
||||
$("body").on("click", @toggleSecondaryActions)
|
||||
$("body").on("keydown", @handleSecondaryActionEscape)
|
||||
@$(".action-list-item").on("blur", @handleSecondaryActionBlur)
|
||||
else
|
||||
$("body").off("click", @toggleSecondaryActions)
|
||||
$("body").off("keydown", @handleSecondaryActionEscape)
|
||||
@$(".action-list-item").off("blur", @handleSecondaryActionBlur)
|
||||
|
||||
handleSecondaryActionEscape: (event) =>
|
||||
if event.keyCode == 27 # Esc
|
||||
@toggleSecondaryActions(event)
|
||||
@$(".action-more").focus()
|
||||
|
||||
handleSecondaryActionBlur: (event) =>
|
||||
setTimeout(
|
||||
=>
|
||||
if @secondaryActionsExpanded && @$(".actions-dropdown :focus").length == 0
|
||||
@toggleSecondaryActions(event)
|
||||
,
|
||||
10
|
||||
)
|
||||
|
||||
toggleFollow: (event) =>
|
||||
event.preventDefault()
|
||||
is_subscribing = not @model.get("subscribed")
|
||||
url = @model.urlFor(if is_subscribing then "follow" else "unfollow")
|
||||
if is_subscribing
|
||||
msg = gettext("We had some trouble subscribing you to this thread. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble unsubscribing you from this thread. Please try again.")
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
{"subscribed": is_subscribing},
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
toggleEndorse: (event) =>
|
||||
event.preventDefault()
|
||||
is_endorsing = not @model.get("endorsed")
|
||||
url = @model.urlFor("endorse")
|
||||
updates =
|
||||
endorsed: is_endorsing
|
||||
endorsement: if is_endorsing then {username: DiscussionUtil.getUser().get("username"), user_id: DiscussionUtil.getUser().id, time: new Date().toISOString()} else null
|
||||
if @model.get('thread').get('thread_type') == 'question'
|
||||
if is_endorsing
|
||||
msg = gettext("We had some trouble marking this response as an answer. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble removing this response as an answer. Please try again.")
|
||||
else
|
||||
if is_endorsing
|
||||
msg = gettext("We had some trouble marking this response endorsed. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble removing this endorsement. Please try again.")
|
||||
beforeFunc = () => @trigger("comment:endorse")
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
updates,
|
||||
{url: url, type: "POST", data: {endorsed: is_endorsing}, beforeSend: beforeFunc, $elem: $(event.currentTarget)},
|
||||
msg
|
||||
).always(@trigger("comment:endorse")) # ensures UI components get updated to the correct state when ajax completes
|
||||
|
||||
toggleVote: (event) =>
|
||||
event.preventDefault()
|
||||
user = DiscussionUtil.getUser()
|
||||
is_voting = not user.voted(@model)
|
||||
url = @model.urlFor(if is_voting then "upvote" else "unvote")
|
||||
updates =
|
||||
upvoted_ids: (if is_voting then _.union else _.difference)(user.get('upvoted_ids'), [@model.id])
|
||||
DiscussionUtil.updateWithUndo(
|
||||
user,
|
||||
updates,
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
gettext("We had some trouble saving your vote. Please try again.")
|
||||
).done(() => if is_voting then @model.vote() else @model.unvote())
|
||||
|
||||
togglePin: (event) =>
|
||||
event.preventDefault()
|
||||
is_pinning = not @model.get("pinned")
|
||||
url = @model.urlFor(if is_pinning then "pinThread" else "unPinThread")
|
||||
if is_pinning
|
||||
msg = gettext("We had some trouble pinning this thread. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble unpinning this thread. Please try again.")
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
{pinned: is_pinning},
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
toggleReport: (event) =>
|
||||
event.preventDefault()
|
||||
if @model.isFlagged()
|
||||
is_flagging = false
|
||||
msg = gettext("We had some trouble removing your flag on this post. Please try again.")
|
||||
else
|
||||
is_flagging = true
|
||||
msg = gettext("We had some trouble reporting this post. Please try again.")
|
||||
url = @model.urlFor(if is_flagging then "flagAbuse" else "unFlagAbuse")
|
||||
updates =
|
||||
abuse_flaggers: (if is_flagging then _.union else _.difference)(@model.get("abuse_flaggers"), [DiscussionUtil.getUser().id])
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
updates,
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
toggleClose: (event) =>
|
||||
event.preventDefault()
|
||||
is_closing = not @model.get('closed')
|
||||
if is_closing
|
||||
msg = gettext("We had some trouble closing this thread. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble reopening this thread. Please try again.")
|
||||
updates = {closed: is_closing}
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
updates,
|
||||
{url: @model.urlFor("close"), type: "POST", data: updates, $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
getAuthorDisplay: ->
|
||||
_.template($("#post-user-display-template").html())(
|
||||
username: @model.get('username') || null
|
||||
user_url: @model.get('user_url')
|
||||
is_community_ta: @model.get('community_ta_authored')
|
||||
is_staff: @model.get('staff_authored')
|
||||
)
|
||||
|
||||
getEndorserDisplay: ->
|
||||
endorsement = @model.get('endorsement')
|
||||
if endorsement and endorsement.username
|
||||
_.template($("#post-user-display-template").html())(
|
||||
username: endorsement.username
|
||||
user_url: DiscussionUtil.urlFor('user_profile', endorsement.user_id)
|
||||
is_community_ta: DiscussionUtil.isTA(endorsement.user_id)
|
||||
is_staff: DiscussionUtil.isStaff(endorsement.user_id)
|
||||
)
|
||||
else
|
||||
null
|
||||
@@ -1,527 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadListView extends Backbone.View
|
||||
events:
|
||||
"click .forum-nav-browse": "toggleBrowseMenu"
|
||||
"keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event)
|
||||
"keyup .forum-nav-browse-filter-input": "filterTopics"
|
||||
"click .forum-nav-browse-menu-wrapper": "ignoreClick"
|
||||
"click .forum-nav-browse-title": "selectTopicHandler"
|
||||
"keydown .forum-nav-search-input": "performSearch"
|
||||
"click .fa-search": "performSearch"
|
||||
"change .forum-nav-sort-control": "sortThreads"
|
||||
"click .forum-nav-thread-link": "threadSelected"
|
||||
"click .forum-nav-load-more-link": "loadMorePages"
|
||||
"change .forum-nav-filter-main-control": "chooseFilter"
|
||||
"change .forum-nav-filter-cohort-control": "chooseCohort"
|
||||
|
||||
initialize: (options) ->
|
||||
@courseSettings = options.courseSettings
|
||||
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
|
||||
@collection.on "change", @reloadDisplayedCollection
|
||||
@discussionIds=""
|
||||
@collection.on "reset", (discussion) =>
|
||||
board = $(".current-board").html()
|
||||
@displayedCollection.current_page = discussion.current_page
|
||||
@displayedCollection.pages = discussion.pages
|
||||
@displayedCollection.reset discussion.models
|
||||
# TODO: filter correctly
|
||||
# target = _.filter($("a.topic:contains('#{board}')"), (el) -> el.innerText == "General" || el.innerHTML == "General")
|
||||
# if target.length > 0
|
||||
# @filterTopic($.Event("filter", {'target': target[0]}))
|
||||
@collection.on "add", @addAndSelectThread
|
||||
@sidebar_padding = 10
|
||||
@boardName
|
||||
@template = _.template($("#thread-list-template").html())
|
||||
@current_search = ""
|
||||
@mode = 'all'
|
||||
|
||||
@searchAlertCollection = new Backbone.Collection([], {model: Backbone.Model})
|
||||
|
||||
@searchAlertCollection.on "add", (searchAlert) =>
|
||||
content = _.template(
|
||||
$("#search-alert-template").html())(
|
||||
{'message': searchAlert.attributes.message, 'cid': searchAlert.cid}
|
||||
)
|
||||
@$(".search-alerts").append(content)
|
||||
@$("#search-alert-" + searchAlert.cid + " a.dismiss").bind "click", searchAlert, (event) =>
|
||||
@removeSearchAlert(event.data.cid)
|
||||
|
||||
@searchAlertCollection.on "remove", (searchAlert) =>
|
||||
@$("#search-alert-" + searchAlert.cid).remove()
|
||||
|
||||
@searchAlertCollection.on "reset", =>
|
||||
@$(".search-alerts").empty()
|
||||
|
||||
addSearchAlert: (message) =>
|
||||
m = new Backbone.Model({"message": message})
|
||||
@searchAlertCollection.add(m)
|
||||
m
|
||||
|
||||
removeSearchAlert: (searchAlert) =>
|
||||
@searchAlertCollection.remove(searchAlert)
|
||||
|
||||
clearSearchAlerts: =>
|
||||
@searchAlertCollection.reset()
|
||||
|
||||
reloadDisplayedCollection: (thread) =>
|
||||
@clearSearchAlerts()
|
||||
thread_id = thread.get('id')
|
||||
content = @renderThread(thread)
|
||||
current_el = @$(".forum-nav-thread[data-id=#{thread_id}]")
|
||||
active = current_el.has(".forum-nav-thread-link.is-active").length != 0
|
||||
current_el.replaceWith(content)
|
||||
@showMetadataAccordingToSort()
|
||||
if active
|
||||
@setActiveThread(thread_id)
|
||||
|
||||
|
||||
#TODO fix this entire chain of events
|
||||
addAndSelectThread: (thread) =>
|
||||
commentable_id = thread.get("commentable_id")
|
||||
menuItem = @$(".forum-nav-browse-menu-item[data-discussion-id]").filter(-> $(this).data("discussion-id") == commentable_id)
|
||||
@setCurrentTopicDisplay(@getPathText(menuItem))
|
||||
@retrieveDiscussion commentable_id, =>
|
||||
@trigger "thread:created", thread.get('id')
|
||||
|
||||
updateSidebar: =>
|
||||
|
||||
scrollTop = $(window).scrollTop();
|
||||
windowHeight = $(window).height();
|
||||
|
||||
discussionBody = $(".discussion-column")
|
||||
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
|
||||
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
|
||||
|
||||
sidebar = $(".forum-nav")
|
||||
if scrollTop > discussionsBodyTop - @sidebar_padding
|
||||
sidebar.css('top', scrollTop - discussionsBodyTop + @sidebar_padding);
|
||||
else
|
||||
sidebar.css('top', '0');
|
||||
|
||||
sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding)
|
||||
|
||||
topOffset = scrollTop + windowHeight
|
||||
discussionBottomOffset = discussionsBodyBottom + @sidebar_padding
|
||||
amount = Math.max(topOffset - discussionBottomOffset, 0)
|
||||
|
||||
sidebarHeight = sidebarHeight - @sidebar_padding - amount
|
||||
sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight())
|
||||
sidebar.css 'height', sidebarHeight
|
||||
|
||||
headerHeight = @$(".forum-nav-header").outerHeight()
|
||||
refineBarHeight = @$(".forum-nav-refine-bar").outerHeight()
|
||||
browseFilterHeight = @$(".forum-nav-browse-filter").outerHeight()
|
||||
@$('.forum-nav-thread-list').css('height', (sidebarHeight - headerHeight - refineBarHeight - 2) + 'px')
|
||||
@$('.forum-nav-browse-menu').css('height', (sidebarHeight - headerHeight - browseFilterHeight - 2) + 'px')
|
||||
|
||||
|
||||
# Because we want the behavior that when the body is clicked the menu is
|
||||
# closed, we need to stop propagation of a click in any part of the menu
|
||||
# that is not a link.
|
||||
ignoreClick: (event) ->
|
||||
event.stopPropagation()
|
||||
|
||||
render: ->
|
||||
@timer = 0
|
||||
@$el.html(
|
||||
@template({
|
||||
isCohorted: @courseSettings.get("is_cohorted"),
|
||||
isPrivilegedUser: DiscussionUtil.isPrivilegedUser()
|
||||
})
|
||||
)
|
||||
@$(".forum-nav-sort-control option").removeProp("selected")
|
||||
@$(".forum-nav-sort-control option[value=#{@collection.sort_preference}]").prop("selected", true)
|
||||
|
||||
$(window).bind "load scroll resize", @updateSidebar
|
||||
|
||||
@displayedCollection.on "reset", @renderThreads
|
||||
@displayedCollection.on "thread:remove", @renderThreads
|
||||
@displayedCollection.on "change:commentable_id", (model, commentable_id) =>
|
||||
@retrieveDiscussions @discussionIds.split(",") if @mode is "commentables"
|
||||
@renderThreads()
|
||||
@
|
||||
|
||||
renderThreads: =>
|
||||
@$(".forum-nav-thread-list").html("")
|
||||
rendered = $("<div></div>")
|
||||
for thread in @displayedCollection.models
|
||||
content = @renderThread(thread)
|
||||
rendered.append content
|
||||
|
||||
@$(".forum-nav-thread-list").html(rendered.html())
|
||||
@showMetadataAccordingToSort()
|
||||
|
||||
@renderMorePages()
|
||||
@updateSidebar()
|
||||
@trigger "threads:rendered"
|
||||
|
||||
showMetadataAccordingToSort: () =>
|
||||
# Ensure that threads display metadata appropriate for the current sort
|
||||
voteCounts = @$(".forum-nav-thread-votes-count")
|
||||
commentCounts = @$(".forum-nav-thread-comments-count")
|
||||
voteCounts.hide()
|
||||
commentCounts.hide()
|
||||
switch @$(".forum-nav-sort-control").val()
|
||||
when "activity", "comments"
|
||||
commentCounts.show()
|
||||
when "votes"
|
||||
voteCounts.show()
|
||||
|
||||
renderMorePages: ->
|
||||
if @displayedCollection.hasMorePages()
|
||||
@$(".forum-nav-thread-list").append("<li class='forum-nav-load-more'><a href='#' class='forum-nav-load-more-link'>" + gettext("Load more") + "</a></li>")
|
||||
|
||||
getLoadingContent: (srText) ->
|
||||
return '<div class="forum-nav-loading" tabindex="0"><span class="icon fa fa-spinner fa-spin"/><span class="sr" role="alert">' + srText + '</span></div>'
|
||||
|
||||
loadMorePages: (event) =>
|
||||
if event
|
||||
event.preventDefault()
|
||||
loadMoreElem = @$(".forum-nav-load-more")
|
||||
loadMoreElem.html(@getLoadingContent(gettext("Loading more threads")))
|
||||
loadingElem = loadMoreElem.find(".forum-nav-loading")
|
||||
DiscussionUtil.makeFocusTrap(loadingElem)
|
||||
loadingElem.focus()
|
||||
options = {filter: @filter}
|
||||
switch @mode
|
||||
when 'search'
|
||||
options.search_text = @current_search
|
||||
if @group_id
|
||||
options.group_id = @group_id
|
||||
when 'followed'
|
||||
options.user_id = window.user.id
|
||||
when 'commentables'
|
||||
options.commentable_ids = @discussionIds
|
||||
if @group_id
|
||||
options.group_id = @group_id
|
||||
when 'all'
|
||||
if @group_id
|
||||
options.group_id = @group_id
|
||||
|
||||
lastThread = @collection.last()?.get('id')
|
||||
if lastThread
|
||||
# Pagination; focus the first thread after what was previously the last thread
|
||||
@once("threads:rendered", ->
|
||||
$(".forum-nav-thread[data-id='#{lastThread}'] + .forum-nav-thread .forum-nav-thread-link").focus()
|
||||
)
|
||||
else
|
||||
# Totally refreshing the list (e.g. from clicking a sort button); focus the first thread
|
||||
@once("threads:rendered", ->
|
||||
$(".forum-nav-thread-link").first()?.focus()
|
||||
)
|
||||
|
||||
error = =>
|
||||
@renderThreads()
|
||||
DiscussionUtil.discussionAlert(gettext("Sorry"), gettext("We had some trouble loading more threads. Please try again."))
|
||||
|
||||
@collection.retrieveAnotherPage(@mode, options, {sort_key: @$(".forum-nav-sort-control").val()}, error)
|
||||
|
||||
renderThread: (thread) =>
|
||||
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
|
||||
unreadCount = thread.get('unread_comments_count') + (if thread.get("read") then 0 else 1)
|
||||
if unreadCount > 0
|
||||
content.find('.forum-nav-thread-comments-count').attr(
|
||||
"data-tooltip",
|
||||
interpolate(
|
||||
ngettext('%(unread_count)s new comment', '%(unread_count)s new comments', unreadCount),
|
||||
{unread_count: unreadCount},
|
||||
true
|
||||
)
|
||||
)
|
||||
content
|
||||
|
||||
threadSelected: (e) =>
|
||||
# Use .attr('data-id') rather than .data('id') because .data does type
|
||||
# coercion. Usually, this is fine, but when Mongo gives an object id with
|
||||
# no letters, it casts it to a Number.
|
||||
|
||||
thread_id = $(e.target).closest(".forum-nav-thread").attr("data-id")
|
||||
@setActiveThread(thread_id)
|
||||
@trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above...
|
||||
false
|
||||
|
||||
threadRemoved: (thread_id) =>
|
||||
@trigger("thread:removed", thread_id)
|
||||
|
||||
setActiveThread: (thread_id) ->
|
||||
@$(".forum-nav-thread-link").find(".sr").remove()
|
||||
@$(".forum-nav-thread[data-id!='#{thread_id}'] .forum-nav-thread-link").removeClass("is-active")
|
||||
@$(".forum-nav-thread[data-id='#{thread_id}'] .forum-nav-thread-link").addClass("is-active").find(".forum-nav-thread-wrapper-1").prepend('<span class="sr">' + gettext("Current conversation") + '</span>')
|
||||
|
||||
goHome: ->
|
||||
@template = _.template($("#discussion-home-template").html())
|
||||
$(".forum-content").html(@template)
|
||||
$(".forum-nav-thread-list a").removeClass("is-active").find(".sr").remove()
|
||||
$("input.email-setting").bind "click", @updateEmailNotifications
|
||||
url = DiscussionUtil.urlFor("notifications_status",window.user.get("id"))
|
||||
DiscussionUtil.safeAjax
|
||||
url: url
|
||||
type: "GET"
|
||||
success: (response, textStatus) =>
|
||||
if response.status
|
||||
$('input.email-setting').attr('checked','checked')
|
||||
else
|
||||
$('input.email-setting').removeAttr('checked')
|
||||
thread_id = null
|
||||
@trigger("thread:removed")
|
||||
#select all threads
|
||||
|
||||
isBrowseMenuVisible: =>
|
||||
@$(".forum-nav-browse-menu-wrapper").is(":visible")
|
||||
|
||||
showBrowseMenu: =>
|
||||
if not @isBrowseMenuVisible()
|
||||
@$(".forum-nav-browse").addClass("is-active")
|
||||
@$(".forum-nav-browse-menu-wrapper").show()
|
||||
@$(".forum-nav-thread-list-wrapper").hide()
|
||||
$(".forum-nav-browse-filter-input").focus()
|
||||
$("body").bind "click", @hideBrowseMenu
|
||||
@updateSidebar()
|
||||
|
||||
hideBrowseMenu: =>
|
||||
if @isBrowseMenuVisible()
|
||||
@$(".forum-nav-browse").removeClass("is-active")
|
||||
@$(".forum-nav-browse-menu-wrapper").hide()
|
||||
@$(".forum-nav-thread-list-wrapper").show()
|
||||
$("body").unbind "click", @hideBrowseMenu
|
||||
@updateSidebar()
|
||||
|
||||
toggleBrowseMenu: (event) =>
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if @isBrowseMenuVisible()
|
||||
@hideBrowseMenu()
|
||||
else
|
||||
@showBrowseMenu()
|
||||
|
||||
# Given a menu item, get the text for it and its ancestors
|
||||
# (starting from the root, separated by " / ")
|
||||
getPathText: (item) ->
|
||||
path = item.parents(".forum-nav-browse-menu-item").andSelf()
|
||||
pathTitles = path.children(".forum-nav-browse-title").map((i, elem) -> $(elem).text()).get()
|
||||
pathText = pathTitles.join(" / ")
|
||||
|
||||
filterTopics: (event) =>
|
||||
query = $(event.target).val()
|
||||
items = @$(".forum-nav-browse-menu-item")
|
||||
if query.length == 0
|
||||
items.show()
|
||||
else
|
||||
# If all filter terms occur in the path to an item then that item and
|
||||
# all its descendants are displayed
|
||||
items.hide()
|
||||
items.each (i, item) =>
|
||||
item = $(item)
|
||||
if not item.is(":visible")
|
||||
pathText = @getPathText(item).toLowerCase()
|
||||
if query.split(" ").every((term) -> pathText.search(term.toLowerCase()) != -1)
|
||||
path = item.parents(".forum-nav-browse-menu-item").andSelf()
|
||||
path.add(item.find(".forum-nav-browse-menu-item")).show()
|
||||
|
||||
setCurrentTopicDisplay: (text) ->
|
||||
@$(".forum-nav-browse-current").text(@fitName(text))
|
||||
|
||||
getNameWidth: (name) ->
|
||||
test = $("<div>")
|
||||
test.css
|
||||
"font-size": @$(".forum-nav-browse-current").css('font-size')
|
||||
opacity: 0
|
||||
position: 'absolute'
|
||||
left: -1000
|
||||
top: -1000
|
||||
$("body").append(test)
|
||||
test.html(name)
|
||||
width = test.width()
|
||||
test.remove()
|
||||
return width
|
||||
|
||||
fitName: (name) ->
|
||||
@maxNameWidth = @$(".forum-nav-browse").width() -
|
||||
@$(".forum-nav-browse .icon").outerWidth(true) -
|
||||
@$(".forum-nav-browse-drop-arrow").outerWidth(true)
|
||||
width = @getNameWidth(name)
|
||||
if width < @maxNameWidth
|
||||
return name
|
||||
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
|
||||
prefix = ""
|
||||
while path.length > 1
|
||||
prefix = gettext("…") + "/"
|
||||
path.shift()
|
||||
partialName = prefix + path.join("/")
|
||||
if @getNameWidth(partialName) < @maxNameWidth
|
||||
return partialName
|
||||
rawName = path[0]
|
||||
name = prefix + rawName
|
||||
while @getNameWidth(name) > @maxNameWidth
|
||||
rawName = rawName[0...rawName.length-1]
|
||||
name = prefix + rawName + gettext("…")
|
||||
return name
|
||||
|
||||
selectTopicHandler: (event) ->
|
||||
event.preventDefault()
|
||||
@selectTopic $(event.target)
|
||||
|
||||
selectTopic: ($target) ->
|
||||
@hideBrowseMenu()
|
||||
@clearSearch()
|
||||
|
||||
item = $target.closest('.forum-nav-browse-menu-item')
|
||||
@setCurrentTopicDisplay(@getPathText(item))
|
||||
if item.hasClass("forum-nav-browse-menu-all")
|
||||
@discussionIds = ""
|
||||
@$('.forum-nav-filter-cohort').show()
|
||||
@retrieveAllThreads()
|
||||
else if item.hasClass("forum-nav-browse-menu-following")
|
||||
@retrieveFollowed()
|
||||
@$('.forum-nav-filter-cohort').hide()
|
||||
else
|
||||
allItems = item.find(".forum-nav-browse-menu-item").andSelf()
|
||||
discussionIds = allItems.filter("[data-discussion-id]").map(
|
||||
(i, elem) -> $(elem).data("discussion-id")
|
||||
).get()
|
||||
@retrieveDiscussions(discussionIds)
|
||||
@$(".forum-nav-filter-cohort").toggle(item.data('cohorted') == true)
|
||||
|
||||
chooseFilter: (event) =>
|
||||
@filter = $(".forum-nav-filter-main-control :selected").val()
|
||||
@retrieveFirstPage()
|
||||
|
||||
chooseCohort: (event) =>
|
||||
@group_id = @$('.forum-nav-filter-cohort-control :selected').val()
|
||||
@retrieveFirstPage()
|
||||
|
||||
retrieveDiscussion: (discussion_id, callback=null) ->
|
||||
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
|
||||
DiscussionUtil.safeAjax
|
||||
url: url
|
||||
type: "GET"
|
||||
success: (response, textStatus) =>
|
||||
@collection.current_page = response.page
|
||||
@collection.pages = response.num_pages
|
||||
@collection.reset(response.discussion_data)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@displayedCollection.reset(@collection.models)# Don't think this is necessary because it's called on collection.reset
|
||||
if callback?
|
||||
callback()
|
||||
|
||||
|
||||
retrieveDiscussions: (discussion_ids) ->
|
||||
@discussionIds = discussion_ids.join(',')
|
||||
@mode = 'commentables'
|
||||
@retrieveFirstPage()
|
||||
|
||||
retrieveAllThreads: () ->
|
||||
@mode = 'all'
|
||||
@retrieveFirstPage()
|
||||
|
||||
retrieveFirstPage: (event)->
|
||||
@collection.current_page = 0
|
||||
@collection.reset()
|
||||
@loadMorePages(event)
|
||||
|
||||
sortThreads: (event) ->
|
||||
@displayedCollection.setSortComparator(@$(".forum-nav-sort-control").val())
|
||||
|
||||
@retrieveFirstPage(event)
|
||||
|
||||
performSearch: (event) ->
|
||||
#event.which 13 represent the Enter button
|
||||
if event.which == 13 or event.type == 'click'
|
||||
event.preventDefault()
|
||||
@hideBrowseMenu()
|
||||
@setCurrentTopicDisplay(gettext("Search Results"))
|
||||
text = @$(".forum-nav-search-input").val()
|
||||
@searchFor(text)
|
||||
|
||||
searchFor: (text) ->
|
||||
@clearSearchAlerts()
|
||||
@clearFilters()
|
||||
@mode = 'search'
|
||||
@current_search = text
|
||||
url = DiscussionUtil.urlFor("search")
|
||||
#TODO: This might be better done by setting discussion.current_page=0 and calling discussion.loadMorePages
|
||||
# Mainly because this currently does not reset any pagination variables which could cause problems.
|
||||
# This doesn't use pagination either.
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".forum-nav-search-input")
|
||||
data: { text: text }
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: 'json'
|
||||
$loading: $
|
||||
loadingCallback: =>
|
||||
@$(".forum-nav-thread-list").html("<li class='forum-nav-load-more'>" + @getLoadingContent(gettext("Loading thread list")) + "</li>")
|
||||
loadedCallback: =>
|
||||
@$(".forum-nav-thread-list .forum-nav-load-more").remove()
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
# TODO: Augment existing collection?
|
||||
@collection.reset(response.discussion_data)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@collection.current_page = response.page
|
||||
@collection.pages = response.num_pages
|
||||
if !_.isNull response.corrected_text
|
||||
message = interpolate(
|
||||
_.escape(gettext('No results found for %(original_query)s. Showing results for %(suggested_query)s.')),
|
||||
{"original_query": "<em>" + _.escape(text) + "</em>", "suggested_query": "<em>" + response.corrected_text + "</em>"},
|
||||
true
|
||||
)
|
||||
@addSearchAlert(message)
|
||||
else if response.discussion_data.length == 0
|
||||
@addSearchAlert(gettext('No threads matched your query.'))
|
||||
# TODO: Perhaps reload user info so that votes can be updated.
|
||||
# In the future we might not load all of a user's votes at once
|
||||
# so this would probably be necessary anyway
|
||||
@displayedCollection.reset(@collection.models) # Don't think this is necessary
|
||||
@searchForUser(text) if text
|
||||
|
||||
|
||||
searchForUser: (text) ->
|
||||
DiscussionUtil.safeAjax
|
||||
data: { username: text }
|
||||
url: DiscussionUtil.urlFor("users")
|
||||
type: "GET"
|
||||
dataType: 'json'
|
||||
error: =>
|
||||
return
|
||||
success: (response) =>
|
||||
if response.users.length > 0
|
||||
message = interpolate(
|
||||
_.escape(gettext('Show posts by %(username)s.')),
|
||||
{"username":
|
||||
_.template('<a class="link-jump" href="<%= url %>"><%- username %></a>')({
|
||||
url: DiscussionUtil.urlFor("user_profile", response.users[0].id),
|
||||
username: response.users[0].username
|
||||
})
|
||||
},
|
||||
true
|
||||
)
|
||||
@addSearchAlert(message)
|
||||
|
||||
clearSearch: ->
|
||||
@$(".forum-nav-search-input").val("")
|
||||
@current_search = ""
|
||||
@clearSearchAlerts()
|
||||
|
||||
clearFilters: ->
|
||||
@$(".forum-nav-filter-main-control").val("all")
|
||||
@$(".forum-nav-filter-cohort-control").val("all")
|
||||
|
||||
retrieveFollowed: () =>
|
||||
@mode = 'followed'
|
||||
@retrieveFirstPage()
|
||||
|
||||
updateEmailNotifications: () =>
|
||||
if $('input.email-setting').attr('checked')
|
||||
DiscussionUtil.safeAjax
|
||||
url: DiscussionUtil.urlFor("enable_notifications")
|
||||
type: "POST"
|
||||
error: () =>
|
||||
$('input.email-setting').removeAttr('checked')
|
||||
else
|
||||
DiscussionUtil.safeAjax
|
||||
url: DiscussionUtil.urlFor("disable_notifications")
|
||||
type: "POST"
|
||||
error: () =>
|
||||
$('input.email-setting').attr('checked','checked')
|
||||
@@ -1,21 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadProfileView extends Backbone.View
|
||||
render: ->
|
||||
@convertMath()
|
||||
@abbreviateBody()
|
||||
params = $.extend(@model.toJSON(),{permalink: @model.urlFor('retrieve')})
|
||||
if not @model.get('anonymous')
|
||||
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
|
||||
@$el.html(_.template($("#profile-thread-template").html())(params))
|
||||
@$("span.timeago").timeago()
|
||||
element = @$(".post-body")
|
||||
if MathJax?
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
@model.set('markdownBody', DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight @model.get('body'))
|
||||
|
||||
abbreviateBody: ->
|
||||
abbreviated = DiscussionUtil.abbreviateHTML @model.get('markdownBody'), 140
|
||||
@model.set('abbreviatedBody', abbreviated)
|
||||
@@ -1,47 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadShowView extends DiscussionContentShowView
|
||||
initialize: (options) ->
|
||||
super()
|
||||
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
|
||||
if @mode not in ["tab", "inline"]
|
||||
throw new Error("invalid mode: " + @mode)
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-show-template").html())
|
||||
context = $.extend(
|
||||
{
|
||||
mode: @mode,
|
||||
flagged: @model.isFlagged(),
|
||||
author_display: @getAuthorDisplay(),
|
||||
cid: @model.cid,
|
||||
readOnly: $('.discussion-module').data('read-only')
|
||||
},
|
||||
@model.attributes,
|
||||
)
|
||||
@template(context)
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@$("span.timeago").timeago()
|
||||
@convertMath()
|
||||
@highlight @$(".post-body")
|
||||
@highlight @$("h1,h3")
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".post-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
|
||||
if MathJax?
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "thread:edit", event
|
||||
|
||||
_delete: (event) ->
|
||||
@trigger "thread:_delete", event
|
||||
|
||||
highlight: (el) ->
|
||||
if el.html()
|
||||
el.html(el.html().replace(/<mark>/g, "<mark>").replace(/<\/mark>/g, "</mark>"))
|
||||
@@ -1,358 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadView extends DiscussionContentView
|
||||
|
||||
INITIAL_RESPONSE_PAGE_SIZE = 25
|
||||
SUBSEQUENT_RESPONSE_PAGE_SIZE = 100
|
||||
|
||||
events:
|
||||
"click .discussion-submit-post": "submitComment"
|
||||
"click .add-response-btn": "scrollToAddResponse"
|
||||
"click .forum-thread-expand": "expand"
|
||||
"click .forum-thread-collapse": "collapse"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
isQuestion: ->
|
||||
@model.get("thread_type") == "question"
|
||||
|
||||
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"
|
||||
@options = _.extend({}, options)
|
||||
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) =>
|
||||
id = @model.get("id")
|
||||
@model = collection.get(id) if collection.get(id)
|
||||
|
||||
@createShowView()
|
||||
@responses = new Comments()
|
||||
@loadedResponses = false
|
||||
if @isQuestion()
|
||||
@markedAnswers = new Comments()
|
||||
|
||||
rerender: () ->
|
||||
if @showView?
|
||||
@showView.undelegateEvents()
|
||||
@undelegateEvents()
|
||||
@$el.empty()
|
||||
@initialize(
|
||||
mode: @mode
|
||||
model: @model
|
||||
el: @el
|
||||
course_settings: @options.course_settings
|
||||
topicId: @topicId
|
||||
)
|
||||
@render()
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-template").html())
|
||||
container = $("#discussion-container")
|
||||
if !container.length
|
||||
# inline discussion
|
||||
container = $(".discussion-module")
|
||||
templateData = _.extend(
|
||||
@model.toJSON(),
|
||||
readOnly: @readOnly,
|
||||
can_create_comment: container.data("user-create-comment")
|
||||
)
|
||||
@template(templateData)
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
|
||||
@renderShowView()
|
||||
@renderAttrs()
|
||||
|
||||
@$("span.timeago").timeago()
|
||||
@makeWmdEditor "reply-body"
|
||||
@renderAddResponseButton()
|
||||
@responses.on("add", (response) => @renderResponseToList(response, ".js-response-list", {}))
|
||||
if @isQuestion()
|
||||
@markedAnswers.on("add", (response) => @renderResponseToList(response, ".js-marked-answer-list", {collapseComments: true}))
|
||||
if @mode == "tab"
|
||||
# Without a delay, jQuery doesn't add the loading extension defined in
|
||||
# utils.coffee before safeAjax is invoked, which results in an error
|
||||
setTimeout((=> @loadInitialResponses()), 100)
|
||||
@$(".post-tools").hide()
|
||||
else # mode == "inline"
|
||||
@collapse()
|
||||
|
||||
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
closed: (closed) ->
|
||||
@$(".discussion-reply-new").toggle(not closed)
|
||||
@$('.comment-form').closest('li').toggle(not closed)
|
||||
@$(".action-vote").toggle(not closed)
|
||||
@$(".display-vote").toggle(closed)
|
||||
@renderAddResponseButton()
|
||||
})
|
||||
|
||||
expand: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@$el.addClass("expanded")
|
||||
@$el.find(".post-body").text(@model.get("body"))
|
||||
@showView.convertMath()
|
||||
@$el.find(".forum-thread-expand").hide()
|
||||
@$el.find(".forum-thread-collapse").show()
|
||||
@$el.find(".post-extended-content").show()
|
||||
if not @loadedResponses
|
||||
@loadInitialResponses()
|
||||
|
||||
collapse: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@$el.removeClass("expanded")
|
||||
@$el.find(".post-body").text(@getAbbreviatedBody())
|
||||
@showView.convertMath()
|
||||
@$el.find(".forum-thread-expand").show()
|
||||
@$el.find(".forum-thread-collapse").hide()
|
||||
@$el.find(".post-extended-content").hide()
|
||||
|
||||
getAbbreviatedBody: ->
|
||||
cached = @model.get("abbreviatedBody")
|
||||
if cached
|
||||
cached
|
||||
else
|
||||
abbreviated = DiscussionUtil.abbreviateString @model.get("body"), 140
|
||||
@model.set("abbreviatedBody", abbreviated)
|
||||
abbreviated
|
||||
|
||||
cleanup: ->
|
||||
if @responsesRequest?
|
||||
@responsesRequest.abort()
|
||||
|
||||
loadResponses: (responseLimit, elem, firstLoad) ->
|
||||
# takeFocus take the page focus to response loading element while responses are being fetched.
|
||||
# - When viewing in the Discussions tab, responses are loaded automatically, Do not scroll to the
|
||||
# element(TNL-1530)
|
||||
# - When viewing inline in courseware, user clicks 'expand' to open responses, Its ok to scroll to the
|
||||
# element (Default)
|
||||
takeFocus = if @mode == "tab" then false else true
|
||||
@responsesRequest = DiscussionUtil.safeAjax
|
||||
url: DiscussionUtil.urlFor('retrieve_single_thread', @model.get('commentable_id'), @model.id)
|
||||
data:
|
||||
resp_skip: @responses.size()
|
||||
resp_limit: responseLimit if responseLimit
|
||||
$elem: elem
|
||||
$loading: elem
|
||||
takeFocus: takeFocus
|
||||
complete: =>
|
||||
@responsesRequest = null
|
||||
success: (data, textStatus, xhr) =>
|
||||
Content.loadContentInfos(data['annotated_content_info'])
|
||||
if @isQuestion()
|
||||
@markedAnswers.add(data["content"]["endorsed_responses"])
|
||||
@responses.add(
|
||||
if @isQuestion()
|
||||
then data["content"]["non_endorsed_responses"]
|
||||
else data["content"]["children"]
|
||||
)
|
||||
@renderResponseCountAndPagination(
|
||||
if @isQuestion()
|
||||
then data["content"]["non_endorsed_resp_total"]
|
||||
else data["content"]["resp_total"]
|
||||
)
|
||||
@trigger "thread:responses:rendered"
|
||||
@loadedResponses = true
|
||||
@$el.find('.discussion-article[data-id="' + @model.id + '"]').focus() # Sends focus to the discussion once the thread loads
|
||||
error: (xhr, textStatus) =>
|
||||
return if textStatus == 'abort'
|
||||
|
||||
if xhr.status == 404
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("The thread you selected has been deleted. Please select another thread.")
|
||||
)
|
||||
else if firstLoad
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading responses. Please reload the page.")
|
||||
)
|
||||
else
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading more responses. Please try again.")
|
||||
)
|
||||
|
||||
loadInitialResponses: () ->
|
||||
@loadResponses(INITIAL_RESPONSE_PAGE_SIZE, @$el.find(".js-response-list"), true)
|
||||
|
||||
renderResponseCountAndPagination: (responseTotal) =>
|
||||
if @isQuestion() && @markedAnswers.length != 0
|
||||
responseCountFormat = ngettext(
|
||||
"%(numResponses)s other response",
|
||||
"%(numResponses)s other responses",
|
||||
responseTotal
|
||||
)
|
||||
else
|
||||
responseCountFormat = ngettext(
|
||||
"%(numResponses)s response",
|
||||
"%(numResponses)s responses",
|
||||
responseTotal
|
||||
)
|
||||
@$el.find(".response-count").html(
|
||||
interpolate(
|
||||
responseCountFormat,
|
||||
{numResponses: responseTotal},
|
||||
true
|
||||
)
|
||||
)
|
||||
responsePagination = @$el.find(".response-pagination")
|
||||
responsePagination.empty()
|
||||
if responseTotal > 0
|
||||
responsesRemaining = responseTotal - @responses.size()
|
||||
showingResponsesText =
|
||||
if responsesRemaining == 0
|
||||
gettext("Showing all responses")
|
||||
else
|
||||
interpolate(
|
||||
ngettext(
|
||||
"Showing first response",
|
||||
"Showing first %(numResponses)s responses",
|
||||
@responses.size()
|
||||
),
|
||||
{numResponses: @responses.size()},
|
||||
true
|
||||
)
|
||||
responsePagination.append($("<span>").addClass("response-display-count").html(
|
||||
_.escape(showingResponsesText)
|
||||
))
|
||||
if responsesRemaining > 0
|
||||
if responsesRemaining < SUBSEQUENT_RESPONSE_PAGE_SIZE
|
||||
responseLimit = null
|
||||
buttonText = gettext("Load all responses")
|
||||
else
|
||||
responseLimit = SUBSEQUENT_RESPONSE_PAGE_SIZE
|
||||
buttonText = interpolate(
|
||||
gettext("Load next %(numResponses)s responses"),
|
||||
{numResponses: responseLimit},
|
||||
true
|
||||
)
|
||||
loadMoreButton = $("<button>").addClass("load-response-button").html(
|
||||
_.escape(buttonText)
|
||||
)
|
||||
loadMoreButton.click((event) => @loadResponses(responseLimit, loadMoreButton))
|
||||
responsePagination.append(loadMoreButton)
|
||||
|
||||
renderResponseToList: (response, listSelector, options) =>
|
||||
response.set('thread', @model)
|
||||
view = new ThreadResponseView($.extend({model: response}, options))
|
||||
view.on "comment:add", @addComment
|
||||
view.on "comment:endorse", @endorseThread
|
||||
view.render()
|
||||
@$el.find(listSelector).append(view.el)
|
||||
view.afterInsert()
|
||||
|
||||
renderAddResponseButton: =>
|
||||
if @model.hasResponses() and @model.can('can_reply') and !@model.get('closed')
|
||||
@$el.find('div.add-response').show()
|
||||
else
|
||||
@$el.find('div.add-response').hide()
|
||||
|
||||
scrollToAddResponse: (event) ->
|
||||
event.preventDefault()
|
||||
form = $(event.target).parents('article.discussion-article').find('form.discussion-reply-new')
|
||||
$('html, body').scrollTop(form.offset().top)
|
||||
form.find('.wmd-panel textarea').focus()
|
||||
|
||||
addComment: =>
|
||||
@model.comment()
|
||||
|
||||
endorseThread: =>
|
||||
@model.set 'endorsed', @$el.find(".action-answer.is-checked").length > 0
|
||||
|
||||
submitComment: (event) ->
|
||||
event.preventDefault()
|
||||
url = @model.urlFor('reply')
|
||||
body = @getWmdContent("reply-body")
|
||||
return if not body.trim().length
|
||||
@setWmdContent("reply-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, abuse_flaggers:[], endorsed: false, user_id: window.user.get("id"))
|
||||
comment.set('thread', @model.get('thread'))
|
||||
@renderResponseToList(comment, ".js-response-list")
|
||||
@model.addComment()
|
||||
@renderAddResponseButton()
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
body: body
|
||||
success: (data, textStatus) =>
|
||||
comment.updateInfo(data.annotated_content_info)
|
||||
comment.set(data.content)
|
||||
|
||||
edit: (event) =>
|
||||
@createEditView()
|
||||
@renderEditView()
|
||||
|
||||
createEditView: () ->
|
||||
if @showView?
|
||||
@showView.undelegateEvents()
|
||||
@showView.$el.empty()
|
||||
@showView = null
|
||||
|
||||
@editView = new DiscussionThreadEditView(
|
||||
container: @$('.thread-content-wrapper')
|
||||
model: @model
|
||||
mode: @mode
|
||||
context: @context
|
||||
course_settings: @options.course_settings
|
||||
)
|
||||
@editView.bind "thread:updated thread:cancel_edit", @closeEditView
|
||||
@editView.bind "comment:endorse", @endorseThread
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$('.thread-content-wrapper'))
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderEditView: () ->
|
||||
@editView.render()
|
||||
|
||||
createShowView: () ->
|
||||
@showView = new DiscussionThreadShowView({model: @model, mode: @mode})
|
||||
@showView.bind "thread:_delete", @_delete
|
||||
@showView.bind "thread:edit", @edit
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
|
||||
closeEditView: (event) =>
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
@renderAttrs()
|
||||
# next call is necessary to re-render the post action controls after
|
||||
# submitting or cancelling a thread edit in inline mode.
|
||||
@$el.find(".post-extended-content").show()
|
||||
|
||||
# If you use "delete" here, it will compile down into JS that includes the
|
||||
# use of DiscussionThreadView.prototype.delete, and that will break IE8
|
||||
# because "delete" is a keyword. So, using an underscore to prevent that.
|
||||
_delete: (event) =>
|
||||
url = @model.urlFor('_delete')
|
||||
if not @model.can('can_delete')
|
||||
return
|
||||
if not confirm gettext("Are you sure you want to delete this post?")
|
||||
return
|
||||
@model.remove()
|
||||
@showView.undelegateEvents()
|
||||
@undelegateEvents()
|
||||
@$el.empty()
|
||||
$elem = $(event.target)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@@ -1,43 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionUserProfileView extends Backbone.View
|
||||
events:
|
||||
"click .discussion-paginator a": "changePage"
|
||||
|
||||
initialize: (options) ->
|
||||
super()
|
||||
@page = options.page
|
||||
@numPages = options.numPages
|
||||
@discussion = new Discussion()
|
||||
@discussion.on("reset", @render)
|
||||
@discussion.reset(@collection, {silent: false})
|
||||
|
||||
render: () =>
|
||||
@$el.html(_.template($("#user-profile-template").html())({threads: @discussion.models}))
|
||||
@discussion.map (thread) ->
|
||||
new DiscussionThreadProfileView(el: @$("article#thread_#{thread.id}"), model: thread).render()
|
||||
baseUri = URI(window.location).removeSearch("page")
|
||||
pageUrlFunc = (page) -> baseUri.clone().addSearch("page", page)
|
||||
paginationParams = DiscussionUtil.getPaginationParams(@page, @numPages, pageUrlFunc)
|
||||
@$el.find(".discussion-pagination").html(_.template($("#pagination-template").html())(paginationParams))
|
||||
|
||||
changePage: (event) ->
|
||||
event.preventDefault()
|
||||
url = $(event.target).attr("href")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$el
|
||||
$loading: $(event.target)
|
||||
takeFocus: true
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: "json"
|
||||
success: (response, textStatus, xhr) =>
|
||||
@page = response.page
|
||||
@numPages = response.num_pages
|
||||
@discussion.reset(response.discussion_data, {silent: false})
|
||||
history.pushState({}, "", url)
|
||||
$("html, body").animate({ scrollTop: 0 });
|
||||
error: =>
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the page you requested. Please try again.")
|
||||
)
|
||||
@@ -1,125 +0,0 @@
|
||||
if Backbone?
|
||||
class @NewPostView extends Backbone.View
|
||||
|
||||
initialize: (options) ->
|
||||
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
|
||||
if @mode not in ["tab", "inline"]
|
||||
throw new Error("invalid mode: " + @mode)
|
||||
@course_settings = options.course_settings
|
||||
@is_commentable_cohorted = options.is_commentable_cohorted
|
||||
@topicId = options.topicId
|
||||
|
||||
render: () ->
|
||||
context = _.clone(@course_settings.attributes)
|
||||
_.extend(context, {
|
||||
cohort_options: @getCohortOptions(),
|
||||
is_commentable_cohorted: @is_commentable_cohorted,
|
||||
mode: @mode,
|
||||
form_id: @mode + (if @topicId then "-" + @topicId else "")
|
||||
})
|
||||
@$el.html(_.template($("#new-post-template").html())(context))
|
||||
threadTypeTemplate = _.template($("#thread-type-template").html());
|
||||
if $('.js-group-select').is(':disabled')
|
||||
$('.group-selector-wrapper').addClass('disabled')
|
||||
@addField(threadTypeTemplate({form_id: _.uniqueId("form-")}));
|
||||
if @isTabMode()
|
||||
@topicView = new DiscussionTopicMenuView {
|
||||
topicId: @topicId
|
||||
course_settings: @course_settings
|
||||
}
|
||||
@topicView.on('thread:topic_change', @toggleGroupDropdown)
|
||||
@addField(@topicView.render())
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "js-post-body"
|
||||
|
||||
addField: (fieldView) ->
|
||||
@$('.forum-new-post-form-wrapper').append fieldView
|
||||
|
||||
isTabMode: () ->
|
||||
@mode is "tab"
|
||||
|
||||
getCohortOptions: () ->
|
||||
if @course_settings.get("is_cohorted") and DiscussionUtil.isPrivilegedUser()
|
||||
user_cohort_id = $("#discussion-container").data("user-cohort-id")
|
||||
_.map @course_settings.get("cohorts"), (cohort) ->
|
||||
{value: cohort.id, text: cohort.name, selected: cohort.id==user_cohort_id}
|
||||
else
|
||||
null
|
||||
|
||||
events:
|
||||
"submit .forum-new-post-form": "createPost"
|
||||
"change .post-option-input": "postOptionChange"
|
||||
"click .cancel": "cancel"
|
||||
"reset .forum-new-post-form": "updateStyles"
|
||||
|
||||
toggleGroupDropdown: ($target) ->
|
||||
if $target.data('cohorted')
|
||||
$('.js-group-select').prop('disabled', false);
|
||||
$('.group-selector-wrapper').removeClass('disabled')
|
||||
else
|
||||
$('.js-group-select').val('').prop('disabled', true);
|
||||
$('.group-selector-wrapper').addClass('disabled')
|
||||
|
||||
postOptionChange: (event) ->
|
||||
$target = $(event.target)
|
||||
$optionElem = $target.closest(".post-option")
|
||||
if $target.is(":checked")
|
||||
$optionElem.addClass("is-enabled")
|
||||
else
|
||||
$optionElem.removeClass("is-enabled")
|
||||
|
||||
createPost: (event) ->
|
||||
event.preventDefault()
|
||||
thread_type = @$(".post-type-input:checked").val()
|
||||
title = @$(".js-post-title").val()
|
||||
body = @$(".js-post-body").find(".wmd-input").val()
|
||||
group = @$(".js-group-select option:selected").attr("value")
|
||||
|
||||
anonymous = false || @$(".js-anon").is(":checked")
|
||||
anonymous_to_peers = false || @$(".js-anon-peers").is(":checked")
|
||||
follow = false || @$(".js-follow").is(":checked")
|
||||
|
||||
topicId = if @isTabMode() then @topicView.getCurrentTopicId() else @topicId
|
||||
url = DiscussionUtil.urlFor('create_thread', topicId)
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target) if event
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
thread_type: thread_type
|
||||
title: title
|
||||
body: body
|
||||
anonymous: anonymous
|
||||
anonymous_to_peers: anonymous_to_peers
|
||||
auto_subscribe: follow
|
||||
group_id: group
|
||||
error: DiscussionUtil.formErrorHandler(@$(".post-errors"))
|
||||
success: (response, textStatus) =>
|
||||
# TODO: Move this out of the callback, this makes it feel sluggish
|
||||
thread = new Thread response['content']
|
||||
@$el.hide()
|
||||
@resetForm()
|
||||
@collection.add thread
|
||||
|
||||
cancel: (event) ->
|
||||
event.preventDefault()
|
||||
if not confirm gettext("Your post will be discarded.")
|
||||
return
|
||||
@trigger('newPost:cancel')
|
||||
@resetForm()
|
||||
|
||||
resetForm: =>
|
||||
@$(".forum-new-post-form")[0].reset()
|
||||
DiscussionUtil.clearFormErrors(@$(".post-errors"))
|
||||
@$(".wmd-preview p").html("")
|
||||
if @isTabMode()
|
||||
@topicView.setTopic(@$("a.topic-title").first())
|
||||
|
||||
updateStyles: =>
|
||||
# form reset doesn't change the style of checkboxes so this event is to do that job
|
||||
setTimeout(
|
||||
(=> @$(".post-option-input").trigger("change")),
|
||||
1
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentEditView extends Backbone.View
|
||||
|
||||
events:
|
||||
"click .post-update": "update"
|
||||
"click .post-cancel": "cancel_edit"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#response-comment-edit-template").html())
|
||||
@$el.html(@template(@model.toJSON()))
|
||||
@delegateEvents()
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-comment-body"
|
||||
@
|
||||
|
||||
update: (event) ->
|
||||
@trigger "comment:update", event
|
||||
|
||||
cancel_edit: (event) ->
|
||||
@trigger "comment:cancel_edit", event
|
||||
@@ -1,44 +0,0 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentShowView extends DiscussionContentShowView
|
||||
tagName: "li"
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#response-comment-show-template").html())
|
||||
@$el.html(
|
||||
@template(
|
||||
_.extend(
|
||||
{
|
||||
cid: @model.cid,
|
||||
author_display: @getAuthorDisplay(),
|
||||
readOnly: $('.discussion-module').data('read-only')
|
||||
},
|
||||
@model.attributes
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@$el.find(".timeago").timeago()
|
||||
@convertMath()
|
||||
@addReplyLink()
|
||||
@
|
||||
|
||||
addReplyLink: () ->
|
||||
if @model.hasOwnProperty('parent')
|
||||
name = @model.parent.get('username') ? gettext("anonymous")
|
||||
html = "<a href='#comment_#{@model.parent.id}'>@#{name}</a>: "
|
||||
p = @$('.response-body p:first')
|
||||
p.prepend(html)
|
||||
|
||||
convertMath: ->
|
||||
body = @$el.find(".response-body")
|
||||
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text()
|
||||
if MathJax?
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
|
||||
|
||||
_delete: (event) =>
|
||||
@trigger "comment:_delete", event
|
||||
|
||||
edit: (event) =>
|
||||
@trigger "comment:edit", event
|
||||
@@ -1,85 +0,0 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentView extends DiscussionContentView
|
||||
tagName: "li"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
render: ->
|
||||
@renderShowView()
|
||||
@
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$el)
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderShowView: () ->
|
||||
if not @showView?
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
@showView = new ResponseCommentShowView(model: @model)
|
||||
@showView.bind "comment:_delete", @_delete
|
||||
@showView.bind "comment:edit", @edit
|
||||
@renderSubView(@showView)
|
||||
|
||||
renderEditView: () ->
|
||||
if not @editView?
|
||||
if @showView?
|
||||
@showView.undelegateEvents()
|
||||
@showView.$el.empty()
|
||||
@showView = null
|
||||
@editView = new ResponseCommentEditView(model: @model)
|
||||
@editView.bind "comment:update", @update
|
||||
@editView.bind "comment:cancel_edit", @cancelEdit
|
||||
@renderSubView(@editView)
|
||||
|
||||
_delete: (event) =>
|
||||
event.preventDefault()
|
||||
if not @model.can('can_delete')
|
||||
return
|
||||
if not confirm gettext("Are you sure you want to delete this comment?")
|
||||
return
|
||||
url = @model.urlFor('_delete')
|
||||
$elem = $(event.target)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.remove()
|
||||
@$el.remove()
|
||||
error: =>
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble deleting this comment. Please try again.")
|
||||
)
|
||||
|
||||
cancelEdit: (event) =>
|
||||
@trigger "comment:cancel_edit", event
|
||||
@renderShowView()
|
||||
|
||||
edit: (event) =>
|
||||
@trigger "comment:edit", event
|
||||
@renderEditView()
|
||||
|
||||
update: (event) =>
|
||||
newBody = @editView.$(".edit-comment-body textarea").val()
|
||||
url = DiscussionUtil.urlFor("update_comment", @model.id)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: "json"
|
||||
data:
|
||||
body: newBody
|
||||
error: DiscussionUtil.formErrorHandler(@$(".edit-comment-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
@model.set("body", newBody)
|
||||
@cancelEdit()
|
||||
@@ -1,25 +0,0 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseEditView extends Backbone.View
|
||||
|
||||
events:
|
||||
"click .post-update": "update"
|
||||
"click .post-cancel": "cancel_edit"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#thread-response-edit-template").html())
|
||||
@$el.html(@template(@model.toJSON()))
|
||||
@delegateEvents()
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
|
||||
@
|
||||
|
||||
update: (event) ->
|
||||
@trigger "response:update", event
|
||||
|
||||
cancel_edit: (event) ->
|
||||
@trigger "response:cancel_edit", event
|
||||
@@ -1,38 +0,0 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseShowView extends DiscussionContentShowView
|
||||
initialize: ->
|
||||
super()
|
||||
@listenTo(@model, "change", @render)
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-response-show-template").html())
|
||||
context = _.extend(
|
||||
{
|
||||
cid: @model.cid,
|
||||
author_display: @getAuthorDisplay(),
|
||||
endorser_display: @getEndorserDisplay(),
|
||||
readOnly: $('.discussion-module').data('read-only')
|
||||
},
|
||||
@model.attributes
|
||||
)
|
||||
@template(context)
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@$el.find(".posted-details .timeago").timeago()
|
||||
@convertMath()
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".response-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
|
||||
if MathJax?
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "response:edit", event
|
||||
|
||||
_delete: (event) ->
|
||||
@trigger "response:_delete", event
|
||||
@@ -1,227 +0,0 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseView extends DiscussionContentView
|
||||
tagName: "li"
|
||||
className: "forum-response"
|
||||
|
||||
events:
|
||||
"click .discussion-submit-comment": "submitComment"
|
||||
"focus .wmd-input": "showEditorChrome"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: (options) ->
|
||||
@collapseComments = options.collapseComments
|
||||
@createShowView()
|
||||
@readOnly = $('.discussion-module').data('read-only')
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-response-template").html())
|
||||
|
||||
container = $("#discussion-container")
|
||||
if !container.length
|
||||
# inline discussion
|
||||
container = $(".discussion-module")
|
||||
templateData = _.extend(
|
||||
@model.toJSON(),
|
||||
wmdId: @model.id ? (new Date()).getTime(),
|
||||
create_sub_comment: container.data("user-create-subcomment"),
|
||||
readOnly: @readOnly
|
||||
)
|
||||
@template(templateData)
|
||||
|
||||
render: ->
|
||||
@$el.addClass("response_" + @model.get("id"))
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
|
||||
@renderShowView()
|
||||
@renderAttrs()
|
||||
if @model.get("thread").get("closed")
|
||||
@hideCommentForm()
|
||||
|
||||
@renderComments()
|
||||
@
|
||||
|
||||
afterInsert: ->
|
||||
@makeWmdEditor "comment-body"
|
||||
@hideEditorChrome()
|
||||
|
||||
hideEditorChrome: ->
|
||||
@$('.wmd-button-row').hide()
|
||||
@$('.wmd-preview-container').hide()
|
||||
@$('.wmd-input').css({
|
||||
height: '35px',
|
||||
padding: '5px'
|
||||
})
|
||||
@$('.comment-post-control').hide()
|
||||
|
||||
showEditorChrome: ->
|
||||
@$('.wmd-button-row').show()
|
||||
@$('.wmd-preview-container').show()
|
||||
@$('.comment-post-control').show()
|
||||
@$('.wmd-input').css({
|
||||
height: '125px',
|
||||
padding: '10px'
|
||||
})
|
||||
|
||||
renderComments: ->
|
||||
comments = new Comments()
|
||||
@commentViews = []
|
||||
comments.comparator = (comment) ->
|
||||
comment.get('created_at')
|
||||
collectComments = (comment) ->
|
||||
comments.add(comment)
|
||||
children = new Comments(comment.get('children'))
|
||||
children.each (child) ->
|
||||
child.parent = comment
|
||||
collectComments(child)
|
||||
@model.get('comments').each collectComments
|
||||
comments.each (comment) => @renderComment(comment, false, null)
|
||||
if @collapseComments && comments.length
|
||||
@$(".comments").hide()
|
||||
@$(".action-show-comments").on("click", (event) =>
|
||||
event.preventDefault()
|
||||
@$(".action-show-comments").hide()
|
||||
@$(".comments").show()
|
||||
)
|
||||
else
|
||||
@$(".action-show-comments").hide()
|
||||
|
||||
renderComment: (comment) =>
|
||||
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?
|
||||
@cancelCommentEdits()
|
||||
@hideCommentForm()
|
||||
view.bind "comment:cancel_edit", () => @showCommentForm()
|
||||
@commentViews.push(view)
|
||||
view
|
||||
|
||||
submitComment: (event) ->
|
||||
event.preventDefault()
|
||||
url = @model.urlFor('reply')
|
||||
body = @getWmdContent("comment-body")
|
||||
return if not body.trim().length
|
||||
@setWmdContent("comment-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), abuse_flaggers:[], user_id: window.user.get("id"), id:"unsaved")
|
||||
view = @renderComment(comment)
|
||||
@hideEditorChrome()
|
||||
@trigger "comment:add", comment
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
body: body
|
||||
success: (response, textStatus) ->
|
||||
comment.set(response.content)
|
||||
comment.updateInfo(response.annotated_content_info)
|
||||
view.render() # This is just to update the id for the most part, but might be useful in general
|
||||
|
||||
_delete: (event) =>
|
||||
event.preventDefault()
|
||||
if not @model.can('can_delete')
|
||||
return
|
||||
if not confirm gettext("Are you sure you want to delete this response?")
|
||||
return
|
||||
url = @model.urlFor('_delete')
|
||||
@model.remove()
|
||||
@$el.remove()
|
||||
$elem = $(event.target)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
|
||||
createEditView: () ->
|
||||
if @showView?
|
||||
@showView.$el.empty()
|
||||
|
||||
if @editView?
|
||||
@editView.model = @model
|
||||
else
|
||||
@editView = new ThreadResponseEditView(model: @model)
|
||||
@editView.bind "response:update", @update
|
||||
@editView.bind "response:cancel_edit", @cancelEdit
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$('.discussion-response'))
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderEditView: () ->
|
||||
@renderSubView(@editView)
|
||||
|
||||
cancelCommentEdits: () ->
|
||||
_.each(@commentViews, (view) -> view.cancelEdit())
|
||||
|
||||
hideCommentForm: () ->
|
||||
@$('.comment-form').closest('li').hide()
|
||||
|
||||
showCommentForm: () ->
|
||||
@$('.comment-form').closest('li').show()
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.$el.empty()
|
||||
|
||||
if @showView?
|
||||
@showView.model = @model
|
||||
else
|
||||
@showView = new ThreadResponseShowView(model: @model)
|
||||
@showView.bind "response:_delete", @_delete
|
||||
@showView.bind "response:edit", @edit
|
||||
@showView.on "comment:endorse", => @trigger("comment:endorse")
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
|
||||
cancelEdit: (event) =>
|
||||
event.preventDefault()
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
@showCommentForm()
|
||||
|
||||
edit: (event) =>
|
||||
@createEditView()
|
||||
@renderEditView()
|
||||
@cancelCommentEdits()
|
||||
@hideCommentForm()
|
||||
|
||||
update: (event) =>
|
||||
|
||||
newBody = @editView.$(".edit-post-body textarea").val()
|
||||
|
||||
url = DiscussionUtil.urlFor('update_comment', @model.id)
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target) if event
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
body: newBody
|
||||
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
|
||||
@editView.$(".wmd-preview p").html("")
|
||||
|
||||
@model.set
|
||||
body: newBody
|
||||
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
@showCommentForm()
|
||||
|
||||
436
common/static/common/js/discussion/content.js
Normal file
436
common/static/common/js/discussion/content.js
Normal file
@@ -0,0 +1,436 @@
|
||||
/* globals DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var __hasProp = {}.hasOwnProperty;
|
||||
|
||||
function __extends(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
}
|
||||
|
||||
var __indexOf = [].indexOf || function(item) {
|
||||
for (var i = 0, l = this.length; i < l; i++) {
|
||||
if (i in this && this[i] === item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.Content = (function(_super) {
|
||||
|
||||
__extends(Content, _super);
|
||||
|
||||
function Content() {
|
||||
return Content.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Content.contents = {};
|
||||
|
||||
Content.contentInfos = {};
|
||||
|
||||
Content.prototype.template = function() {
|
||||
return DiscussionUtil.getTemplate('_content');
|
||||
};
|
||||
|
||||
Content.prototype.actions = {
|
||||
editable: '.admin-edit',
|
||||
can_reply: '.discussion-reply',
|
||||
can_delete: '.admin-delete',
|
||||
can_openclose: '.admin-openclose',
|
||||
can_report: '.admin-report',
|
||||
can_vote: '.admin-vote'
|
||||
};
|
||||
|
||||
Content.prototype.urlMappers = {};
|
||||
|
||||
Content.prototype.urlFor = function(name) {
|
||||
return this.urlMappers[name].apply(this);
|
||||
};
|
||||
|
||||
Content.prototype.can = function(action) {
|
||||
return (this.get('ability') || {})[action];
|
||||
};
|
||||
|
||||
Content.prototype.canBeEndorsed = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
Content.prototype.updateInfo = function(info) {
|
||||
if (info) {
|
||||
this.set('ability', info.ability);
|
||||
this.set('voted', info.voted);
|
||||
return this.set('subscribed', info.subscribed);
|
||||
}
|
||||
};
|
||||
|
||||
Content.prototype.addComment = function(comment, options) {
|
||||
var comments_count, model, thread;
|
||||
options = (options) ? options : {};
|
||||
if (!options.silent) {
|
||||
thread = this.get('thread');
|
||||
comments_count = parseInt(thread.get('comments_count'));
|
||||
thread.set('comments_count', comments_count + 1);
|
||||
}
|
||||
this.get('children').push(comment);
|
||||
model = new Comment($.extend({}, comment, {
|
||||
thread: this.get('thread')
|
||||
}));
|
||||
this.get('comments').add(model);
|
||||
this.trigger("comment:add");
|
||||
return model;
|
||||
};
|
||||
|
||||
Content.prototype.removeComment = function(comment) {
|
||||
var comments_count, thread;
|
||||
thread = this.get('thread');
|
||||
comments_count = parseInt(thread.get('comments_count'));
|
||||
thread.set('comments_count', comments_count - 1 - comment.getCommentsCount());
|
||||
return this.trigger("comment:remove");
|
||||
};
|
||||
|
||||
Content.prototype.resetComments = function(children) {
|
||||
var comment, _i, _len, _ref, _results;
|
||||
this.set('children', []);
|
||||
this.set('comments', new Comments()); // jshint ignore:line
|
||||
_ref = children || [];
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
comment = _ref[_i];
|
||||
_results.push(this.addComment(comment, {
|
||||
silent: true
|
||||
}));
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Content.prototype.initialize = function() {
|
||||
var userId;
|
||||
Content.addContent(this.id, this);
|
||||
userId = this.get('user_id');
|
||||
if (userId) {
|
||||
this.set('staff_authored', DiscussionUtil.isStaff(userId));
|
||||
this.set('community_ta_authored', DiscussionUtil.isTA(userId));
|
||||
} else {
|
||||
this.set('staff_authored', false);
|
||||
this.set('community_ta_authored', false);
|
||||
}
|
||||
if (Content.getInfo(this.id)) {
|
||||
this.updateInfo(Content.getInfo(this.id));
|
||||
}
|
||||
this.set('user_url', DiscussionUtil.urlFor('user_profile', userId));
|
||||
return this.resetComments(this.get('children'));
|
||||
};
|
||||
|
||||
Content.prototype.remove = function() {
|
||||
if (this.get('type') === 'comment') {
|
||||
this.get('thread').removeComment(this);
|
||||
return this.get('thread').trigger("comment:remove", this);
|
||||
} else {
|
||||
return this.trigger("thread:remove", this);
|
||||
}
|
||||
};
|
||||
|
||||
Content.addContent = function(id, content) {
|
||||
this.contents[id] = content;
|
||||
};
|
||||
|
||||
Content.getContent = function(id) {
|
||||
return this.contents[id];
|
||||
};
|
||||
|
||||
Content.getInfo = function(id) {
|
||||
return this.contentInfos[id];
|
||||
};
|
||||
|
||||
Content.loadContentInfos = function(infos) {
|
||||
var id, info;
|
||||
for (id in infos) {
|
||||
if (infos.hasOwnProperty(id)) {
|
||||
info = infos[id];
|
||||
if (this.getContent(id)) {
|
||||
this.getContent(id).updateInfo(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $.extend(this.contentInfos, infos);
|
||||
};
|
||||
|
||||
Content.prototype.pinThread = function() {
|
||||
var pinned;
|
||||
pinned = this.get("pinned");
|
||||
this.set("pinned", pinned);
|
||||
return this.trigger("change", this);
|
||||
};
|
||||
|
||||
Content.prototype.unPinThread = function() {
|
||||
var pinned;
|
||||
pinned = this.get("pinned");
|
||||
this.set("pinned", pinned);
|
||||
return this.trigger("change", this);
|
||||
};
|
||||
|
||||
Content.prototype.flagAbuse = function() {
|
||||
var temp_array;
|
||||
temp_array = this.get("abuse_flaggers");
|
||||
temp_array.push(window.user.get('id'));
|
||||
this.set("abuse_flaggers", temp_array);
|
||||
return this.trigger("change", this);
|
||||
};
|
||||
|
||||
Content.prototype.unflagAbuse = function() {
|
||||
this.get("abuse_flaggers").pop(window.user.get('id'));
|
||||
return this.trigger("change", this);
|
||||
};
|
||||
|
||||
Content.prototype.isFlagged = function() {
|
||||
var flaggers, user;
|
||||
user = DiscussionUtil.getUser();
|
||||
flaggers = this.get("abuse_flaggers");
|
||||
return user && (
|
||||
(__indexOf.call(flaggers, user.id) >= 0) ||
|
||||
(DiscussionUtil.isPrivilegedUser(user.id) && flaggers.length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
Content.prototype.incrementVote = function(increment) {
|
||||
var newVotes;
|
||||
newVotes = _.clone(this.get("votes"));
|
||||
newVotes.up_count = newVotes.up_count + increment;
|
||||
return this.set("votes", newVotes);
|
||||
};
|
||||
|
||||
Content.prototype.vote = function() {
|
||||
return this.incrementVote(1);
|
||||
};
|
||||
|
||||
Content.prototype.unvote = function() {
|
||||
return this.incrementVote(-1);
|
||||
};
|
||||
|
||||
return Content;
|
||||
|
||||
})(Backbone.Model);
|
||||
this.Thread = (function(_super) {
|
||||
|
||||
__extends(Thread, _super);
|
||||
|
||||
function Thread() {
|
||||
return Thread.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Thread.prototype.urlMappers = {
|
||||
'retrieve': function() {
|
||||
return DiscussionUtil.urlFor('retrieve_single_thread', this.get('commentable_id'), this.id);
|
||||
},
|
||||
'reply': function() {
|
||||
return DiscussionUtil.urlFor('create_comment', this.id);
|
||||
},
|
||||
'unvote': function() {
|
||||
return DiscussionUtil.urlFor("undo_vote_for_" + (this.get('type')), this.id);
|
||||
},
|
||||
'upvote': function() {
|
||||
return DiscussionUtil.urlFor("upvote_" + (this.get('type')), this.id);
|
||||
},
|
||||
'downvote': function() {
|
||||
return DiscussionUtil.urlFor("downvote_" + (this.get('type')), this.id);
|
||||
},
|
||||
'close': function() {
|
||||
return DiscussionUtil.urlFor('openclose_thread', this.id);
|
||||
},
|
||||
'update': function() {
|
||||
return DiscussionUtil.urlFor('update_thread', this.id);
|
||||
},
|
||||
'_delete': function() {
|
||||
return DiscussionUtil.urlFor('delete_thread', this.id);
|
||||
},
|
||||
'follow': function() {
|
||||
return DiscussionUtil.urlFor('follow_thread', this.id);
|
||||
},
|
||||
'unfollow': function() {
|
||||
return DiscussionUtil.urlFor('unfollow_thread', this.id);
|
||||
},
|
||||
'flagAbuse': function() {
|
||||
return DiscussionUtil.urlFor("flagAbuse_" + (this.get('type')), this.id);
|
||||
},
|
||||
'unFlagAbuse': function() {
|
||||
return DiscussionUtil.urlFor("unFlagAbuse_" + (this.get('type')), this.id);
|
||||
},
|
||||
'pinThread': function() {
|
||||
return DiscussionUtil.urlFor("pin_thread", this.id);
|
||||
},
|
||||
'unPinThread': function() {
|
||||
return DiscussionUtil.urlFor("un_pin_thread", this.id);
|
||||
}
|
||||
};
|
||||
|
||||
Thread.prototype.initialize = function() {
|
||||
this.set('thread', this);
|
||||
return Thread.__super__.initialize.call(this);
|
||||
};
|
||||
|
||||
Thread.prototype.comment = function() {
|
||||
return this.set("comments_count", parseInt(this.get("comments_count")) + 1);
|
||||
};
|
||||
|
||||
Thread.prototype.follow = function() {
|
||||
return this.set('subscribed', true);
|
||||
};
|
||||
|
||||
Thread.prototype.unfollow = function() {
|
||||
return this.set('subscribed', false);
|
||||
};
|
||||
|
||||
Thread.prototype.display_body = function() {
|
||||
if (this.has("highlighted_body")) {
|
||||
return String(this.get("highlighted_body"))
|
||||
.replace(/<highlight>/g, '<mark>')
|
||||
.replace(/<\/highlight>/g, '</mark>');
|
||||
} else {
|
||||
return this.get("body");
|
||||
}
|
||||
};
|
||||
|
||||
Thread.prototype.display_title = function() {
|
||||
if (this.has("highlighted_title")) {
|
||||
return String(this.get("highlighted_title"))
|
||||
.replace(/<highlight>/g, '<mark>')
|
||||
.replace(/<\/highlight>/g, '</mark>');
|
||||
} else {
|
||||
return this.get("title");
|
||||
}
|
||||
};
|
||||
|
||||
Thread.prototype.toJSON = function() {
|
||||
var json_attributes;
|
||||
json_attributes = _.clone(this.attributes);
|
||||
return _.extend(json_attributes, {
|
||||
title: this.display_title(),
|
||||
body: this.display_body()
|
||||
});
|
||||
};
|
||||
|
||||
Thread.prototype.created_at_date = function() {
|
||||
return new Date(this.get("created_at"));
|
||||
};
|
||||
|
||||
Thread.prototype.created_at_time = function() {
|
||||
return new Date(this.get("created_at")).getTime();
|
||||
};
|
||||
|
||||
Thread.prototype.hasResponses = function() {
|
||||
return this.get('comments_count') > 0;
|
||||
};
|
||||
|
||||
return Thread;
|
||||
|
||||
})(this.Content);
|
||||
this.Comment = (function(_super) {
|
||||
|
||||
__extends(Comment, _super);
|
||||
|
||||
function Comment() {
|
||||
var self = this;
|
||||
this.canBeEndorsed = function() {
|
||||
return Comment.prototype.canBeEndorsed.apply(self, arguments);
|
||||
};
|
||||
return Comment.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Comment.prototype.urlMappers = {
|
||||
'reply': function() {
|
||||
return DiscussionUtil.urlFor('create_sub_comment', this.id);
|
||||
},
|
||||
'unvote': function() {
|
||||
return DiscussionUtil.urlFor("undo_vote_for_" + (this.get('type')), this.id);
|
||||
},
|
||||
'upvote': function() {
|
||||
return DiscussionUtil.urlFor("upvote_" + (this.get('type')), this.id);
|
||||
},
|
||||
'downvote': function() {
|
||||
return DiscussionUtil.urlFor("downvote_" + (this.get('type')), this.id);
|
||||
},
|
||||
'endorse': function() {
|
||||
return DiscussionUtil.urlFor('endorse_comment', this.id);
|
||||
},
|
||||
'update': function() {
|
||||
return DiscussionUtil.urlFor('update_comment', this.id);
|
||||
},
|
||||
'_delete': function() {
|
||||
return DiscussionUtil.urlFor('delete_comment', this.id);
|
||||
},
|
||||
'flagAbuse': function() {
|
||||
return DiscussionUtil.urlFor("flagAbuse_" + (this.get('type')), this.id);
|
||||
},
|
||||
'unFlagAbuse': function() {
|
||||
return DiscussionUtil.urlFor("unFlagAbuse_" + (this.get('type')), this.id);
|
||||
}
|
||||
};
|
||||
|
||||
Comment.prototype.getCommentsCount = function() {
|
||||
var count;
|
||||
count = 0;
|
||||
this.get('comments').each(function(comment) {
|
||||
return count += comment.getCommentsCount() + 1;
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
Comment.prototype.canBeEndorsed = function() {
|
||||
var user_id;
|
||||
user_id = window.user.get("id");
|
||||
return user_id && (
|
||||
DiscussionUtil.isPrivilegedUser(user_id) ||
|
||||
(
|
||||
this.get('thread').get('thread_type') === 'question' &&
|
||||
this.get('thread').get('user_id') === user_id
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return Comment;
|
||||
|
||||
})(this.Content);
|
||||
|
||||
this.Comments = (function(_super) {
|
||||
|
||||
__extends(Comments, _super);
|
||||
|
||||
function Comments() {
|
||||
return Comments.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Comments.prototype.model = Comment;
|
||||
|
||||
Comments.prototype.initialize = function() {
|
||||
var self = this;
|
||||
return this.bind("add", function(item) {
|
||||
item.collection = self;
|
||||
});
|
||||
};
|
||||
|
||||
Comments.prototype.find = function(id) {
|
||||
return _.first(this.where({
|
||||
id: id
|
||||
}));
|
||||
};
|
||||
|
||||
return Comments;
|
||||
|
||||
})(Backbone.Collection);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
222
common/static/common/js/discussion/discussion.js
Normal file
222
common/static/common/js/discussion/discussion.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/* globals Thread, DiscussionUtil, Content */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.Discussion = (function(_super) {
|
||||
|
||||
__extends(Discussion, _super);
|
||||
|
||||
function Discussion() {
|
||||
return Discussion.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Discussion.prototype.model = Thread;
|
||||
|
||||
Discussion.prototype.initialize = function(models, options) {
|
||||
var self = this;
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
this.pages = options.pages || 1;
|
||||
this.current_page = 1;
|
||||
this.sort_preference = options.sort;
|
||||
this.bind("add", function(item) {
|
||||
item.discussion = self;
|
||||
});
|
||||
this.setSortComparator(this.sort_preference);
|
||||
return this.on("thread:remove", function(thread) {
|
||||
self.remove(thread);
|
||||
});
|
||||
};
|
||||
|
||||
Discussion.prototype.find = function(id) {
|
||||
return _.first(this.where({
|
||||
id: id
|
||||
}));
|
||||
};
|
||||
|
||||
Discussion.prototype.hasMorePages = function() {
|
||||
return this.current_page < this.pages;
|
||||
};
|
||||
|
||||
Discussion.prototype.setSortComparator = function(sortBy) {
|
||||
switch (sortBy) {
|
||||
case 'activity':
|
||||
this.comparator = this.sortByDateRecentFirst;
|
||||
break;
|
||||
case 'votes':
|
||||
this.comparator = this.sortByVotes;
|
||||
break;
|
||||
case 'comments':
|
||||
this.comparator = this.sortByComments;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Discussion.prototype.addThread = function(thread) {
|
||||
var model;
|
||||
if (!this.find(thread.id)) {
|
||||
model = new Thread(thread);
|
||||
this.add(model);
|
||||
return model;
|
||||
}
|
||||
};
|
||||
|
||||
Discussion.prototype.retrieveAnotherPage = function(mode, options, sort_options, error) {
|
||||
var data, url,
|
||||
self = this;
|
||||
if (options === null) {
|
||||
options = {};
|
||||
}
|
||||
if (sort_options === null) {
|
||||
sort_options = {};
|
||||
}
|
||||
data = {
|
||||
page: this.current_page + 1
|
||||
};
|
||||
if (_.contains(["unread", "unanswered", "flagged"], options.filter)) {
|
||||
data[options.filter] = true;
|
||||
}
|
||||
switch (mode) {
|
||||
case 'search':
|
||||
url = DiscussionUtil.urlFor('search');
|
||||
data.text = options.search_text;
|
||||
break;
|
||||
case 'commentables':
|
||||
url = DiscussionUtil.urlFor('search');
|
||||
data.commentable_ids = options.commentable_ids;
|
||||
break;
|
||||
case 'all':
|
||||
url = DiscussionUtil.urlFor('threads');
|
||||
break;
|
||||
case 'followed':
|
||||
url = DiscussionUtil.urlFor('followed_threads', options.user_id);
|
||||
}
|
||||
if (options.group_id) {
|
||||
data.group_id = options.group_id;
|
||||
}
|
||||
data.sort_key = sort_options.sort_key || 'activity';
|
||||
data.sort_order = sort_options.sort_order || 'desc';
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: this.$el,
|
||||
url: url,
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
var models, new_collection, new_threads;
|
||||
models = self.models;
|
||||
new_threads = [
|
||||
(function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = response.discussion_data;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
data = _ref[_i];
|
||||
_results.push(new Thread(data));
|
||||
}
|
||||
return _results;
|
||||
})()
|
||||
][0];
|
||||
new_collection = _.union(models, new_threads);
|
||||
Content.loadContentInfos(response.annotated_content_info);
|
||||
self.pages = response.num_pages;
|
||||
self.current_page = response.page;
|
||||
return self.reset(new_collection);
|
||||
},
|
||||
error: error
|
||||
});
|
||||
};
|
||||
|
||||
Discussion.prototype.sortByDate = function(thread) {
|
||||
/*
|
||||
The comment client asks each thread for a value by which to sort the collection
|
||||
and calls this sort routine regardless of the order returned from the LMS/comments service
|
||||
so, this takes advantage of this per-thread value and returns tomorrow's date
|
||||
for pinned threads, ensuring that they appear first, (which is the intent of pinned threads)
|
||||
*/
|
||||
return this.pinnedThreadsSortComparatorWithDate(thread, true);
|
||||
};
|
||||
|
||||
Discussion.prototype.sortByDateRecentFirst = function(thread) {
|
||||
/*
|
||||
Same as above
|
||||
but negative to flip the order (newest first)
|
||||
*/
|
||||
return this.pinnedThreadsSortComparatorWithDate(thread, false);
|
||||
};
|
||||
|
||||
Discussion.prototype.sortByVotes = function(thread1, thread2) {
|
||||
var thread1_count, thread2_count;
|
||||
thread1_count = parseInt(thread1.get("votes").up_count);
|
||||
thread2_count = parseInt(thread2.get("votes").up_count);
|
||||
return this.pinnedThreadsSortComparatorWithCount(thread1, thread2, thread1_count, thread2_count);
|
||||
};
|
||||
|
||||
Discussion.prototype.sortByComments = function(thread1, thread2) {
|
||||
var thread1_count, thread2_count;
|
||||
thread1_count = parseInt(thread1.get("comments_count"));
|
||||
thread2_count = parseInt(thread2.get("comments_count"));
|
||||
return this.pinnedThreadsSortComparatorWithCount(thread1, thread2, thread1_count, thread2_count);
|
||||
};
|
||||
|
||||
Discussion.prototype.pinnedThreadsSortComparatorWithCount = function(
|
||||
thread1, thread2, thread1_count, thread2_count
|
||||
) {
|
||||
if (thread1.get('pinned') && !thread2.get('pinned')) {
|
||||
return -1;
|
||||
} else if (thread2.get('pinned') && !thread1.get('pinned')) {
|
||||
return 1;
|
||||
} else {
|
||||
if (thread1_count > thread2_count) {
|
||||
return -1;
|
||||
} else if (thread2_count > thread1_count) {
|
||||
return 1;
|
||||
} else {
|
||||
if (thread1.created_at_time() > thread2.created_at_time()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Discussion.prototype.pinnedThreadsSortComparatorWithDate = function(thread, ascending) {
|
||||
var preferredDate, threadLastActivityAtTime, today;
|
||||
threadLastActivityAtTime = new Date(thread.get("last_activity_at")).getTime();
|
||||
if (thread.get('pinned')) {
|
||||
today = new Date();
|
||||
preferredDate = new Date(today.getTime() + (24 * 60 * 60 * 1000) + threadLastActivityAtTime);
|
||||
} else {
|
||||
preferredDate = threadLastActivityAtTime;
|
||||
}
|
||||
if (ascending) {
|
||||
return preferredDate;
|
||||
} else {
|
||||
return -preferredDate;
|
||||
}
|
||||
};
|
||||
|
||||
return Discussion;
|
||||
|
||||
})(Backbone.Collection);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
266
common/static/common/js/discussion/discussion_module_view.js
Normal file
266
common/static/common/js/discussion/discussion_module_view.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/* globals Discussion, DiscussionUtil, DiscussionUser, DiscussionCourseSettings, DiscussionThreadView, Content,
|
||||
NewPostView */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionModuleView = (function(_super) {
|
||||
|
||||
__extends(DiscussionModuleView, _super);
|
||||
|
||||
function DiscussionModuleView() {
|
||||
var self = this;
|
||||
this.navigateToPage = function() {
|
||||
return DiscussionModuleView.prototype.navigateToPage.apply(self, arguments);
|
||||
};
|
||||
this.renderPagination = function() {
|
||||
return DiscussionModuleView.prototype.renderPagination.apply(self, arguments);
|
||||
};
|
||||
this.addThread = function() {
|
||||
return DiscussionModuleView.prototype.addThread.apply(self, arguments);
|
||||
};
|
||||
this.renderDiscussion = function() {
|
||||
return DiscussionModuleView.prototype.renderDiscussion.apply(self, arguments);
|
||||
};
|
||||
this.loadPage = function() {
|
||||
return DiscussionModuleView.prototype.loadPage.apply(self, arguments);
|
||||
};
|
||||
this.toggleDiscussion = function() {
|
||||
return DiscussionModuleView.prototype.toggleDiscussion.apply(self, arguments);
|
||||
};
|
||||
this.hideDiscussion = function() {
|
||||
return DiscussionModuleView.prototype.hideDiscussion.apply(self, arguments);
|
||||
};
|
||||
this.hideNewPost = function() {
|
||||
return DiscussionModuleView.prototype.hideNewPost.apply(self, arguments);
|
||||
};
|
||||
this.toggleNewPost = function() {
|
||||
return DiscussionModuleView.prototype.toggleNewPost.apply(self, arguments);
|
||||
};
|
||||
return DiscussionModuleView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionModuleView.prototype.events = {
|
||||
"click .discussion-show": "toggleDiscussion",
|
||||
"keydown .discussion-show": function(event) {
|
||||
return DiscussionUtil.activateOnSpace(event, this.toggleDiscussion);
|
||||
},
|
||||
"click .new-post-btn": "toggleNewPost",
|
||||
"keydown .new-post-btn": function(event) {
|
||||
return DiscussionUtil.activateOnSpace(event, this.toggleNewPost);
|
||||
},
|
||||
"click .discussion-paginator a": "navigateToPage"
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.page_re = /\?discussion_page=(\d+)/;
|
||||
|
||||
DiscussionModuleView.prototype.initialize = function(options) {
|
||||
var match;
|
||||
this.toggleDiscussionBtn = this.$(".discussion-show");
|
||||
match = this.page_re.exec(window.location.href);
|
||||
this.context = options.context || "course";
|
||||
if (match) {
|
||||
this.page = parseInt(match[1]);
|
||||
} else {
|
||||
this.page = 1;
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.toggleNewPost = function(event) {
|
||||
event.preventDefault();
|
||||
if (!this.newPostForm) {
|
||||
this.toggleDiscussion();
|
||||
this.isWaitingOnNewPost = true;
|
||||
return;
|
||||
}
|
||||
if (this.showed) {
|
||||
this.newPostForm.slideDown(300);
|
||||
} else {
|
||||
this.newPostForm.show().focus();
|
||||
}
|
||||
this.toggleDiscussionBtn.addClass('shown');
|
||||
this.toggleDiscussionBtn.find('.button-text').html(gettext("Hide Discussion"));
|
||||
this.$("section.discussion").slideDown();
|
||||
this.showed = true;
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.hideNewPost = function() {
|
||||
return this.newPostForm.slideUp(300);
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.hideDiscussion = function() {
|
||||
this.$("section.discussion").slideUp();
|
||||
this.toggleDiscussionBtn.removeClass('shown');
|
||||
this.toggleDiscussionBtn.find('.button-text').html(gettext("Show Discussion"));
|
||||
this.showed = false;
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.toggleDiscussion = function() {
|
||||
var $elem,
|
||||
self = this;
|
||||
if (this.showed) {
|
||||
return this.hideDiscussion();
|
||||
} else {
|
||||
this.toggleDiscussionBtn.addClass('shown');
|
||||
this.toggleDiscussionBtn.find('.button-text').html(gettext("Hide Discussion"));
|
||||
if (this.retrieved) {
|
||||
this.$("section.discussion").slideDown();
|
||||
this.showed = true;
|
||||
} else {
|
||||
$elem = this.toggleDiscussionBtn;
|
||||
return this.loadPage($elem, function() {
|
||||
self.hideDiscussion();
|
||||
return DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the discussion. Please try again.")
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.loadPage = function($elem, error) {
|
||||
var discussionId, url,
|
||||
self = this;
|
||||
discussionId = this.$el.data("discussion-id");
|
||||
url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + ("?page=" + this.page);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $elem,
|
||||
$loading: $elem,
|
||||
takeFocus: true,
|
||||
url: url,
|
||||
type: "GET",
|
||||
dataType: 'json',
|
||||
success: function(response, textStatus) {
|
||||
return self.renderDiscussion($elem, response, textStatus, discussionId);
|
||||
},
|
||||
error: error
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.renderDiscussion = function($elem, response, textStatus, discussionId) {
|
||||
var $discussion, user,
|
||||
self = this;
|
||||
$elem.focus();
|
||||
user = new DiscussionUser(response.user_info);
|
||||
window.user = user;
|
||||
DiscussionUtil.setUser(user);
|
||||
Content.loadContentInfos(response.annotated_content_info);
|
||||
DiscussionUtil.loadRoles(response.roles);
|
||||
this.course_settings = new DiscussionCourseSettings(response.course_settings);
|
||||
this.discussion = new Discussion();
|
||||
this.discussion.reset(response.discussion_data, {
|
||||
silent: false
|
||||
});
|
||||
$discussion = _.template($("#inline-discussion-template").html())({
|
||||
'threads': response.discussion_data,
|
||||
'discussionId': discussionId
|
||||
});
|
||||
if (this.$('section.discussion').length) {
|
||||
this.$('section.discussion').replaceWith($discussion);
|
||||
} else {
|
||||
this.$el.append($discussion);
|
||||
}
|
||||
this.newPostForm = this.$el.find('.new-post-article');
|
||||
this.threadviews = this.discussion.map(function(thread) {
|
||||
var view;
|
||||
view = new DiscussionThreadView({
|
||||
el: self.$("article#thread_" + thread.id),
|
||||
model: thread,
|
||||
mode: "inline",
|
||||
context: self.context,
|
||||
course_settings: self.course_settings,
|
||||
topicId: discussionId
|
||||
});
|
||||
thread.on("thread:thread_type_updated", function() {
|
||||
view.rerender();
|
||||
return view.expand();
|
||||
});
|
||||
return view;
|
||||
});
|
||||
_.each(this.threadviews, function(dtv) {
|
||||
return dtv.render();
|
||||
});
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info);
|
||||
this.newPostView = new NewPostView({
|
||||
el: this.newPostForm,
|
||||
collection: this.discussion,
|
||||
course_settings: this.course_settings,
|
||||
topicId: discussionId,
|
||||
is_commentable_cohorted: response.is_commentable_cohorted
|
||||
});
|
||||
this.newPostView.render();
|
||||
this.listenTo(this.newPostView, 'newPost:cancel', this.hideNewPost);
|
||||
this.discussion.on("add", this.addThread);
|
||||
this.retrieved = true;
|
||||
this.showed = true;
|
||||
this.renderPagination(response.num_pages);
|
||||
if (this.isWaitingOnNewPost) {
|
||||
return this.newPostForm.show().focus();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.addThread = function(thread) {
|
||||
var article, threadView;
|
||||
article = $("<article class='discussion-thread' id='thread_" + thread.id + "'></article>");
|
||||
this.$('section.discussion > .threads').prepend(article);
|
||||
threadView = new DiscussionThreadView({
|
||||
el: article,
|
||||
model: thread,
|
||||
mode: "inline",
|
||||
context: this.context,
|
||||
course_settings: this.course_settings,
|
||||
topicId: this.$el.data("discussion-id")
|
||||
});
|
||||
threadView.render();
|
||||
return this.threadviews.unshift(threadView);
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.renderPagination = function(numPages) {
|
||||
var pageUrl, pagination, params;
|
||||
pageUrl = function(number) {
|
||||
return "?discussion_page=" + number;
|
||||
};
|
||||
params = DiscussionUtil.getPaginationParams(this.page, numPages, pageUrl);
|
||||
pagination = _.template($("#pagination-template").html())(params);
|
||||
return this.$('section.discussion-pagination').html(pagination);
|
||||
};
|
||||
|
||||
DiscussionModuleView.prototype.navigateToPage = function(event) {
|
||||
var currPage,
|
||||
self = this;
|
||||
event.preventDefault();
|
||||
window.history.pushState({}, window.document.title, event.target.href);
|
||||
currPage = this.page;
|
||||
this.page = $(event.target).data('page-number');
|
||||
return this.loadPage($(event.target), function() {
|
||||
self.page = currPage;
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the threads you requested. Please try again.")
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return DiscussionModuleView;
|
||||
|
||||
})(Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
167
common/static/common/js/discussion/discussion_router.js
Normal file
167
common/static/common/js/discussion/discussion_router.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/* globals DiscussionThreadListView, DiscussionThreadView, DiscussionUtil, NewPostView */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionRouter = (function(_super) {
|
||||
|
||||
__extends(DiscussionRouter, _super);
|
||||
|
||||
function DiscussionRouter() {
|
||||
var self = this;
|
||||
this.hideNewPost = function() {
|
||||
return DiscussionRouter.prototype.hideNewPost.apply(self, arguments);
|
||||
};
|
||||
this.showNewPost = function() {
|
||||
return DiscussionRouter.prototype.showNewPost.apply(self, arguments);
|
||||
};
|
||||
this.navigateToAllThreads = function() {
|
||||
return DiscussionRouter.prototype.navigateToAllThreads.apply(self, arguments);
|
||||
};
|
||||
this.navigateToThread = function() {
|
||||
return DiscussionRouter.prototype.navigateToThread.apply(self, arguments);
|
||||
};
|
||||
this.showMain = function() {
|
||||
return DiscussionRouter.prototype.showMain.apply(self, arguments);
|
||||
};
|
||||
this.setActiveThread = function() {
|
||||
return DiscussionRouter.prototype.setActiveThread.apply(self, arguments);
|
||||
};
|
||||
return DiscussionRouter.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionRouter.prototype.routes = {
|
||||
"": "allThreads",
|
||||
":forum_name/threads/:thread_id": "showThread"
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.initialize = function(options) {
|
||||
var self = this;
|
||||
this.discussion = options.discussion;
|
||||
this.course_settings = options.course_settings;
|
||||
this.nav = new DiscussionThreadListView({
|
||||
collection: this.discussion,
|
||||
el: $(".forum-nav"),
|
||||
courseSettings: this.course_settings
|
||||
});
|
||||
this.nav.on("thread:selected", this.navigateToThread);
|
||||
this.nav.on("thread:removed", this.navigateToAllThreads);
|
||||
this.nav.on("threads:rendered", this.setActiveThread);
|
||||
this.nav.on("thread:created", this.navigateToThread);
|
||||
this.nav.render();
|
||||
this.newPost = $('.new-post-article');
|
||||
this.newPostView = new NewPostView({
|
||||
el: this.newPost,
|
||||
collection: this.discussion,
|
||||
course_settings: this.course_settings,
|
||||
mode: "tab"
|
||||
});
|
||||
this.newPostView.render();
|
||||
this.listenTo(this.newPostView, 'newPost:cancel', this.hideNewPost);
|
||||
$('.new-post-btn').bind("click", this.showNewPost);
|
||||
return $('.new-post-btn').bind("keydown", function(event) {
|
||||
return DiscussionUtil.activateOnSpace(event, self.showNewPost);
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.allThreads = function() {
|
||||
this.nav.updateSidebar();
|
||||
return this.nav.goHome();
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.setActiveThread = function() {
|
||||
if (this.thread) {
|
||||
return this.nav.setActiveThread(this.thread.get("id"));
|
||||
} else {
|
||||
return this.nav.goHome;
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.showThread = function(forum_name, thread_id) {
|
||||
this.thread = this.discussion.get(thread_id);
|
||||
this.thread.set("unread_comments_count", 0);
|
||||
this.thread.set("read", true);
|
||||
this.setActiveThread();
|
||||
return this.showMain();
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.showMain = function() {
|
||||
var self = this;
|
||||
if (this.main) {
|
||||
this.main.cleanup();
|
||||
this.main.undelegateEvents();
|
||||
}
|
||||
if (!($(".forum-content").is(":visible"))) {
|
||||
$(".forum-content").fadeIn();
|
||||
}
|
||||
if (this.newPost.is(":visible")) {
|
||||
this.newPost.fadeOut();
|
||||
}
|
||||
this.main = new DiscussionThreadView({
|
||||
el: $(".forum-content"),
|
||||
model: this.thread,
|
||||
mode: "tab",
|
||||
course_settings: this.course_settings
|
||||
});
|
||||
this.main.render();
|
||||
this.main.on("thread:responses:rendered", function() {
|
||||
return self.nav.updateSidebar();
|
||||
});
|
||||
return this.thread.on("thread:thread_type_updated", this.showMain);
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.navigateToThread = function(thread_id) {
|
||||
var thread;
|
||||
thread = this.discussion.get(thread_id);
|
||||
return this.navigate("" + (thread.get("commentable_id")) + "/threads/" + thread_id, {
|
||||
trigger: true
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.navigateToAllThreads = function() {
|
||||
return this.navigate("", {
|
||||
trigger: true
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.showNewPost = function() {
|
||||
var self = this;
|
||||
return $('.forum-content').fadeOut({
|
||||
duration: 200,
|
||||
complete: function() {
|
||||
return self.newPost.fadeIn(200).focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionRouter.prototype.hideNewPost = function() {
|
||||
return this.newPost.fadeOut({
|
||||
duration: 200,
|
||||
complete: function() {
|
||||
return $('.forum-content').fadeIn(200).find('.thread-wrapper').focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return DiscussionRouter;
|
||||
|
||||
})(Backbone.Router);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
76
common/static/common/js/discussion/main.js
Normal file
76
common/static/common/js/discussion/main.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/* global $$course_id, Content, Discussion, DiscussionRouter, DiscussionCourseSettings,
|
||||
DiscussionUser, DiscussionUserProfileView, DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
var DiscussionApp, DiscussionProfileApp;
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
DiscussionApp = {
|
||||
start: function(elem) {
|
||||
var content_info, course_settings, discussion, element, sort_preference, thread_pages, threads,
|
||||
user, user_info;
|
||||
DiscussionUtil.loadRolesFromContainer();
|
||||
element = $(elem);
|
||||
window.$$course_id = element.data("course-id");
|
||||
window.courseName = element.data("course-name");
|
||||
user_info = element.data("user-info");
|
||||
sort_preference = element.data("sort-preference");
|
||||
threads = element.data("threads");
|
||||
thread_pages = element.data("thread-pages");
|
||||
content_info = element.data("content-info");
|
||||
user = new DiscussionUser(user_info);
|
||||
DiscussionUtil.setUser(user);
|
||||
window.user = user;
|
||||
Content.loadContentInfos(content_info);
|
||||
discussion = new Discussion(threads, {
|
||||
pages: thread_pages,
|
||||
sort: sort_preference
|
||||
});
|
||||
course_settings = new DiscussionCourseSettings(element.data("course-settings"));
|
||||
// suppressing Do not use 'new' for side effects.
|
||||
/* jshint -W031*/
|
||||
new DiscussionRouter({
|
||||
discussion: discussion,
|
||||
course_settings: course_settings
|
||||
});
|
||||
/* jshint +W031*/
|
||||
return Backbone.history.start({
|
||||
pushState: true,
|
||||
root: "/courses/" + $$course_id + "/discussion/forum/"
|
||||
});
|
||||
}
|
||||
};
|
||||
DiscussionProfileApp = {
|
||||
start: function(elem) {
|
||||
var element, numPages, page, threads, user_info;
|
||||
DiscussionUtil.loadRoles({
|
||||
"Moderator": [],
|
||||
"Administrator": [],
|
||||
"Community TA": []
|
||||
});
|
||||
element = $(elem);
|
||||
window.$$course_id = element.data("course-id");
|
||||
threads = element.data("threads");
|
||||
user_info = element.data("user-info");
|
||||
window.user = new DiscussionUser(user_info);
|
||||
page = element.data("page");
|
||||
numPages = element.data("num-pages");
|
||||
return new DiscussionUserProfileView({
|
||||
el: element,
|
||||
collection: threads,
|
||||
page: page,
|
||||
numPages: numPages
|
||||
});
|
||||
}
|
||||
};
|
||||
$(function() {
|
||||
$("section.discussion").each(function(index, elem) {
|
||||
return DiscussionApp.start(elem);
|
||||
});
|
||||
return $("section.discussion-user-threads").each(function(index, elem) {
|
||||
return DiscussionProfileApp.start(elem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,34 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionCourseSettings = (function(_super) {
|
||||
|
||||
__extends(DiscussionCourseSettings, _super);
|
||||
|
||||
function DiscussionCourseSettings() {
|
||||
return DiscussionCourseSettings.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
return DiscussionCourseSettings;
|
||||
|
||||
})(Backbone.Model);
|
||||
}
|
||||
|
||||
}).call(this);
|
||||
52
common/static/common/js/discussion/models/discussion_user.js
Normal file
52
common/static/common/js/discussion/models/discussion_user.js
Normal file
@@ -0,0 +1,52 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionUser = (function(_super) {
|
||||
|
||||
__extends(DiscussionUser, _super);
|
||||
|
||||
function DiscussionUser() {
|
||||
return DiscussionUser.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionUser.prototype.following = function(thread) {
|
||||
return _.include(this.get('subscribed_thread_ids'), thread.id);
|
||||
};
|
||||
|
||||
DiscussionUser.prototype.voted = function(thread) {
|
||||
return _.include(this.get('upvoted_ids'), thread.id);
|
||||
};
|
||||
|
||||
DiscussionUser.prototype.vote = function(thread) {
|
||||
this.get('upvoted_ids').push(thread.id);
|
||||
return thread.vote();
|
||||
};
|
||||
|
||||
DiscussionUser.prototype.unvote = function(thread) {
|
||||
this.set('upvoted_ids', _.without(this.get('upvoted_ids'), thread.id));
|
||||
return thread.unvote();
|
||||
};
|
||||
|
||||
return DiscussionUser;
|
||||
|
||||
})(Backbone.Model);
|
||||
}
|
||||
|
||||
}).call(this);
|
||||
489
common/static/common/js/discussion/utils.js
Normal file
489
common/static/common/js/discussion/utils.js
Normal file
@@ -0,0 +1,489 @@
|
||||
/* globals $$course_id, Content, Markdown, URI */
|
||||
(function() {
|
||||
'use strict';
|
||||
this.DiscussionUtil = (function() {
|
||||
|
||||
function DiscussionUtil() {
|
||||
}
|
||||
|
||||
DiscussionUtil.wmdEditors = {};
|
||||
|
||||
DiscussionUtil.getTemplate = function(id) {
|
||||
return $("script#" + id).html();
|
||||
};
|
||||
|
||||
DiscussionUtil.setUser = function(user) {
|
||||
this.user = user;
|
||||
};
|
||||
|
||||
DiscussionUtil.getUser = function() {
|
||||
return this.user;
|
||||
};
|
||||
|
||||
DiscussionUtil.loadRoles = function(roles) {
|
||||
this.roleIds = roles;
|
||||
};
|
||||
|
||||
DiscussionUtil.loadRolesFromContainer = function() {
|
||||
return this.loadRoles($("#discussion-container").data("roles"));
|
||||
};
|
||||
|
||||
DiscussionUtil.isStaff = function(userId) {
|
||||
var staff;
|
||||
if (userId === null) {
|
||||
userId = this.user ? this.user.id : void 0;
|
||||
}
|
||||
staff = _.union(this.roleIds.Moderator, this.roleIds.Administrator);
|
||||
return _.include(staff, parseInt(userId));
|
||||
};
|
||||
|
||||
DiscussionUtil.isTA = function(userId) {
|
||||
var ta;
|
||||
if (userId === null) {
|
||||
userId = this.user ? this.user.id : void 0;
|
||||
}
|
||||
ta = _.union(this.roleIds['Community TA']);
|
||||
return _.include(ta, parseInt(userId));
|
||||
};
|
||||
|
||||
DiscussionUtil.isPrivilegedUser = function(userId) {
|
||||
return this.isStaff(userId) || this.isTA(userId);
|
||||
};
|
||||
|
||||
DiscussionUtil.bulkUpdateContentInfo = function(infos) {
|
||||
var id, info, _results;
|
||||
_results = [];
|
||||
for (id in infos) {
|
||||
if (infos.hasOwnProperty(id)) {
|
||||
info = infos[id];
|
||||
_results.push(Content.getContent(id).updateInfo(info));
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
DiscussionUtil.generateDiscussionLink = function(cls, txt, handler) {
|
||||
return $("<a>")
|
||||
.addClass("discussion-link").attr("href", "#")
|
||||
.addClass(cls).html(txt).click(function() {return handler(this);});
|
||||
};
|
||||
|
||||
DiscussionUtil.urlFor = function(name, param, param1, param2) {
|
||||
return {
|
||||
follow_discussion: "/courses/" + $$course_id + "/discussion/" + param + "/follow",
|
||||
unfollow_discussion: "/courses/" + $$course_id + "/discussion/" + param + "/unfollow",
|
||||
create_thread: "/courses/" + $$course_id + "/discussion/" + param + "/threads/create",
|
||||
update_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/update",
|
||||
create_comment: "/courses/" + $$course_id + "/discussion/threads/" + param + "/reply",
|
||||
delete_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/delete",
|
||||
flagAbuse_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/flagAbuse",
|
||||
unFlagAbuse_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/unFlagAbuse",
|
||||
flagAbuse_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/flagAbuse",
|
||||
unFlagAbuse_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/unFlagAbuse",
|
||||
upvote_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/upvote",
|
||||
downvote_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/downvote",
|
||||
pin_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/pin",
|
||||
un_pin_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/unpin",
|
||||
undo_vote_for_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/unvote",
|
||||
follow_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/follow",
|
||||
unfollow_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/unfollow",
|
||||
update_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/update",
|
||||
endorse_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/endorse",
|
||||
create_sub_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/reply",
|
||||
delete_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/delete",
|
||||
upvote_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/upvote",
|
||||
downvote_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/downvote",
|
||||
undo_vote_for_comment: "/courses/" + $$course_id + "/discussion/comments/" + param + "/unvote",
|
||||
upload: "/courses/" + $$course_id + "/discussion/upload",
|
||||
users: "/courses/" + $$course_id + "/discussion/users",
|
||||
search: "/courses/" + $$course_id + "/discussion/forum/search",
|
||||
retrieve_discussion: "/courses/" + $$course_id + "/discussion/forum/" + param + "/inline",
|
||||
retrieve_single_thread: "/courses/" + $$course_id + "/discussion/forum/" + param + "/threads/" + param1,
|
||||
openclose_thread: "/courses/" + $$course_id + "/discussion/threads/" + param + "/close",
|
||||
permanent_link_thread: "/courses/" + $$course_id + "/discussion/forum/" + param + "/threads/" + param1,
|
||||
permanent_link_comment: "/courses/" + $$course_id +
|
||||
"/discussion/forum/" + param + "/threads/" + param1 + "#" + param2,
|
||||
user_profile: "/courses/" + $$course_id + "/discussion/forum/users/" + param,
|
||||
followed_threads: "/courses/" + $$course_id + "/discussion/forum/users/" + param + "/followed",
|
||||
threads: "/courses/" + $$course_id + "/discussion/forum",
|
||||
"enable_notifications": "/notification_prefs/enable/",
|
||||
"disable_notifications": "/notification_prefs/disable/",
|
||||
"notifications_status": "/notification_prefs/status/"
|
||||
}[name];
|
||||
};
|
||||
|
||||
DiscussionUtil.ignoreEnterKey = function(event) {
|
||||
if (event.which === 13) {
|
||||
return event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionUtil.activateOnSpace = function(event, func) {
|
||||
if (event.which === 32) {
|
||||
event.preventDefault();
|
||||
return func(event);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionUtil.makeFocusTrap = function(elem) {
|
||||
return elem.keydown(function(event) {
|
||||
if (event.which === 9) {
|
||||
return event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionUtil.showLoadingIndicator = function(element, takeFocus) {
|
||||
this.$_loading = $(
|
||||
"<div class='loading-animation' tabindex='0'><span class='sr'>" +
|
||||
gettext("Loading content") +
|
||||
"</span></div>"
|
||||
);
|
||||
element.after(this.$_loading);
|
||||
if (takeFocus) {
|
||||
this.makeFocusTrap(this.$_loading);
|
||||
return this.$_loading.focus();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionUtil.hideLoadingIndicator = function() {
|
||||
return this.$_loading.remove();
|
||||
};
|
||||
|
||||
DiscussionUtil.discussionAlert = function(header, body) {
|
||||
var alertDiv, alertTrigger;
|
||||
if ($("#discussion-alert").length === 0) {
|
||||
alertDiv = $(
|
||||
"<div class='modal' role='alertdialog' id='discussion-alert' " +
|
||||
"aria-describedby='discussion-alert-message'/>"
|
||||
).css("display", "none");
|
||||
alertDiv.html(
|
||||
"<div class='inner-wrapper discussion-alert-wrapper'>" +
|
||||
" <button class='close-modal dismiss' title='" + gettext("Close") + "'>" +
|
||||
" <span class='icon fa fa-times' aria-hidden='true'></span>" +
|
||||
" </button>" +
|
||||
" <header><h2/><hr/></header>" +
|
||||
" <p id='discussion-alert-message'/><hr/>" +
|
||||
" <button class='dismiss'>" + gettext("OK") + "</button>" +
|
||||
"</div>"
|
||||
);
|
||||
this.makeFocusTrap(alertDiv.find("button"));
|
||||
alertTrigger = $("<a href='#discussion-alert' id='discussion-alert-trigger'/>").css("display", "none");
|
||||
alertTrigger.leanModal({
|
||||
closeButton: "#discussion-alert .dismiss",
|
||||
overlay: 1,
|
||||
top: 200
|
||||
});
|
||||
$("body").append(alertDiv).append(alertTrigger);
|
||||
}
|
||||
$("#discussion-alert header h2").html(header);
|
||||
$("#discussion-alert p").html(body);
|
||||
$("#discussion-alert-trigger").click();
|
||||
return $("#discussion-alert button").focus();
|
||||
};
|
||||
|
||||
DiscussionUtil.safeAjax = function(params) {
|
||||
var $elem, deferred, request,
|
||||
self = this;
|
||||
$elem = params.$elem;
|
||||
if ($elem && $elem.attr("disabled")) {
|
||||
deferred = $.Deferred();
|
||||
deferred.reject();
|
||||
return deferred.promise();
|
||||
}
|
||||
params.url = URI(params.url).addSearch({
|
||||
ajax: 1
|
||||
});
|
||||
params.beforeSend = function() {
|
||||
if ($elem) {
|
||||
$elem.attr("disabled", "disabled");
|
||||
}
|
||||
if (params.$loading) {
|
||||
if (params.loadingCallback) {
|
||||
return params.loadingCallback.apply(params.$loading);
|
||||
} else {
|
||||
return self.showLoadingIndicator($(params.$loading), params.takeFocus);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!params.error) {
|
||||
params.error = function() {
|
||||
self.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext(
|
||||
"We had some trouble processing your request. Please ensure you have copied any " +
|
||||
"unsaved work and then reload the page.")
|
||||
);
|
||||
};
|
||||
}
|
||||
request = $.ajax(params).always(function() {
|
||||
if ($elem) {
|
||||
$elem.removeAttr("disabled");
|
||||
}
|
||||
if (params.$loading) {
|
||||
if (params.loadedCallback) {
|
||||
return params.loadedCallback.apply(params.$loading);
|
||||
} else {
|
||||
return self.hideLoadingIndicator();
|
||||
}
|
||||
}
|
||||
});
|
||||
return request;
|
||||
};
|
||||
|
||||
DiscussionUtil.updateWithUndo = function(model, updates, safeAjaxParams, errorMsg) {
|
||||
var undo,
|
||||
self = this;
|
||||
if (errorMsg) {
|
||||
safeAjaxParams.error = function() {
|
||||
return self.discussionAlert(gettext("Sorry"), errorMsg);
|
||||
};
|
||||
}
|
||||
undo = _.pick(model.attributes, _.keys(updates));
|
||||
model.set(updates);
|
||||
return this.safeAjax(safeAjaxParams).fail(function() {
|
||||
return model.set(undo);
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionUtil.bindLocalEvents = function($local, eventsHandler) {
|
||||
var event, eventSelector, handler, selector, _ref, _results;
|
||||
_results = [];
|
||||
for (eventSelector in eventsHandler) {
|
||||
if (eventsHandler.hasOwnProperty(eventSelector)){
|
||||
handler = eventsHandler[eventSelector];
|
||||
_ref = eventSelector.split(' ');
|
||||
event = _ref[0];
|
||||
selector = _ref[1];
|
||||
_results.push($local(selector).unbind(event)[event](handler));
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
DiscussionUtil.formErrorHandler = function(errorsField) {
|
||||
return function(xhr, textStatus, error) {
|
||||
var makeErrorElem, response, _i, _len, _ref, _results;
|
||||
makeErrorElem = function(message) {
|
||||
return $("<li>").addClass("post-error").html(message);
|
||||
};
|
||||
errorsField.empty().show();
|
||||
if (xhr.status === 400) {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
if (response.errors) {
|
||||
_ref = response.errors;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
error = _ref[_i];
|
||||
_results.push(errorsField.append(makeErrorElem(error)));
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
} else {
|
||||
return errorsField.append(makeErrorElem(
|
||||
gettext("We had some trouble processing your request. Please try again."))
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
DiscussionUtil.clearFormErrors = function(errorsField) {
|
||||
return errorsField.empty();
|
||||
};
|
||||
|
||||
DiscussionUtil.postMathJaxProcessor = function(text) {
|
||||
var RE_DISPLAYMATH, RE_INLINEMATH;
|
||||
RE_INLINEMATH = /^\$([^\$]*)\$/g;
|
||||
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g;
|
||||
return this.processEachMathAndCode(text, function(s, type) {
|
||||
if (type === 'display') {
|
||||
return s.replace(RE_DISPLAYMATH, function($0, $1) {
|
||||
return "\\[" + $1 + "\\]";
|
||||
});
|
||||
} else if (type === 'inline') {
|
||||
return s.replace(RE_INLINEMATH, function($0, $1) {
|
||||
return "\\(" + $1 + "\\)";
|
||||
});
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionUtil.makeWmdEditor = function($content, $local, cls_identifier) {
|
||||
var appended_id, editor, elem, id, imageUploadUrl, placeholder, _processor;
|
||||
elem = $local("." + cls_identifier);
|
||||
placeholder = elem.data('placeholder');
|
||||
id = elem.attr("data-id");
|
||||
appended_id = "-" + cls_identifier + "-" + id;
|
||||
imageUploadUrl = this.urlFor('upload');
|
||||
_processor = function(self) {
|
||||
return function(text) {
|
||||
return self.postMathJaxProcessor(text);
|
||||
};
|
||||
};
|
||||
editor = Markdown.makeWmdEditor(elem, appended_id, imageUploadUrl, _processor(this));
|
||||
this.wmdEditors["" + cls_identifier + "-" + id] = editor;
|
||||
if (placeholder) {
|
||||
elem.find("#wmd-input" + appended_id).attr('placeholder', placeholder);
|
||||
}
|
||||
return editor;
|
||||
};
|
||||
|
||||
DiscussionUtil.getWmdEditor = function($content, $local, cls_identifier) {
|
||||
var elem, id;
|
||||
elem = $local("." + cls_identifier);
|
||||
id = elem.attr("data-id");
|
||||
return this.wmdEditors["" + cls_identifier + "-" + id];
|
||||
};
|
||||
|
||||
DiscussionUtil.getWmdInput = function($content, $local, cls_identifier) {
|
||||
var elem, id;
|
||||
elem = $local("." + cls_identifier);
|
||||
id = elem.attr("data-id");
|
||||
return $local("#wmd-input-" + cls_identifier + "-" + id);
|
||||
};
|
||||
|
||||
DiscussionUtil.getWmdContent = function($content, $local, cls_identifier) {
|
||||
return this.getWmdInput($content, $local, cls_identifier).val();
|
||||
};
|
||||
|
||||
DiscussionUtil.setWmdContent = function($content, $local, cls_identifier, text) {
|
||||
this.getWmdInput($content, $local, cls_identifier).val(text);
|
||||
return this.getWmdEditor($content, $local, cls_identifier).refreshPreview();
|
||||
};
|
||||
|
||||
DiscussionUtil.processEachMathAndCode = function(text, processor) {
|
||||
var $div, ESCAPED_BACKSLASH, ESCAPED_DOLLAR, RE_DISPLAYMATH, RE_INLINEMATH, cnt, codeArchive, processedText;
|
||||
codeArchive = [];
|
||||
RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m;
|
||||
RE_INLINEMATH = /^([^\$]*?)\$([^\$]+?)\$(.*)$/m;
|
||||
ESCAPED_DOLLAR = '@@ESCAPED_D@@';
|
||||
ESCAPED_BACKSLASH = '@@ESCAPED_B@@';
|
||||
processedText = "";
|
||||
$div = $("<div>").html(text);
|
||||
$div.find("code").each(function(index, code) {
|
||||
codeArchive.push($(code).html());
|
||||
return $(code).html(codeArchive.length - 1);
|
||||
});
|
||||
text = $div.html();
|
||||
text = text.replace(/\\\$/g, ESCAPED_DOLLAR);
|
||||
// suppressing Don't make functions within a loop.
|
||||
/* jshint -W083 */
|
||||
while (true) {
|
||||
if (RE_INLINEMATH.test(text)) {
|
||||
text = text.replace(RE_INLINEMATH, function($0, $1, $2, $3) {
|
||||
processedText += $1 + processor("$" + $2 + "$", 'inline');
|
||||
return $3;
|
||||
});
|
||||
} else if (RE_DISPLAYMATH.test(text)) {
|
||||
text = text.replace(RE_DISPLAYMATH, function($0, $1, $2, $3) {
|
||||
/*
|
||||
bug fix, ordering is off
|
||||
*/
|
||||
processedText = processor("$$" + $2 + "$$", 'display') + processedText;
|
||||
processedText = $1 + processedText;
|
||||
return $3;
|
||||
});
|
||||
} else {
|
||||
processedText += text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* jshint +W083 */
|
||||
text = processedText;
|
||||
text = text.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$');
|
||||
text = text.replace(/\\\\\\\\/g, ESCAPED_BACKSLASH);
|
||||
text = text.replace(/\\begin\{([a-z]*\*?)\}([\s\S]*?)\\end\{\1\}/img, function($0, $1, $2) {
|
||||
return processor(("\\begin{" + $1 + "}") + $2 + ("\\end{" + $1 + "}"));
|
||||
});
|
||||
text = text.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\');
|
||||
$div = $("<div>").html(text);
|
||||
cnt = 0;
|
||||
$div.find("code").each(function(index, code) {
|
||||
$(code).html(processor(codeArchive[cnt], 'code'));
|
||||
return cnt += 1;
|
||||
});
|
||||
text = $div.html();
|
||||
return text;
|
||||
};
|
||||
|
||||
DiscussionUtil.unescapeHighlightTag = function(text) {
|
||||
return text.replace(
|
||||
/\<\;highlight\>\;/g,
|
||||
"<span class='search-highlight'>").replace(/\<\;\/highlight\>\;/g, "</span>"
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionUtil.stripHighlight = function(text) {
|
||||
return text.replace(
|
||||
/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "").replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, ""
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionUtil.stripLatexHighlight = function(text) {
|
||||
return this.processEachMathAndCode(text, this.stripHighlight);
|
||||
};
|
||||
|
||||
DiscussionUtil.markdownWithHighlight = function(text) {
|
||||
var converter;
|
||||
text = text.replace(/^\>\;/gm, ">");
|
||||
converter = Markdown.getMathCompatibleConverter();
|
||||
text = this.unescapeHighlightTag(this.stripLatexHighlight(converter.makeHtml(text)));
|
||||
return text.replace(/^>/gm, ">");
|
||||
};
|
||||
|
||||
DiscussionUtil.abbreviateString = function(text, minLength) {
|
||||
if (text.length < minLength) {
|
||||
return text;
|
||||
} else {
|
||||
while (minLength < text.length && text[minLength] !== ' ') {
|
||||
minLength++;
|
||||
}
|
||||
return text.substr(0, minLength) + gettext('…');
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionUtil.abbreviateHTML = function(html, minLength) {
|
||||
var $result, imagesToReplace, truncated_text;
|
||||
truncated_text = jQuery.truncate(html, {
|
||||
length: minLength,
|
||||
noBreaks: true,
|
||||
ellipsis: gettext('…')
|
||||
});
|
||||
$result = $("<div>" + truncated_text + "</div>");
|
||||
imagesToReplace = $result.find("img:not(:first)");
|
||||
if (imagesToReplace.length > 0) {
|
||||
$result.append("<p><em>Some images in this post have been omitted</em></p>");
|
||||
}
|
||||
imagesToReplace.replaceWith("<em>image omitted</em>");
|
||||
return $result.html();
|
||||
};
|
||||
|
||||
DiscussionUtil.getPaginationParams = function(curPage, numPages, pageUrlFunc) {
|
||||
var delta, maxPage, minPage, pageInfo;
|
||||
delta = 2;
|
||||
minPage = Math.max(curPage - delta, 1);
|
||||
maxPage = Math.min(curPage + delta, numPages);
|
||||
pageInfo = function(pageNum) {
|
||||
return {
|
||||
number: pageNum,
|
||||
url: pageUrlFunc(pageNum)
|
||||
};
|
||||
};
|
||||
return {
|
||||
page: curPage,
|
||||
lowPages: _.range(minPage, curPage).map(pageInfo),
|
||||
highPages: _.range(curPage + 1, maxPage + 1).map(pageInfo),
|
||||
previous: curPage > 1 ? pageInfo(curPage - 1) : null,
|
||||
next: curPage < numPages ? pageInfo(curPage + 1) : null,
|
||||
leftdots: minPage > 2,
|
||||
rightdots: maxPage < numPages - 1,
|
||||
first: minPage > 1 ? pageInfo(1) : null,
|
||||
last: maxPage < numPages ? pageInfo(numPages) : null
|
||||
};
|
||||
};
|
||||
|
||||
return DiscussionUtil;
|
||||
}).call(this);
|
||||
}).call(window);
|
||||
@@ -0,0 +1,520 @@
|
||||
/* globals DiscussionContentView, DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionContentView = (function(_super) {
|
||||
|
||||
__extends(DiscussionContentView, _super);
|
||||
|
||||
function DiscussionContentView() {
|
||||
var self = this;
|
||||
this.setWmdContent = function() {
|
||||
return DiscussionContentView.prototype.setWmdContent.apply(self, arguments);
|
||||
};
|
||||
this.getWmdContent = function() {
|
||||
return DiscussionContentView.prototype.getWmdContent.apply(self, arguments);
|
||||
};
|
||||
this.getWmdEditor = function() {
|
||||
return DiscussionContentView.prototype.getWmdEditor.apply(self, arguments);
|
||||
};
|
||||
this.makeWmdEditor = function() {
|
||||
return DiscussionContentView.prototype.makeWmdEditor.apply(self, arguments);
|
||||
};
|
||||
return DiscussionContentView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionContentView.prototype.events = {
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse",
|
||||
"keydown .discussion-flag-abuse": function(event) {
|
||||
return DiscussionUtil.activateOnSpace(event, this.toggleFlagAbuse);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.attrRenderer = {
|
||||
ability: function(ability) {
|
||||
var action, selector, _ref, _results;
|
||||
_ref = this.abilityRenderer;
|
||||
_results = [];
|
||||
for (action in _ref) {
|
||||
if (_ref.hasOwnProperty(action)){
|
||||
selector = _ref[action];
|
||||
if (!ability[action]) {
|
||||
_results.push(selector.disable.apply(this));
|
||||
} else {
|
||||
_results.push(selector.enable.apply(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.abilityRenderer = {
|
||||
editable: {
|
||||
enable: function() {
|
||||
return this.$(".action-edit").closest(".actions-item").removeClass("is-hidden");
|
||||
},
|
||||
disable: function() {
|
||||
return this.$(".action-edit").closest(".actions-item").addClass("is-hidden");
|
||||
}
|
||||
},
|
||||
can_delete: {
|
||||
enable: function() {
|
||||
return this.$(".action-delete").closest(".actions-item").removeClass("is-hidden");
|
||||
},
|
||||
disable: function() {
|
||||
return this.$(".action-delete").closest(".actions-item").addClass("is-hidden");
|
||||
}
|
||||
},
|
||||
can_openclose: {
|
||||
enable: function() {
|
||||
var self = this;
|
||||
return _.each([".action-close", ".action-pin"], function(selector) {
|
||||
return self.$(selector).closest(".actions-item").removeClass("is-hidden");
|
||||
});
|
||||
},
|
||||
disable: function() {
|
||||
var self = this;
|
||||
return _.each([".action-close", ".action-pin"], function(selector) {
|
||||
return self.$(selector).closest(".actions-item").addClass("is-hidden");
|
||||
});
|
||||
}
|
||||
},
|
||||
can_report: {
|
||||
enable: function() {
|
||||
return this.$(".action-report").closest(".actions-item").removeClass("is-hidden");
|
||||
},
|
||||
disable: function() {
|
||||
return this.$(".action-report").closest(".actions-item").addClass("is-hidden");
|
||||
}
|
||||
},
|
||||
can_vote: {
|
||||
enable: function() {
|
||||
return this.$(".action-vote").closest(".actions-item").removeClass("is-hidden");
|
||||
},
|
||||
disable: function() {
|
||||
return this.$(".action-vote").closest(".actions-item").addClass("is-hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.renderPartialAttrs = function() {
|
||||
var attr, value, _ref, _results;
|
||||
_ref = this.model.changedAttributes();
|
||||
_results = [];
|
||||
for (attr in _ref) {
|
||||
if (_ref.hasOwnProperty(attr)) {
|
||||
value = _ref[attr];
|
||||
if (this.attrRenderer[attr]) {
|
||||
_results.push(this.attrRenderer[attr].apply(this, [value]));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.renderAttrs = function() {
|
||||
var attr, value, _ref, _results;
|
||||
_ref = this.model.attributes;
|
||||
_results = [];
|
||||
for (attr in _ref) {
|
||||
if (_ref.hasOwnProperty(attr)) {
|
||||
value = _ref[attr];
|
||||
if (this.attrRenderer[attr]) {
|
||||
_results.push(this.attrRenderer[attr].apply(this, [value]));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.makeWmdEditor = function(cls_identifier) {
|
||||
if (!this.$el.find(".wmd-panel").length) {
|
||||
return DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), cls_identifier);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.getWmdEditor = function(cls_identifier) {
|
||||
return DiscussionUtil.getWmdEditor(this.$el, $.proxy(this.$, this), cls_identifier);
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.getWmdContent = function(cls_identifier) {
|
||||
return DiscussionUtil.getWmdContent(this.$el, $.proxy(this.$, this), cls_identifier);
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.setWmdContent = function(cls_identifier, text) {
|
||||
return DiscussionUtil.setWmdContent(this.$el, $.proxy(this.$, this), cls_identifier, text);
|
||||
};
|
||||
|
||||
DiscussionContentView.prototype.initialize = function() {
|
||||
var self = this;
|
||||
this.model.bind('change', this.renderPartialAttrs, this);
|
||||
return this.listenTo(this.model, "change:endorsed", function() {
|
||||
if (self.model instanceof Comment) {
|
||||
return self.trigger("comment:endorse");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return DiscussionContentView;
|
||||
|
||||
})(Backbone.View);
|
||||
this.DiscussionContentShowView = (function(_super) {
|
||||
__extends(DiscussionContentShowView, _super);
|
||||
|
||||
function DiscussionContentShowView() {
|
||||
var self = this;
|
||||
this.toggleClose = function() {
|
||||
return DiscussionContentShowView.prototype.toggleClose.apply(self, arguments);
|
||||
};
|
||||
this.toggleReport = function() {
|
||||
return DiscussionContentShowView.prototype.toggleReport.apply(self, arguments);
|
||||
};
|
||||
this.togglePin = function() {
|
||||
return DiscussionContentShowView.prototype.togglePin.apply(self, arguments);
|
||||
};
|
||||
this.toggleVote = function() {
|
||||
return DiscussionContentShowView.prototype.toggleVote.apply(self, arguments);
|
||||
};
|
||||
this.toggleEndorse = function() {
|
||||
return DiscussionContentShowView.prototype.toggleEndorse.apply(self, arguments);
|
||||
};
|
||||
this.toggleFollow = function() {
|
||||
return DiscussionContentShowView.prototype.toggleFollow.apply(self, arguments);
|
||||
};
|
||||
this.handleSecondaryActionBlur = function() {
|
||||
return DiscussionContentShowView.prototype.handleSecondaryActionBlur.apply(self, arguments);
|
||||
};
|
||||
this.handleSecondaryActionEscape = function() {
|
||||
return DiscussionContentShowView.prototype.handleSecondaryActionEscape.apply(self, arguments);
|
||||
};
|
||||
this.toggleSecondaryActions = function() {
|
||||
return DiscussionContentShowView.prototype.toggleSecondaryActions.apply(self, arguments);
|
||||
};
|
||||
this.updateButtonState = function() {
|
||||
return DiscussionContentShowView.prototype.updateButtonState.apply(self, arguments);
|
||||
};
|
||||
return DiscussionContentShowView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionContentShowView.prototype.events = _.reduce(
|
||||
[
|
||||
[".action-follow", "toggleFollow"],
|
||||
[".action-answer", "toggleEndorse"],
|
||||
[".action-endorse", "toggleEndorse"],
|
||||
[".action-vote", "toggleVote"],
|
||||
[".action-more", "toggleSecondaryActions"],
|
||||
[".action-pin", "togglePin"],
|
||||
[".action-edit", "edit"],
|
||||
[".action-delete", "_delete"],
|
||||
[".action-report", "toggleReport"],
|
||||
[".action-close", "toggleClose"]
|
||||
],
|
||||
function(obj, event) {
|
||||
var funcName, selector;
|
||||
selector = event[0];
|
||||
funcName = event[1];
|
||||
obj["click " + selector] = function(event) {
|
||||
return this[funcName](event);
|
||||
};
|
||||
obj["keydown " + selector] = function(event) {
|
||||
return DiscussionUtil.activateOnSpace(event, this[funcName]);
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
DiscussionContentShowView.prototype.updateButtonState = function(selector, checked) {
|
||||
var $button;
|
||||
$button = this.$(selector);
|
||||
$button.toggleClass("is-checked", checked);
|
||||
return $button.attr("aria-checked", checked);
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.attrRenderer = $.extend(
|
||||
{},
|
||||
DiscussionContentView.prototype.attrRenderer,
|
||||
{
|
||||
subscribed: function(subscribed) {
|
||||
return this.updateButtonState(".action-follow", subscribed);
|
||||
},
|
||||
endorsed: function(endorsed) {
|
||||
var $button, selector;
|
||||
selector = this.model.get("thread").get("thread_type") === "question" ?
|
||||
".action-answer" :
|
||||
".action-endorse";
|
||||
this.updateButtonState(selector, endorsed);
|
||||
$button = this.$(selector);
|
||||
$button.closest(".actions-item").toggleClass("is-hidden", !this.model.canBeEndorsed());
|
||||
return $button.toggleClass("is-checked", endorsed);
|
||||
},
|
||||
votes: function(votes) {
|
||||
var button, numVotes, selector, votesHtml, votesCountMsg;
|
||||
selector = ".action-vote";
|
||||
this.updateButtonState(selector, window.user.voted(this.model));
|
||||
button = this.$el.find(selector);
|
||||
numVotes = votes.up_count;
|
||||
votesCountMsg = ngettext(
|
||||
"there is currently %(numVotes)s vote", "there are currently %(numVotes)s votes", numVotes
|
||||
);
|
||||
button.find(".js-sr-vote-count").html(interpolate(votesCountMsg, {numVotes: numVotes }, true));
|
||||
votesHtml = interpolate(ngettext("%(numVotes)s Vote", "%(numVotes)s Votes", numVotes), {
|
||||
numVotes: numVotes
|
||||
}, true);
|
||||
button.find(".vote-count").html(votesHtml);
|
||||
return this.$el.find('.display-vote .vote-count').html(votesHtml);
|
||||
},
|
||||
pinned: function(pinned) {
|
||||
this.updateButtonState(".action-pin", pinned);
|
||||
return this.$(".post-label-pinned").toggleClass("is-hidden", !pinned);
|
||||
},
|
||||
abuse_flaggers: function() {
|
||||
var flagged;
|
||||
flagged = this.model.isFlagged();
|
||||
this.updateButtonState(".action-report", flagged);
|
||||
return this.$(".post-label-reported").toggleClass("is-hidden", !flagged);
|
||||
},
|
||||
closed: function(closed) {
|
||||
this.updateButtonState(".action-close", closed);
|
||||
this.$(".post-label-closed").toggleClass("is-hidden", !closed);
|
||||
return this.$(".display-vote").toggle(closed);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
DiscussionContentShowView.prototype.toggleSecondaryActions = function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.secondaryActionsExpanded = !this.secondaryActionsExpanded;
|
||||
this.$(".action-more").toggleClass("is-expanded", this.secondaryActionsExpanded);
|
||||
this.$(".actions-dropdown")
|
||||
.toggleClass("is-expanded", this.secondaryActionsExpanded)
|
||||
.attr("aria-expanded", this.secondaryActionsExpanded);
|
||||
|
||||
if (this.secondaryActionsExpanded) {
|
||||
if (event.type === "keydown") {
|
||||
this.$(".action-list-item:first").focus();
|
||||
}
|
||||
$("body").on("click", this.toggleSecondaryActions);
|
||||
$("body").on("keydown", this.handleSecondaryActionEscape);
|
||||
return this.$(".action-list-item").on("blur", this.handleSecondaryActionBlur);
|
||||
} else {
|
||||
$("body").off("click", this.toggleSecondaryActions);
|
||||
$("body").off("keydown", this.handleSecondaryActionEscape);
|
||||
return this.$(".action-list-item").off("blur", this.handleSecondaryActionBlur);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.handleSecondaryActionEscape = function(event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.toggleSecondaryActions(event);
|
||||
return this.$(".action-more").focus();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.handleSecondaryActionBlur = function(event) {
|
||||
var self = this;
|
||||
return setTimeout(function() {
|
||||
if (self.secondaryActionsExpanded && self.$(".actions-dropdown :focus").length === 0) {
|
||||
return self.toggleSecondaryActions(event);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.toggleFollow = function(event) {
|
||||
var is_subscribing, msg, url;
|
||||
event.preventDefault();
|
||||
is_subscribing = !this.model.get("subscribed");
|
||||
url = this.model.urlFor(is_subscribing ? "follow" : "unfollow");
|
||||
if (is_subscribing) {
|
||||
msg = gettext("We had some trouble subscribing you to this thread. Please try again.");
|
||||
} else {
|
||||
msg = gettext("We had some trouble unsubscribing you from this thread. Please try again.");
|
||||
}
|
||||
return DiscussionUtil.updateWithUndo(this.model, {
|
||||
"subscribed": is_subscribing
|
||||
}, {
|
||||
url: url,
|
||||
type: "POST",
|
||||
$elem: $(event.currentTarget)
|
||||
}, msg);
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.toggleEndorse = function(event) {
|
||||
var beforeFunc, is_endorsing, msg, updates, url,
|
||||
self = this;
|
||||
event.preventDefault();
|
||||
is_endorsing = !this.model.get("endorsed");
|
||||
url = this.model.urlFor("endorse");
|
||||
updates = {
|
||||
endorsed: is_endorsing,
|
||||
endorsement: is_endorsing ? {
|
||||
username: DiscussionUtil.getUser().get("username"),
|
||||
user_id: DiscussionUtil.getUser().id,
|
||||
time: new Date().toISOString()
|
||||
} : null
|
||||
};
|
||||
if (this.model.get('thread').get('thread_type') === 'question') {
|
||||
if (is_endorsing) {
|
||||
msg = gettext("We had some trouble marking this response as an answer. Please try again.");
|
||||
} else {
|
||||
msg = gettext("We had some trouble removing this response as an answer. Please try again.");
|
||||
}
|
||||
} else {
|
||||
if (is_endorsing) {
|
||||
msg = gettext("We had some trouble marking this response endorsed. Please try again.");
|
||||
} else {
|
||||
msg = gettext("We had some trouble removing this endorsement. Please try again.");
|
||||
}
|
||||
}
|
||||
beforeFunc = function() {
|
||||
return self.trigger("comment:endorse");
|
||||
};
|
||||
return DiscussionUtil.updateWithUndo(this.model, updates, {
|
||||
url: url,
|
||||
type: "POST",
|
||||
data: {
|
||||
endorsed: is_endorsing
|
||||
},
|
||||
beforeSend: beforeFunc,
|
||||
$elem: $(event.currentTarget)
|
||||
}, msg).always(this.trigger("comment:endorse"));
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.toggleVote = function(event) {
|
||||
var is_voting, updates, url, user,
|
||||
self = this;
|
||||
event.preventDefault();
|
||||
user = DiscussionUtil.getUser();
|
||||
is_voting = !user.voted(this.model);
|
||||
url = this.model.urlFor(is_voting ? "upvote" : "unvote");
|
||||
updates = {
|
||||
upvoted_ids: (is_voting ? _.union : _.difference)(user.get('upvoted_ids'), [this.model.id])
|
||||
};
|
||||
return DiscussionUtil.updateWithUndo(user, updates, {
|
||||
url: url,
|
||||
type: "POST",
|
||||
$elem: $(event.currentTarget)
|
||||
}, gettext("We had some trouble saving your vote. Please try again.")).done(function() {
|
||||
if (is_voting) {
|
||||
return self.model.vote();
|
||||
} else {
|
||||
return self.model.unvote();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.togglePin = function(event) {
|
||||
var is_pinning, msg, url;
|
||||
event.preventDefault();
|
||||
is_pinning = !this.model.get("pinned");
|
||||
url = this.model.urlFor(is_pinning ? "pinThread" : "unPinThread");
|
||||
if (is_pinning) {
|
||||
msg = gettext("We had some trouble pinning this thread. Please try again.");
|
||||
} else {
|
||||
msg = gettext("We had some trouble unpinning this thread. Please try again.");
|
||||
}
|
||||
return DiscussionUtil.updateWithUndo(this.model, {
|
||||
pinned: is_pinning
|
||||
}, {
|
||||
url: url,
|
||||
type: "POST",
|
||||
$elem: $(event.currentTarget)
|
||||
}, msg);
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.toggleReport = function(event) {
|
||||
var is_flagging, msg, updates, url;
|
||||
event.preventDefault();
|
||||
if (this.model.isFlagged()) {
|
||||
is_flagging = false;
|
||||
msg = gettext("We had some trouble removing your flag on this post. Please try again.");
|
||||
} else {
|
||||
is_flagging = true;
|
||||
msg = gettext("We had some trouble reporting this post. Please try again.");
|
||||
}
|
||||
url = this.model.urlFor(is_flagging ? "flagAbuse" : "unFlagAbuse");
|
||||
updates = {
|
||||
abuse_flaggers: (is_flagging ? _.union : _.difference)(
|
||||
this.model.get("abuse_flaggers"), [DiscussionUtil.getUser().id]
|
||||
)
|
||||
};
|
||||
return DiscussionUtil.updateWithUndo(this.model, updates, {
|
||||
url: url,
|
||||
type: "POST",
|
||||
$elem: $(event.currentTarget)
|
||||
}, msg);
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.toggleClose = function(event) {
|
||||
var is_closing, msg, updates;
|
||||
event.preventDefault();
|
||||
is_closing = !this.model.get('closed');
|
||||
if (is_closing) {
|
||||
msg = gettext("We had some trouble closing this thread. Please try again.");
|
||||
} else {
|
||||
msg = gettext("We had some trouble reopening this thread. Please try again.");
|
||||
}
|
||||
updates = {
|
||||
closed: is_closing
|
||||
};
|
||||
return DiscussionUtil.updateWithUndo(this.model, updates, {
|
||||
url: this.model.urlFor("close"),
|
||||
type: "POST",
|
||||
data: updates,
|
||||
$elem: $(event.currentTarget)
|
||||
}, msg);
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.getAuthorDisplay = function() {
|
||||
return _.template($("#post-user-display-template").html())({
|
||||
username: this.model.get('username') || null,
|
||||
user_url: this.model.get('user_url'),
|
||||
is_community_ta: this.model.get('community_ta_authored'),
|
||||
is_staff: this.model.get('staff_authored')
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionContentShowView.prototype.getEndorserDisplay = function() {
|
||||
var endorsement;
|
||||
endorsement = this.model.get('endorsement');
|
||||
if (endorsement && endorsement.username) {
|
||||
return _.template($("#post-user-display-template").html())({
|
||||
username: endorsement.username,
|
||||
user_url: DiscussionUtil.urlFor('user_profile', endorsement.user_id),
|
||||
is_community_ta: DiscussionUtil.isTA(endorsement.user_id),
|
||||
is_staff: DiscussionUtil.isStaff(endorsement.user_id)
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return DiscussionContentShowView;
|
||||
|
||||
}).call(this, this.DiscussionContentView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -1,4 +1,5 @@
|
||||
(function(Backbone) {
|
||||
/* globals DiscussionTopicMenuView, DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
if (Backbone) {
|
||||
this.DiscussionThreadEditView = Backbone.View.extend({
|
||||
@@ -50,7 +51,7 @@
|
||||
return this;
|
||||
},
|
||||
|
||||
isTabMode: function () {
|
||||
isTabMode: function() {
|
||||
return this.mode === 'tab';
|
||||
},
|
||||
|
||||
@@ -85,7 +86,7 @@
|
||||
this.model.set(postData).unset('abbreviatedBody');
|
||||
this.trigger('thread:updated');
|
||||
if (this.threadType !== threadType) {
|
||||
this.model.set("thread_type", threadType)
|
||||
this.model.set("thread_type", threadType);
|
||||
this.model.trigger('thread:thread_type_updated');
|
||||
this.trigger('comment:endorse');
|
||||
}
|
||||
@@ -109,4 +110,4 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
}).call(this, Backbone);
|
||||
}).call(window); // jshint ignore:line
|
||||
@@ -0,0 +1,792 @@
|
||||
/* globals Content, Discussion, DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionThreadListView = (function(_super) {
|
||||
__extends(DiscussionThreadListView, _super);
|
||||
|
||||
function DiscussionThreadListView() {
|
||||
var self = this;
|
||||
this.updateEmailNotifications = function() {
|
||||
return DiscussionThreadListView.prototype.updateEmailNotifications.apply(self, arguments);
|
||||
};
|
||||
this.retrieveFollowed = function() {
|
||||
return DiscussionThreadListView.prototype.retrieveFollowed.apply(self, arguments);
|
||||
};
|
||||
this.chooseCohort = function() {
|
||||
return DiscussionThreadListView.prototype.chooseCohort.apply(self, arguments);
|
||||
};
|
||||
this.chooseFilter = function() {
|
||||
return DiscussionThreadListView.prototype.chooseFilter.apply(self, arguments);
|
||||
};
|
||||
this.filterTopics = function() {
|
||||
return DiscussionThreadListView.prototype.filterTopics.apply(self, arguments);
|
||||
};
|
||||
this.toggleBrowseMenu = function() {
|
||||
return DiscussionThreadListView.prototype.toggleBrowseMenu.apply(self, arguments);
|
||||
};
|
||||
this.hideBrowseMenu = function() {
|
||||
return DiscussionThreadListView.prototype.hideBrowseMenu.apply(self, arguments);
|
||||
};
|
||||
this.showBrowseMenu = function() {
|
||||
return DiscussionThreadListView.prototype.showBrowseMenu.apply(self, arguments);
|
||||
};
|
||||
this.isBrowseMenuVisible = function() {
|
||||
return DiscussionThreadListView.prototype.isBrowseMenuVisible.apply(self, arguments);
|
||||
};
|
||||
this.threadRemoved = function() {
|
||||
return DiscussionThreadListView.prototype.threadRemoved.apply(self, arguments);
|
||||
};
|
||||
this.threadSelected = function() {
|
||||
return DiscussionThreadListView.prototype.threadSelected.apply(self, arguments);
|
||||
};
|
||||
this.renderThread = function() {
|
||||
return DiscussionThreadListView.prototype.renderThread.apply(self, arguments);
|
||||
};
|
||||
this.loadMorePages = function() {
|
||||
return DiscussionThreadListView.prototype.loadMorePages.apply(self, arguments);
|
||||
};
|
||||
this.showMetadataAccordingToSort = function() {
|
||||
return DiscussionThreadListView.prototype.showMetadataAccordingToSort.apply(self, arguments);
|
||||
};
|
||||
this.renderThreads = function() {
|
||||
return DiscussionThreadListView.prototype.renderThreads.apply(self, arguments);
|
||||
};
|
||||
this.updateSidebar = function() {
|
||||
return DiscussionThreadListView.prototype.updateSidebar.apply(self, arguments);
|
||||
};
|
||||
this.addAndSelectThread = function() {
|
||||
return DiscussionThreadListView.prototype.addAndSelectThread.apply(self, arguments);
|
||||
};
|
||||
this.reloadDisplayedCollection = function() {
|
||||
return DiscussionThreadListView.prototype.reloadDisplayedCollection.apply(self, arguments);
|
||||
};
|
||||
this.clearSearchAlerts = function() {
|
||||
return DiscussionThreadListView.prototype.clearSearchAlerts.apply(self, arguments);
|
||||
};
|
||||
this.removeSearchAlert = function() {
|
||||
return DiscussionThreadListView.prototype.removeSearchAlert.apply(self, arguments);
|
||||
};
|
||||
this.addSearchAlert = function() {
|
||||
return DiscussionThreadListView.prototype.addSearchAlert.apply(self, arguments);
|
||||
};
|
||||
return DiscussionThreadListView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionThreadListView.prototype.events = {
|
||||
"click .forum-nav-browse": "toggleBrowseMenu",
|
||||
"keypress .forum-nav-browse-filter-input": function(event) {
|
||||
return DiscussionUtil.ignoreEnterKey(event);
|
||||
},
|
||||
"keyup .forum-nav-browse-filter-input": "filterTopics",
|
||||
"click .forum-nav-browse-menu-wrapper": "ignoreClick",
|
||||
"click .forum-nav-browse-title": "selectTopicHandler",
|
||||
"keydown .forum-nav-search-input": "performSearch",
|
||||
"click .fa-search": "performSearch",
|
||||
"change .forum-nav-sort-control": "sortThreads",
|
||||
"click .forum-nav-thread-link": "threadSelected",
|
||||
"click .forum-nav-load-more-link": "loadMorePages",
|
||||
"change .forum-nav-filter-main-control": "chooseFilter",
|
||||
"change .forum-nav-filter-cohort-control": "chooseCohort"
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.initialize = function(options) {
|
||||
var self = this;
|
||||
this.courseSettings = options.courseSettings;
|
||||
this.displayedCollection = new Discussion(this.collection.models, {
|
||||
pages: this.collection.pages
|
||||
});
|
||||
this.collection.on("change", this.reloadDisplayedCollection);
|
||||
this.discussionIds = "";
|
||||
this.collection.on("reset", function(discussion) {
|
||||
var board;
|
||||
board = $(".current-board").html();
|
||||
self.displayedCollection.current_page = discussion.current_page;
|
||||
self.displayedCollection.pages = discussion.pages;
|
||||
return self.displayedCollection.reset(discussion.models);
|
||||
});
|
||||
this.collection.on("add", this.addAndSelectThread);
|
||||
this.sidebar_padding = 10;
|
||||
this.boardName = null;
|
||||
this.template = _.template($("#thread-list-template").html());
|
||||
this.current_search = "";
|
||||
this.mode = 'all';
|
||||
this.searchAlertCollection = new Backbone.Collection([], {
|
||||
model: Backbone.Model
|
||||
});
|
||||
this.searchAlertCollection.on("add", function(searchAlert) {
|
||||
var content;
|
||||
content = _.template($("#search-alert-template").html())({
|
||||
'message': searchAlert.attributes.message,
|
||||
'cid': searchAlert.cid
|
||||
});
|
||||
self.$(".search-alerts").append(content);
|
||||
return self.$("#search-alert-" + searchAlert.cid + " a.dismiss")
|
||||
.bind("click", searchAlert, function(event) {
|
||||
return self.removeSearchAlert(event.data.cid);
|
||||
});
|
||||
});
|
||||
this.searchAlertCollection.on("remove", function(searchAlert) {
|
||||
return self.$("#search-alert-" + searchAlert.cid).remove();
|
||||
});
|
||||
return this.searchAlertCollection.on("reset", function() {
|
||||
return self.$(".search-alerts").empty();
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.addSearchAlert = function(message) {
|
||||
var m;
|
||||
m = new Backbone.Model({
|
||||
"message": message
|
||||
});
|
||||
this.searchAlertCollection.add(m);
|
||||
return m;
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.removeSearchAlert = function(searchAlert) {
|
||||
return this.searchAlertCollection.remove(searchAlert);
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.clearSearchAlerts = function() {
|
||||
return this.searchAlertCollection.reset();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.reloadDisplayedCollection = function(thread) {
|
||||
var active, content, current_el, thread_id;
|
||||
this.clearSearchAlerts();
|
||||
thread_id = thread.get('id');
|
||||
content = this.renderThread(thread);
|
||||
current_el = this.$(".forum-nav-thread[data-id=" + thread_id + "]");
|
||||
active = current_el.has(".forum-nav-thread-link.is-active").length !== 0;
|
||||
current_el.replaceWith(content);
|
||||
this.showMetadataAccordingToSort();
|
||||
if (active) {
|
||||
return this.setActiveThread(thread_id);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
TODO fix this entire chain of events
|
||||
*/
|
||||
|
||||
|
||||
DiscussionThreadListView.prototype.addAndSelectThread = function(thread) {
|
||||
var commentable_id, menuItem,
|
||||
self = this;
|
||||
commentable_id = thread.get("commentable_id");
|
||||
menuItem = this.$(".forum-nav-browse-menu-item[data-discussion-id]").filter(function() {
|
||||
return $(this).data("discussion-id") === commentable_id;
|
||||
});
|
||||
this.setCurrentTopicDisplay(this.getPathText(menuItem));
|
||||
return this.retrieveDiscussion(commentable_id, function() {
|
||||
return self.trigger("thread:created", thread.get('id'));
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.updateSidebar = function() {
|
||||
var amount, browseFilterHeight, discussionBody, discussionBottomOffset, discussionsBodyBottom,
|
||||
discussionsBodyTop, headerHeight, refineBarHeight, scrollTop, sidebar, sidebarHeight, topOffset,
|
||||
windowHeight;
|
||||
scrollTop = $(window).scrollTop();
|
||||
windowHeight = $(window).height();
|
||||
discussionBody = $(".discussion-column");
|
||||
discussionsBodyTop = discussionBody[0] ? discussionBody.offset().top : void 0;
|
||||
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight();
|
||||
sidebar = $(".forum-nav");
|
||||
if (scrollTop > discussionsBodyTop - this.sidebar_padding) {
|
||||
sidebar.css('top', scrollTop - discussionsBodyTop + this.sidebar_padding);
|
||||
} else {
|
||||
sidebar.css('top', '0');
|
||||
}
|
||||
sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, this.sidebar_padding);
|
||||
topOffset = scrollTop + windowHeight;
|
||||
discussionBottomOffset = discussionsBodyBottom + this.sidebar_padding;
|
||||
amount = Math.max(topOffset - discussionBottomOffset, 0);
|
||||
sidebarHeight = sidebarHeight - this.sidebar_padding - amount;
|
||||
sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight());
|
||||
sidebar.css('height', sidebarHeight);
|
||||
headerHeight = this.$(".forum-nav-header").outerHeight();
|
||||
refineBarHeight = this.$(".forum-nav-refine-bar").outerHeight();
|
||||
browseFilterHeight = this.$(".forum-nav-browse-filter").outerHeight();
|
||||
this.$('.forum-nav-thread-list')
|
||||
.css('height', (sidebarHeight - headerHeight - refineBarHeight - 2) + 'px');
|
||||
this.$('.forum-nav-browse-menu')
|
||||
.css('height', (sidebarHeight - headerHeight - browseFilterHeight - 2) + 'px');
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.ignoreClick = function(event) {
|
||||
return event.stopPropagation();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.render = function() {
|
||||
var self = this;
|
||||
this.timer = 0;
|
||||
this.$el.html(this.template({
|
||||
isCohorted: this.courseSettings.get("is_cohorted"),
|
||||
isPrivilegedUser: DiscussionUtil.isPrivilegedUser()
|
||||
}));
|
||||
this.$(".forum-nav-sort-control option").removeProp("selected");
|
||||
this.$(".forum-nav-sort-control option[value=" + this.collection.sort_preference + "]")
|
||||
.prop("selected", true);
|
||||
$(window).bind("load scroll resize", this.updateSidebar);
|
||||
this.displayedCollection.on("reset", this.renderThreads);
|
||||
this.displayedCollection.on("thread:remove", this.renderThreads);
|
||||
this.displayedCollection.on("change:commentable_id", function() {
|
||||
if (self.mode === "commentables") {
|
||||
return self.retrieveDiscussions(self.discussionIds.split(","));
|
||||
}
|
||||
});
|
||||
this.renderThreads();
|
||||
return this;
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.renderThreads = function() {
|
||||
var content, rendered, thread, _i, _len, _ref;
|
||||
this.$(".forum-nav-thread-list").html("");
|
||||
rendered = $("<div></div>");
|
||||
_ref = this.displayedCollection.models;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
thread = _ref[_i];
|
||||
content = this.renderThread(thread);
|
||||
rendered.append(content);
|
||||
}
|
||||
this.$(".forum-nav-thread-list").html(rendered.html());
|
||||
this.showMetadataAccordingToSort();
|
||||
this.renderMorePages();
|
||||
this.updateSidebar();
|
||||
return this.trigger("threads:rendered");
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.showMetadataAccordingToSort = function() {
|
||||
var commentCounts, voteCounts;
|
||||
voteCounts = this.$(".forum-nav-thread-votes-count");
|
||||
commentCounts = this.$(".forum-nav-thread-comments-count");
|
||||
voteCounts.hide();
|
||||
commentCounts.hide();
|
||||
switch (this.$(".forum-nav-sort-control").val()) {
|
||||
case "activity":
|
||||
case "comments":
|
||||
return commentCounts.show();
|
||||
case "votes":
|
||||
return voteCounts.show();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.renderMorePages = function() {
|
||||
if (this.displayedCollection.hasMorePages()) {
|
||||
return this.$(".forum-nav-thread-list")
|
||||
.append(
|
||||
"<li class='forum-nav-load-more'>" +
|
||||
" <a href='#' class='forum-nav-load-more-link'>" + gettext("Load more") + "</a>" +
|
||||
"</li>"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.getLoadingContent = function(srText) {
|
||||
return '<div class="forum-nav-loading" tabindex="0">' +
|
||||
' <span class="icon fa fa-spinner fa-spin"/><span class="sr" role="alert">' + srText + '</span>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.loadMorePages = function(event) {
|
||||
var error, lastThread, loadMoreElem, loadingElem, options, _ref,
|
||||
self = this;
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
loadMoreElem = this.$(".forum-nav-load-more");
|
||||
loadMoreElem.html(this.getLoadingContent(gettext("Loading more threads")));
|
||||
loadingElem = loadMoreElem.find(".forum-nav-loading");
|
||||
DiscussionUtil.makeFocusTrap(loadingElem);
|
||||
loadingElem.focus();
|
||||
options = {
|
||||
filter: this.filter
|
||||
};
|
||||
switch (this.mode) {
|
||||
case 'search':
|
||||
options.search_text = this.current_search;
|
||||
if (this.group_id) {
|
||||
options.group_id = this.group_id;
|
||||
}
|
||||
break;
|
||||
case 'followed':
|
||||
options.user_id = window.user.id;
|
||||
break;
|
||||
case 'commentables':
|
||||
options.commentable_ids = this.discussionIds;
|
||||
if (this.group_id) {
|
||||
options.group_id = this.group_id;
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
if (this.group_id) {
|
||||
options.group_id = this.group_id;
|
||||
}
|
||||
}
|
||||
_ref = this.collection.last();
|
||||
lastThread = _ref ? _ref.get('id') : void 0;
|
||||
if (lastThread) {
|
||||
this.once("threads:rendered", function() {
|
||||
var classSelector =
|
||||
".forum-nav-thread[data-id='" + lastThread + "'] + .forum-nav-thread " +
|
||||
".forum-nav-thread-link";
|
||||
return $(classSelector).focus();
|
||||
});
|
||||
} else {
|
||||
this.once("threads:rendered", function() {
|
||||
var _ref1 = $(".forum-nav-thread-link").first();
|
||||
return _ref1 ? _ref1.focus() : void 0;
|
||||
});
|
||||
}
|
||||
error = function() {
|
||||
self.renderThreads();
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"), gettext("We had some trouble loading more threads. Please try again.")
|
||||
);
|
||||
};
|
||||
return this.collection.retrieveAnotherPage(this.mode, options, {
|
||||
sort_key: this.$(".forum-nav-sort-control").val()
|
||||
}, error);
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.renderThread = function(thread) {
|
||||
var content, unreadCount;
|
||||
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()));
|
||||
unreadCount = thread.get('unread_comments_count') + (thread.get("read") ? 0 : 1);
|
||||
if (unreadCount > 0) {
|
||||
content.find('.forum-nav-thread-comments-count').attr(
|
||||
"data-tooltip",
|
||||
interpolate(
|
||||
ngettext('%(unread_count)s new comment', '%(unread_count)s new comments', unreadCount),
|
||||
{unread_count: unreadCount},
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.threadSelected = function(e) {
|
||||
var thread_id;
|
||||
thread_id = $(e.target).closest(".forum-nav-thread").attr("data-id");
|
||||
this.setActiveThread(thread_id);
|
||||
this.trigger("thread:selected", thread_id);
|
||||
return false;
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.threadRemoved = function(thread_id) {
|
||||
return this.trigger("thread:removed", thread_id);
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.setActiveThread = function(thread_id) {
|
||||
this.$(".forum-nav-thread-link").find(".sr").remove();
|
||||
this.$(".forum-nav-thread[data-id!='" + thread_id + "'] .forum-nav-thread-link")
|
||||
.removeClass("is-active");
|
||||
this.$(".forum-nav-thread[data-id='" + thread_id + "'] .forum-nav-thread-link")
|
||||
.addClass("is-active").find(".forum-nav-thread-wrapper-1")
|
||||
.prepend('<span class="sr">' + gettext("Current conversation") + '</span>');
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.goHome = function() {
|
||||
var thread_id, url;
|
||||
this.template = _.template($("#discussion-home-template").html());
|
||||
$(".forum-content").html(this.template);
|
||||
$(".forum-nav-thread-list a").removeClass("is-active").find(".sr").remove();
|
||||
$("input.email-setting").bind("click", this.updateEmailNotifications);
|
||||
url = DiscussionUtil.urlFor("notifications_status", window.user.get("id"));
|
||||
DiscussionUtil.safeAjax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
success: function(response) {
|
||||
if (response.status) {
|
||||
return $('input.email-setting').attr('checked', 'checked');
|
||||
} else {
|
||||
return $('input.email-setting').removeAttr('checked');
|
||||
}
|
||||
}
|
||||
});
|
||||
thread_id = null;
|
||||
return this.trigger("thread:removed");
|
||||
/*
|
||||
select all threads
|
||||
*/
|
||||
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.isBrowseMenuVisible = function() {
|
||||
return this.$(".forum-nav-browse-menu-wrapper").is(":visible");
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.showBrowseMenu = function() {
|
||||
if (!this.isBrowseMenuVisible()) {
|
||||
this.$(".forum-nav-browse").addClass("is-active");
|
||||
this.$(".forum-nav-browse-menu-wrapper").show();
|
||||
this.$(".forum-nav-thread-list-wrapper").hide();
|
||||
$(".forum-nav-browse-filter-input").focus();
|
||||
$("body").bind("click", this.hideBrowseMenu);
|
||||
return this.updateSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.hideBrowseMenu = function() {
|
||||
if (this.isBrowseMenuVisible()) {
|
||||
this.$(".forum-nav-browse").removeClass("is-active");
|
||||
this.$(".forum-nav-browse-menu-wrapper").hide();
|
||||
this.$(".forum-nav-thread-list-wrapper").show();
|
||||
$("body").unbind("click", this.hideBrowseMenu);
|
||||
return this.updateSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.toggleBrowseMenu = function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this.isBrowseMenuVisible()) {
|
||||
return this.hideBrowseMenu();
|
||||
} else {
|
||||
return this.showBrowseMenu();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.getPathText = function(item) {
|
||||
var path, pathTitles;
|
||||
path = item.parents(".forum-nav-browse-menu-item").andSelf();
|
||||
pathTitles = path.children(".forum-nav-browse-title").map(function(i, elem) {
|
||||
return $(elem).text();
|
||||
}).get();
|
||||
return pathTitles.join(" / ");
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.filterTopics = function(event) {
|
||||
var items, query,
|
||||
self = this;
|
||||
query = $(event.target).val();
|
||||
items = this.$(".forum-nav-browse-menu-item");
|
||||
if (query.length === 0) {
|
||||
return items.show();
|
||||
} else {
|
||||
items.hide();
|
||||
return items.each(function(i, item) {
|
||||
var path, pathText;
|
||||
item = $(item);
|
||||
if (!item.is(":visible")) {
|
||||
pathText = self.getPathText(item).toLowerCase();
|
||||
if (query.split(" ").every(function(term) {
|
||||
return pathText.search(term.toLowerCase()) !== -1;
|
||||
})) {
|
||||
path = item.parents(".forum-nav-browse-menu-item").andSelf();
|
||||
return path.add(item.find(".forum-nav-browse-menu-item")).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.setCurrentTopicDisplay = function(text) {
|
||||
return this.$(".forum-nav-browse-current").text(this.fitName(text));
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.getNameWidth = function(name) {
|
||||
var test, width;
|
||||
test = $("<div>");
|
||||
test.css({
|
||||
"font-size": this.$(".forum-nav-browse-current").css('font-size'),
|
||||
opacity: 0,
|
||||
position: 'absolute',
|
||||
left: -1000,
|
||||
top: -1000
|
||||
});
|
||||
$("body").append(test);
|
||||
test.html(name);
|
||||
width = test.width();
|
||||
test.remove();
|
||||
return width;
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.fitName = function(name) {
|
||||
var partialName, path, prefix, rawName, width, x;
|
||||
this.maxNameWidth = this.$(".forum-nav-browse").width() -
|
||||
this.$(".forum-nav-browse .icon").outerWidth(true) -
|
||||
this.$(".forum-nav-browse-drop-arrow").outerWidth(true);
|
||||
width = this.getNameWidth(name);
|
||||
if (width < this.maxNameWidth) {
|
||||
return name;
|
||||
}
|
||||
path = (function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = name.split("/");
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
x = _ref[_i];
|
||||
_results.push(x.replace(/^\s+|\s+$/g, ""));
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
prefix = "";
|
||||
while (path.length > 1) {
|
||||
prefix = gettext("…") + "/";
|
||||
path.shift();
|
||||
partialName = prefix + path.join("/");
|
||||
if (this.getNameWidth(partialName) < this.maxNameWidth) {
|
||||
return partialName;
|
||||
}
|
||||
}
|
||||
rawName = path[0];
|
||||
name = prefix + rawName;
|
||||
while (this.getNameWidth(name) > this.maxNameWidth) {
|
||||
rawName = rawName.slice(0, rawName.length - 1);
|
||||
name = prefix + rawName + gettext("…");
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.selectTopicHandler = function(event) {
|
||||
event.preventDefault();
|
||||
return this.selectTopic($(event.target));
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.selectTopic = function($target) {
|
||||
var allItems, discussionIds, item;
|
||||
this.hideBrowseMenu();
|
||||
this.clearSearch();
|
||||
item = $target.closest('.forum-nav-browse-menu-item');
|
||||
this.setCurrentTopicDisplay(this.getPathText(item));
|
||||
if (item.hasClass("forum-nav-browse-menu-all")) {
|
||||
this.discussionIds = "";
|
||||
this.$('.forum-nav-filter-cohort').show();
|
||||
return this.retrieveAllThreads();
|
||||
} else if (item.hasClass("forum-nav-browse-menu-following")) {
|
||||
this.retrieveFollowed();
|
||||
return this.$('.forum-nav-filter-cohort').hide();
|
||||
} else {
|
||||
allItems = item.find(".forum-nav-browse-menu-item").andSelf();
|
||||
discussionIds = allItems.filter("[data-discussion-id]").map(function(i, elem) {
|
||||
return $(elem).data("discussion-id");
|
||||
}).get();
|
||||
this.retrieveDiscussions(discussionIds);
|
||||
return this.$(".forum-nav-filter-cohort").toggle(item.data('cohorted') === true);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.chooseFilter = function() {
|
||||
this.filter = $(".forum-nav-filter-main-control :selected").val();
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.chooseCohort = function() {
|
||||
this.group_id = this.$('.forum-nav-filter-cohort-control :selected').val();
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.retrieveDiscussion = function(discussion_id, callback) {
|
||||
var url, self = this;
|
||||
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id);
|
||||
return DiscussionUtil.safeAjax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
success: function(response) {
|
||||
self.collection.current_page = response.page;
|
||||
self.collection.pages = response.num_pages;
|
||||
self.collection.reset(response.discussion_data);
|
||||
Content.loadContentInfos(response.annotated_content_info);
|
||||
self.displayedCollection.reset(self.collection.models);
|
||||
if (callback) {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.retrieveDiscussions = function(discussion_ids) {
|
||||
this.discussionIds = discussion_ids.join(',');
|
||||
this.mode = 'commentables';
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.retrieveAllThreads = function() {
|
||||
this.mode = 'all';
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.retrieveFirstPage = function(event) {
|
||||
this.collection.current_page = 0;
|
||||
this.collection.reset();
|
||||
return this.loadMorePages(event);
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.sortThreads = function(event) {
|
||||
this.displayedCollection.setSortComparator(this.$(".forum-nav-sort-control").val());
|
||||
return this.retrieveFirstPage(event);
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.performSearch = function(event) {
|
||||
/*
|
||||
event.which 13 represent the Enter button
|
||||
*/
|
||||
|
||||
var text;
|
||||
if (event.which === 13 || event.type === 'click') {
|
||||
event.preventDefault();
|
||||
this.hideBrowseMenu();
|
||||
this.setCurrentTopicDisplay(gettext("Search Results"));
|
||||
text = this.$(".forum-nav-search-input").val();
|
||||
return this.searchFor(text);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.searchFor = function(text) {
|
||||
var url,
|
||||
self = this;
|
||||
this.clearSearchAlerts();
|
||||
this.clearFilters();
|
||||
this.mode = 'search';
|
||||
this.current_search = text;
|
||||
url = DiscussionUtil.urlFor("search");
|
||||
/*
|
||||
TODO: This might be better done by setting discussion.current_page=0 and
|
||||
calling discussion.loadMorePages
|
||||
Mainly because this currently does not reset any pagination variables which could cause problems.
|
||||
This doesn't use pagination either.
|
||||
*/
|
||||
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: this.$(".forum-nav-search-input"),
|
||||
data: {
|
||||
text: text
|
||||
},
|
||||
url: url,
|
||||
type: "GET",
|
||||
dataType: 'json',
|
||||
$loading: $,
|
||||
loadingCallback: function() {
|
||||
return self.$(".forum-nav-thread-list")
|
||||
.html(
|
||||
"<li class='forum-nav-load-more'>" +
|
||||
self.getLoadingContent(gettext("Loading thread list")) +
|
||||
"</li>"
|
||||
);
|
||||
},
|
||||
loadedCallback: function() {
|
||||
return self.$(".forum-nav-thread-list .forum-nav-load-more").remove();
|
||||
},
|
||||
success: function(response, textStatus) {
|
||||
var message, noResponseMsg;
|
||||
if (textStatus === 'success') {
|
||||
self.collection.reset(response.discussion_data);
|
||||
Content.loadContentInfos(response.annotated_content_info);
|
||||
self.collection.current_page = response.page;
|
||||
self.collection.pages = response.num_pages;
|
||||
if (!_.isNull(response.corrected_text)) {
|
||||
noResponseMsg = _.escape(
|
||||
gettext(
|
||||
'No results found for %(original_query)s. ' +
|
||||
'Showing results for %(suggested_query)s.'
|
||||
)
|
||||
);
|
||||
message = interpolate(
|
||||
noResponseMsg,
|
||||
{
|
||||
"original_query": "<em>" + _.escape(text) + "</em>",
|
||||
"suggested_query": "<em>" + response.corrected_text + "</em>"
|
||||
},
|
||||
true
|
||||
);
|
||||
self.addSearchAlert(message);
|
||||
} else if (response.discussion_data.length === 0) {
|
||||
self.addSearchAlert(gettext('No threads matched your query.'));
|
||||
}
|
||||
self.displayedCollection.reset(self.collection.models);
|
||||
if (text) {
|
||||
return self.searchForUser(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.searchForUser = function(text) {
|
||||
var self = this;
|
||||
return DiscussionUtil.safeAjax({
|
||||
data: {
|
||||
username: text
|
||||
},
|
||||
url: DiscussionUtil.urlFor("users"),
|
||||
type: "GET",
|
||||
dataType: 'json',
|
||||
error: function() {
|
||||
},
|
||||
success: function(response) {
|
||||
var message;
|
||||
if (response.users.length > 0) {
|
||||
message = interpolate(_.escape(gettext('Show posts by %(username)s.')), {
|
||||
"username": _.template('<a class="link-jump" href="<%= url %>"><%- username %></a>')({
|
||||
url: DiscussionUtil.urlFor("user_profile", response.users[0].id),
|
||||
username: response.users[0].username
|
||||
})
|
||||
}, true);
|
||||
return self.addSearchAlert(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.clearSearch = function() {
|
||||
this.$(".forum-nav-search-input").val("");
|
||||
this.current_search = "";
|
||||
return this.clearSearchAlerts();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.clearFilters = function() {
|
||||
this.$(".forum-nav-filter-main-control").val("all");
|
||||
return this.$(".forum-nav-filter-cohort-control").val("all");
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.retrieveFollowed = function() {
|
||||
this.mode = 'followed';
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.updateEmailNotifications = function() {
|
||||
if ($('input.email-setting').attr('checked')) {
|
||||
return DiscussionUtil.safeAjax({
|
||||
url: DiscussionUtil.urlFor("enable_notifications"),
|
||||
type: "POST",
|
||||
error: function() {
|
||||
return $('input.email-setting').removeAttr('checked');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return DiscussionUtil.safeAjax({
|
||||
url: DiscussionUtil.urlFor("disable_notifications"),
|
||||
type: "POST",
|
||||
error: function() {
|
||||
return $('input.email-setting').attr('checked', 'checked');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return DiscussionThreadListView;
|
||||
|
||||
}).call(this, Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,72 @@
|
||||
/* globals DiscussionUtil, MathJax */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionThreadProfileView = (function(_super) {
|
||||
|
||||
__extends(DiscussionThreadProfileView, _super);
|
||||
|
||||
function DiscussionThreadProfileView() {
|
||||
return DiscussionThreadProfileView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionThreadProfileView.prototype.render = function() {
|
||||
var element, params;
|
||||
this.convertMath();
|
||||
this.abbreviateBody();
|
||||
params = $.extend(this.model.toJSON(), {
|
||||
permalink: this.model.urlFor('retrieve')
|
||||
});
|
||||
if (!this.model.get('anonymous')) {
|
||||
params = $.extend(params, {
|
||||
user: {
|
||||
username: this.model.username,
|
||||
user_url: this.model.user_url
|
||||
}
|
||||
});
|
||||
}
|
||||
this.$el.html(_.template($("#profile-thread-template").html())(params));
|
||||
this.$("span.timeago").timeago();
|
||||
element = this.$(".post-body");
|
||||
if (typeof MathJax !== "undefined" && MathJax !== null) {
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub, element[0]]);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
DiscussionThreadProfileView.prototype.convertMath = function() {
|
||||
return this.model.set(
|
||||
'markdownBody',
|
||||
DiscussionUtil.postMathJaxProcessor(DiscussionUtil.markdownWithHighlight(this.model.get('body')))
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionThreadProfileView.prototype.abbreviateBody = function() {
|
||||
var abbreviated;
|
||||
abbreviated = DiscussionUtil.abbreviateHTML(this.model.get('markdownBody'), 140);
|
||||
return this.model.set('abbreviatedBody', abbreviated);
|
||||
};
|
||||
|
||||
return DiscussionThreadProfileView;
|
||||
|
||||
})(Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,91 @@
|
||||
/* globals DiscussionUtil, DiscussionContentShowView, MathJax */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionThreadShowView = (function(_super) {
|
||||
|
||||
__extends(DiscussionThreadShowView, _super);
|
||||
|
||||
function DiscussionThreadShowView() {
|
||||
return DiscussionThreadShowView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionThreadShowView.prototype.initialize = function(options) {
|
||||
var _ref;
|
||||
DiscussionThreadShowView.__super__.initialize.call(this);
|
||||
this.mode = options.mode || "inline";
|
||||
if ((_ref = this.mode) !== "tab" && _ref !== "inline") {
|
||||
throw new Error("invalid mode: " + this.mode);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadShowView.prototype.renderTemplate = function() {
|
||||
var context;
|
||||
this.template = _.template($("#thread-show-template").html());
|
||||
context = $.extend({
|
||||
mode: this.mode,
|
||||
flagged: this.model.isFlagged(),
|
||||
author_display: this.getAuthorDisplay(),
|
||||
cid: this.model.cid,
|
||||
readOnly: $('.discussion-module').data('read-only')
|
||||
}, this.model.attributes);
|
||||
return this.template(context);
|
||||
};
|
||||
|
||||
DiscussionThreadShowView.prototype.render = function() {
|
||||
this.$el.html(this.renderTemplate());
|
||||
this.delegateEvents();
|
||||
this.renderAttrs();
|
||||
this.$("span.timeago").timeago();
|
||||
this.convertMath();
|
||||
this.highlight(this.$(".post-body"));
|
||||
this.highlight(this.$("h1,h3"));
|
||||
return this;
|
||||
};
|
||||
|
||||
DiscussionThreadShowView.prototype.convertMath = function() {
|
||||
var element;
|
||||
element = this.$(".post-body");
|
||||
element.html(DiscussionUtil.postMathJaxProcessor(DiscussionUtil.markdownWithHighlight(element.text())));
|
||||
if (typeof MathJax !== "undefined" && MathJax !== null) {
|
||||
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element[0]]);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadShowView.prototype.edit = function(event) {
|
||||
return this.trigger("thread:edit", event);
|
||||
};
|
||||
|
||||
DiscussionThreadShowView.prototype._delete = function(event) {
|
||||
return this.trigger("thread:_delete", event);
|
||||
};
|
||||
|
||||
DiscussionThreadShowView.prototype.highlight = function(el) {
|
||||
if (el.html()) {
|
||||
return el.html(el.html().replace(/<mark>/g, "<mark>").replace(/<\/mark>/g, "</mark>"));
|
||||
}
|
||||
};
|
||||
|
||||
return DiscussionThreadShowView;
|
||||
|
||||
})(DiscussionContentShowView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,489 @@
|
||||
/* globals
|
||||
Comments, Content, DiscussionContentView, DiscussionThreadEditView,
|
||||
DiscussionThreadShowView, DiscussionUtil, ThreadResponseView
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionThreadView = (function(_super) {
|
||||
var INITIAL_RESPONSE_PAGE_SIZE, SUBSEQUENT_RESPONSE_PAGE_SIZE;
|
||||
|
||||
__extends(DiscussionThreadView, _super);
|
||||
|
||||
function DiscussionThreadView() {
|
||||
var self = this;
|
||||
this._delete = function() {
|
||||
return DiscussionThreadView.prototype._delete.apply(self, arguments);
|
||||
};
|
||||
this.closeEditView = function() {
|
||||
return DiscussionThreadView.prototype.closeEditView.apply(self, arguments);
|
||||
};
|
||||
this.edit = function() {
|
||||
return DiscussionThreadView.prototype.edit.apply(self, arguments);
|
||||
};
|
||||
this.endorseThread = function() {
|
||||
return DiscussionThreadView.prototype.endorseThread.apply(self, arguments);
|
||||
};
|
||||
this.addComment = function() {
|
||||
return DiscussionThreadView.prototype.addComment.apply(self, arguments);
|
||||
};
|
||||
this.renderAddResponseButton = function() {
|
||||
return DiscussionThreadView.prototype.renderAddResponseButton.apply(self, arguments);
|
||||
};
|
||||
this.renderResponseToList = function() {
|
||||
return DiscussionThreadView.prototype.renderResponseToList.apply(self, arguments);
|
||||
};
|
||||
this.renderResponseCountAndPagination = function() {
|
||||
return DiscussionThreadView.prototype.renderResponseCountAndPagination.apply(self, arguments);
|
||||
};
|
||||
return DiscussionThreadView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
INITIAL_RESPONSE_PAGE_SIZE = 25;
|
||||
|
||||
SUBSEQUENT_RESPONSE_PAGE_SIZE = 100;
|
||||
|
||||
DiscussionThreadView.prototype.events = {
|
||||
"click .discussion-submit-post": "submitComment",
|
||||
"click .add-response-btn": "scrollToAddResponse",
|
||||
"click .forum-thread-expand": "expand",
|
||||
"click .forum-thread-collapse": "collapse"
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.$ = function(selector) {
|
||||
return this.$el.find(selector);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.isQuestion = function() {
|
||||
return this.model.get("thread_type") === "question";
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.initialize = function(options) {
|
||||
var _ref,
|
||||
self = this;
|
||||
DiscussionThreadView.__super__.initialize.call(this);
|
||||
this.mode = options.mode || "inline";
|
||||
this.context = options.context || "course";
|
||||
this.options = _.extend({}, options);
|
||||
if ((_ref = this.mode) !== "tab" && _ref !== "inline") {
|
||||
throw new Error("invalid mode: " + this.mode);
|
||||
}
|
||||
this.readOnly = $(".discussion-module").data('read-only');
|
||||
this.model.collection.on("reset", function(collection) {
|
||||
var id;
|
||||
id = self.model.get("id");
|
||||
if (collection.get(id)) {
|
||||
self.model = collection.get(id);
|
||||
}
|
||||
});
|
||||
this.createShowView();
|
||||
this.responses = new Comments();
|
||||
this.loadedResponses = false;
|
||||
if (this.isQuestion()) {
|
||||
this.markedAnswers = new Comments();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.rerender = function() {
|
||||
if (this.showView) {
|
||||
this.showView.undelegateEvents();
|
||||
}
|
||||
this.undelegateEvents();
|
||||
this.$el.empty();
|
||||
this.initialize({
|
||||
mode: this.mode,
|
||||
model: this.model,
|
||||
el: this.el,
|
||||
course_settings: this.options.course_settings,
|
||||
topicId: this.topicId
|
||||
});
|
||||
return this.render();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderTemplate = function() {
|
||||
var container, templateData;
|
||||
this.template = _.template($("#thread-template").html());
|
||||
container = $("#discussion-container");
|
||||
if (!container.length) {
|
||||
container = $(".discussion-module");
|
||||
}
|
||||
templateData = _.extend(this.model.toJSON(), {
|
||||
readOnly: this.readOnly,
|
||||
can_create_comment: container.data("user-create-comment")
|
||||
});
|
||||
return this.template(templateData);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.render = function() {
|
||||
var self = this;
|
||||
this.$el.html(this.renderTemplate());
|
||||
this.delegateEvents();
|
||||
this.renderShowView();
|
||||
this.renderAttrs();
|
||||
this.$("span.timeago").timeago();
|
||||
this.makeWmdEditor("reply-body");
|
||||
this.renderAddResponseButton();
|
||||
this.responses.on("add", function(response) {
|
||||
return self.renderResponseToList(response, ".js-response-list", {});
|
||||
});
|
||||
if (this.isQuestion()) {
|
||||
this.markedAnswers.on("add", function(response) {
|
||||
return self.renderResponseToList(response, ".js-marked-answer-list", {
|
||||
collapseComments: true
|
||||
});
|
||||
});
|
||||
}
|
||||
if (this.mode === "tab") {
|
||||
setTimeout(function() {
|
||||
return self.loadInitialResponses();
|
||||
}, 100);
|
||||
return this.$(".post-tools").hide();
|
||||
} else {
|
||||
return this.collapse();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.attrRenderer = $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
closed: function(closed) {
|
||||
this.$(".discussion-reply-new").toggle(!closed);
|
||||
this.$('.comment-form').closest('li').toggle(!closed);
|
||||
this.$(".action-vote").toggle(!closed);
|
||||
this.$(".display-vote").toggle(closed);
|
||||
return this.renderAddResponseButton();
|
||||
}
|
||||
});
|
||||
|
||||
DiscussionThreadView.prototype.expand = function(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.$el.addClass("expanded");
|
||||
this.$el.find(".post-body").text(this.model.get("body"));
|
||||
this.showView.convertMath();
|
||||
this.$el.find(".forum-thread-expand").hide();
|
||||
this.$el.find(".forum-thread-collapse").show();
|
||||
this.$el.find(".post-extended-content").show();
|
||||
if (!this.loadedResponses) {
|
||||
return this.loadInitialResponses();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.collapse = function(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.$el.removeClass("expanded");
|
||||
this.$el.find(".post-body").text(this.getAbbreviatedBody());
|
||||
this.showView.convertMath();
|
||||
this.$el.find(".forum-thread-expand").show();
|
||||
this.$el.find(".forum-thread-collapse").hide();
|
||||
return this.$el.find(".post-extended-content").hide();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.getAbbreviatedBody = function() {
|
||||
var abbreviated, cached;
|
||||
cached = this.model.get("abbreviatedBody");
|
||||
if (cached) {
|
||||
return cached;
|
||||
} else {
|
||||
abbreviated = DiscussionUtil.abbreviateString(this.model.get("body"), 140);
|
||||
this.model.set("abbreviatedBody", abbreviated);
|
||||
return abbreviated;
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.cleanup = function() {
|
||||
if (this.responsesRequest) {
|
||||
return this.responsesRequest.abort();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.loadResponses = function(responseLimit, elem, firstLoad) {
|
||||
var takeFocus,
|
||||
self = this;
|
||||
takeFocus = this.mode === "tab" ? false : true;
|
||||
this.responsesRequest = DiscussionUtil.safeAjax({
|
||||
url: DiscussionUtil.urlFor(
|
||||
'retrieve_single_thread', this.model.get('commentable_id'), this.model.id
|
||||
),
|
||||
data: {
|
||||
resp_skip: this.responses.size(),
|
||||
resp_limit: responseLimit ? responseLimit : void 0
|
||||
},
|
||||
$elem: elem,
|
||||
$loading: elem,
|
||||
takeFocus: takeFocus,
|
||||
complete: function() {
|
||||
self.responsesRequest = null;
|
||||
},
|
||||
success: function(data) {
|
||||
Content.loadContentInfos(data.annotated_content_info);
|
||||
if (self.isQuestion()) {
|
||||
self.markedAnswers.add(data.content.endorsed_responses);
|
||||
}
|
||||
self.responses.add(
|
||||
self.isQuestion() ? data.content.non_endorsed_responses : data.content.children
|
||||
);
|
||||
self.renderResponseCountAndPagination(
|
||||
self.isQuestion() ?
|
||||
data.content.non_endorsed_resp_total :
|
||||
data.content.resp_total
|
||||
);
|
||||
self.trigger("thread:responses:rendered");
|
||||
self.loadedResponses = true;
|
||||
return self.$el.find('.discussion-article[data-id="' + self.model.id + '"]').focus();
|
||||
},
|
||||
error: function(xhr, textStatus) {
|
||||
if (textStatus === 'abort') {
|
||||
return;
|
||||
}
|
||||
if (xhr.status === 404) {
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("The thread you selected has been deleted. Please select another thread.")
|
||||
);
|
||||
} else if (firstLoad) {
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading responses. Please reload the page.")
|
||||
);
|
||||
} else {
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading more responses. Please try again.")
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.loadInitialResponses = function() {
|
||||
return this.loadResponses(INITIAL_RESPONSE_PAGE_SIZE, this.$el.find(".js-response-list"), true);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderResponseCountAndPagination = function(responseTotal) {
|
||||
var buttonText, loadMoreButton, responseCountFormat, responseLimit, responsePagination,
|
||||
responsesRemaining, showingResponsesText, self = this;
|
||||
if (this.isQuestion() && this.markedAnswers.length !== 0) {
|
||||
responseCountFormat = ngettext(
|
||||
"%(numResponses)s other response", "%(numResponses)s other responses", responseTotal
|
||||
);
|
||||
} else {
|
||||
responseCountFormat = ngettext(
|
||||
"%(numResponses)s response", "%(numResponses)s responses", responseTotal
|
||||
);
|
||||
}
|
||||
this.$el.find(".response-count").html(interpolate(responseCountFormat, {
|
||||
numResponses: responseTotal
|
||||
}, true));
|
||||
responsePagination = this.$el.find(".response-pagination");
|
||||
responsePagination.empty();
|
||||
if (responseTotal > 0) {
|
||||
responsesRemaining = responseTotal - this.responses.size();
|
||||
if (responsesRemaining === 0) {
|
||||
showingResponsesText = gettext("Showing all responses");
|
||||
}
|
||||
else {
|
||||
showingResponsesText = interpolate(
|
||||
ngettext(
|
||||
"Showing first response", "Showing first %(numResponses)s responses",
|
||||
this.responses.size()
|
||||
),
|
||||
{ numResponses: this.responses.size() },
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
responsePagination.append($("<span>")
|
||||
.addClass("response-display-count").html(_.escape(showingResponsesText)));
|
||||
if (responsesRemaining > 0) {
|
||||
if (responsesRemaining < SUBSEQUENT_RESPONSE_PAGE_SIZE) {
|
||||
responseLimit = null;
|
||||
buttonText = gettext("Load all responses");
|
||||
} else {
|
||||
responseLimit = SUBSEQUENT_RESPONSE_PAGE_SIZE;
|
||||
buttonText = interpolate(gettext("Load next %(numResponses)s responses"), {
|
||||
numResponses: responseLimit
|
||||
}, true);
|
||||
}
|
||||
loadMoreButton = $("<button>").addClass("load-response-button").html(_.escape(buttonText));
|
||||
loadMoreButton.click(function() {
|
||||
return self.loadResponses(responseLimit, loadMoreButton);
|
||||
});
|
||||
return responsePagination.append(loadMoreButton);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderResponseToList = function(response, listSelector, options) {
|
||||
var view;
|
||||
response.set('thread', this.model);
|
||||
view = new ThreadResponseView($.extend({
|
||||
model: response
|
||||
}, options));
|
||||
view.on("comment:add", this.addComment);
|
||||
view.on("comment:endorse", this.endorseThread);
|
||||
view.render();
|
||||
this.$el.find(listSelector).append(view.el);
|
||||
return view.afterInsert();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderAddResponseButton = function() {
|
||||
if (this.model.hasResponses() && this.model.can('can_reply') && !this.model.get('closed')) {
|
||||
return this.$el.find('div.add-response').show();
|
||||
} else {
|
||||
return this.$el.find('div.add-response').hide();
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.scrollToAddResponse = function(event) {
|
||||
var form;
|
||||
event.preventDefault();
|
||||
form = $(event.target).parents('article.discussion-article').find('form.discussion-reply-new');
|
||||
$('html, body').scrollTop(form.offset().top);
|
||||
return form.find('.wmd-panel textarea').focus();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.addComment = function() {
|
||||
return this.model.comment();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.endorseThread = function() {
|
||||
return this.model.set('endorsed', this.$el.find(".action-answer.is-checked").length > 0);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.submitComment = function(event) {
|
||||
var body, comment, url;
|
||||
event.preventDefault();
|
||||
url = this.model.urlFor('reply');
|
||||
body = this.getWmdContent("reply-body");
|
||||
if (!body.trim().length) {
|
||||
return;
|
||||
}
|
||||
this.setWmdContent("reply-body", "");
|
||||
comment = new Comment({
|
||||
body: body,
|
||||
created_at: (new Date()).toISOString(),
|
||||
username: window.user.get("username"),
|
||||
votes: {
|
||||
up_count: 0
|
||||
},
|
||||
abuse_flaggers: [],
|
||||
endorsed: false,
|
||||
user_id: window.user.get("id")
|
||||
});
|
||||
comment.set('thread', this.model.get('thread'));
|
||||
this.renderResponseToList(comment, ".js-response-list");
|
||||
this.model.addComment();
|
||||
this.renderAddResponseButton();
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $(event.target),
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: 'json',
|
||||
data: {
|
||||
body: body
|
||||
},
|
||||
success: function(data) {
|
||||
comment.updateInfo(data.annotated_content_info);
|
||||
return comment.set(data.content);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.edit = function() {
|
||||
this.createEditView();
|
||||
return this.renderEditView();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.createEditView = function() {
|
||||
if (this.showView) {
|
||||
this.showView.undelegateEvents();
|
||||
this.showView.$el.empty();
|
||||
this.showView = null;
|
||||
}
|
||||
this.editView = new DiscussionThreadEditView({
|
||||
container: this.$('.thread-content-wrapper'),
|
||||
model: this.model,
|
||||
mode: this.mode,
|
||||
context: this.context,
|
||||
course_settings: this.options.course_settings
|
||||
});
|
||||
this.editView.bind("thread:updated thread:cancel_edit", this.closeEditView);
|
||||
return this.editView.bind("comment:endorse", this.endorseThread);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderSubView = function(view) {
|
||||
view.setElement(this.$('.thread-content-wrapper'));
|
||||
view.render();
|
||||
return view.delegateEvents();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderEditView = function() {
|
||||
return this.editView.render();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.createShowView = function() {
|
||||
this.showView = new DiscussionThreadShowView({
|
||||
model: this.model,
|
||||
mode: this.mode
|
||||
});
|
||||
this.showView.bind("thread:_delete", this._delete);
|
||||
return this.showView.bind("thread:edit", this.edit);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.renderShowView = function() {
|
||||
return this.renderSubView(this.showView);
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype.closeEditView = function() {
|
||||
this.createShowView();
|
||||
this.renderShowView();
|
||||
this.renderAttrs();
|
||||
return this.$el.find(".post-extended-content").show();
|
||||
};
|
||||
|
||||
DiscussionThreadView.prototype._delete = function(event) {
|
||||
var $elem, url;
|
||||
url = this.model.urlFor('_delete');
|
||||
if (!this.model.can('can_delete')) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(gettext("Are you sure you want to delete this post?"))) {
|
||||
return;
|
||||
}
|
||||
this.model.remove();
|
||||
this.showView.undelegateEvents();
|
||||
this.undelegateEvents();
|
||||
this.$el.empty();
|
||||
$elem = $(event.target);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $elem,
|
||||
url: url,
|
||||
type: "POST"
|
||||
});
|
||||
};
|
||||
|
||||
return DiscussionThreadView;
|
||||
|
||||
})(DiscussionContentView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(Backbone) {
|
||||
(function() {
|
||||
'use strict';
|
||||
if (Backbone) {
|
||||
this.DiscussionTopicMenuView = Backbone.View.extend({
|
||||
@@ -42,7 +42,9 @@
|
||||
this.selectedTopic = this.$('.js-selected-topic');
|
||||
this.hideTopicDropdown();
|
||||
if (this.getCurrentTopicId()) {
|
||||
this.setTopic(this.$('a.topic-title').filter('[data-discussion-id="' + this.getCurrentTopicId() + '"]'));
|
||||
this.setTopic(this.$('a.topic-title').filter(
|
||||
'[data-discussion-id="' + this.getCurrentTopicId() + '"]')
|
||||
);
|
||||
} else {
|
||||
this.setTopic(this.$('a.topic-title').first());
|
||||
}
|
||||
@@ -174,7 +176,7 @@
|
||||
// TODO: this helper class duplicates functionality in DiscussionThreadListView.filterTopics
|
||||
// for use with a very similar category dropdown in the New Post form. The two menus' implementations
|
||||
// should be merged into a single reusable view.
|
||||
filterDrop: function (e) {
|
||||
filterDrop: function(e) {
|
||||
var $drop, $items, query;
|
||||
$drop = $(e.target).parents('.topic-menu-wrapper');
|
||||
query = $(e.target).val();
|
||||
@@ -186,14 +188,14 @@
|
||||
}
|
||||
|
||||
$items.addClass('hidden');
|
||||
$items.each(function (_index, item) {
|
||||
$items.each(function(_index, item) {
|
||||
var path, pathText, pathTitles;
|
||||
path = $(item).parents(".topic-menu-item").andSelf();
|
||||
pathTitles = path.children(".topic-title").map(function (_, elem) {
|
||||
pathTitles = path.children(".topic-title").map(function(_, elem) {
|
||||
return $(elem).text();
|
||||
}).get();
|
||||
pathText = pathTitles.join(" / ").toLowerCase();
|
||||
if (query.split(" ").every(function (term) {
|
||||
if (query.split(" ").every(function(term) {
|
||||
return pathText.search(term.toLowerCase()) !== -1;
|
||||
})) {
|
||||
$(item).removeClass('hidden');
|
||||
@@ -204,4 +206,4 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
}).call(this, Backbone);
|
||||
}).call(this);
|
||||
@@ -0,0 +1,107 @@
|
||||
/* globals Discussion, DiscussionThreadProfileView, DiscussionUtil, URI */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.DiscussionUserProfileView = (function(_super) {
|
||||
|
||||
__extends(DiscussionUserProfileView, _super);
|
||||
|
||||
function DiscussionUserProfileView() {
|
||||
var self = this;
|
||||
this.render = function() {
|
||||
return DiscussionUserProfileView.prototype.render.apply(self, arguments);
|
||||
};
|
||||
return DiscussionUserProfileView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
DiscussionUserProfileView.prototype.events = {
|
||||
"click .discussion-paginator a": "changePage"
|
||||
};
|
||||
|
||||
DiscussionUserProfileView.prototype.initialize = function(options) {
|
||||
DiscussionUserProfileView.__super__.initialize.call(this);
|
||||
this.page = options.page;
|
||||
this.numPages = options.numPages;
|
||||
this.discussion = new Discussion();
|
||||
this.discussion.on("reset", this.render);
|
||||
return this.discussion.reset(this.collection, {
|
||||
silent: false
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionUserProfileView.prototype.render = function() {
|
||||
var baseUri, pageUrlFunc, paginationParams,
|
||||
self = this;
|
||||
this.$el.html(_.template($("#user-profile-template").html())({
|
||||
threads: this.discussion.models
|
||||
}));
|
||||
this.discussion.map(function(thread) {
|
||||
return new DiscussionThreadProfileView({
|
||||
el: self.$("article#thread_" + thread.id),
|
||||
model: thread
|
||||
}).render();
|
||||
});
|
||||
baseUri = URI(window.location).removeSearch("page");
|
||||
pageUrlFunc = function(page) {
|
||||
return baseUri.clone().addSearch("page", page);
|
||||
};
|
||||
paginationParams = DiscussionUtil.getPaginationParams(this.page, this.numPages, pageUrlFunc);
|
||||
this.$el.find(".discussion-pagination")
|
||||
.html(_.template($("#pagination-template").html())(paginationParams));
|
||||
};
|
||||
|
||||
DiscussionUserProfileView.prototype.changePage = function(event) {
|
||||
var url,
|
||||
self = this;
|
||||
event.preventDefault();
|
||||
url = $(event.target).attr("href");
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: this.$el,
|
||||
$loading: $(event.target),
|
||||
takeFocus: true,
|
||||
url: url,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
self.page = response.page;
|
||||
self.numPages = response.num_pages;
|
||||
self.discussion.reset(response.discussion_data, {
|
||||
silent: false
|
||||
});
|
||||
history.pushState({}, "", url);
|
||||
return $("html, body").animate({
|
||||
scrollTop: 0
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
return DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the page you requested. Please try again.")
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return DiscussionUserProfileView;
|
||||
|
||||
})(Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
195
common/static/common/js/discussion/views/new_post_view.js
Normal file
195
common/static/common/js/discussion/views/new_post_view.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/* globals DiscussionTopicMenuView, DiscussionUtil, Thread */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.NewPostView = (function(_super) {
|
||||
|
||||
__extends(NewPostView, _super);
|
||||
|
||||
function NewPostView() {
|
||||
var self = this;
|
||||
this.updateStyles = function() {
|
||||
return NewPostView.prototype.updateStyles.apply(self, arguments);
|
||||
};
|
||||
this.resetForm = function() {
|
||||
return NewPostView.prototype.resetForm.apply(self, arguments);
|
||||
};
|
||||
return NewPostView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
NewPostView.prototype.initialize = function(options) {
|
||||
var _ref;
|
||||
this.mode = options.mode || "inline";
|
||||
if ((_ref = this.mode) !== "tab" && _ref !== "inline") {
|
||||
throw new Error("invalid mode: " + this.mode);
|
||||
}
|
||||
this.course_settings = options.course_settings;
|
||||
this.is_commentable_cohorted = options.is_commentable_cohorted;
|
||||
this.topicId = options.topicId;
|
||||
};
|
||||
|
||||
NewPostView.prototype.render = function() {
|
||||
var context, threadTypeTemplate;
|
||||
context = _.clone(this.course_settings.attributes);
|
||||
_.extend(context, {
|
||||
cohort_options: this.getCohortOptions(),
|
||||
is_commentable_cohorted: this.is_commentable_cohorted,
|
||||
mode: this.mode,
|
||||
form_id: this.mode + (this.topicId ? "-" + this.topicId : "")
|
||||
});
|
||||
this.$el.html(_.template($("#new-post-template").html())(context));
|
||||
threadTypeTemplate = _.template($("#thread-type-template").html());
|
||||
if ($('.js-group-select').is(':disabled')) {
|
||||
$('.group-selector-wrapper').addClass('disabled');
|
||||
}
|
||||
this.addField(threadTypeTemplate({
|
||||
form_id: _.uniqueId("form-")
|
||||
}));
|
||||
if (this.isTabMode()) {
|
||||
this.topicView = new DiscussionTopicMenuView({
|
||||
topicId: this.topicId,
|
||||
course_settings: this.course_settings
|
||||
});
|
||||
this.topicView.on('thread:topic_change', this.toggleGroupDropdown);
|
||||
this.addField(this.topicView.render());
|
||||
}
|
||||
return DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), "js-post-body");
|
||||
};
|
||||
|
||||
NewPostView.prototype.addField = function(fieldView) {
|
||||
return this.$('.forum-new-post-form-wrapper').append(fieldView);
|
||||
};
|
||||
|
||||
NewPostView.prototype.isTabMode = function() {
|
||||
return this.mode === "tab";
|
||||
};
|
||||
|
||||
NewPostView.prototype.getCohortOptions = function() {
|
||||
var user_cohort_id;
|
||||
if (this.course_settings.get("is_cohorted") && DiscussionUtil.isPrivilegedUser()) {
|
||||
user_cohort_id = $("#discussion-container").data("user-cohort-id");
|
||||
return _.map(this.course_settings.get("cohorts"), function(cohort) {
|
||||
return {
|
||||
value: cohort.id,
|
||||
text: cohort.name,
|
||||
selected: cohort.id === user_cohort_id
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
NewPostView.prototype.events = {
|
||||
"submit .forum-new-post-form": "createPost",
|
||||
"change .post-option-input": "postOptionChange",
|
||||
"click .cancel": "cancel",
|
||||
"reset .forum-new-post-form": "updateStyles"
|
||||
};
|
||||
|
||||
NewPostView.prototype.toggleGroupDropdown = function($target) {
|
||||
if ($target.data('cohorted')) {
|
||||
$('.js-group-select').prop('disabled', false);
|
||||
return $('.group-selector-wrapper').removeClass('disabled');
|
||||
} else {
|
||||
$('.js-group-select').val('').prop('disabled', true);
|
||||
return $('.group-selector-wrapper').addClass('disabled');
|
||||
}
|
||||
};
|
||||
|
||||
NewPostView.prototype.postOptionChange = function(event) {
|
||||
var $optionElem, $target;
|
||||
$target = $(event.target);
|
||||
$optionElem = $target.closest(".post-option");
|
||||
if ($target.is(":checked")) {
|
||||
return $optionElem.addClass("is-enabled");
|
||||
} else {
|
||||
return $optionElem.removeClass("is-enabled");
|
||||
}
|
||||
};
|
||||
|
||||
NewPostView.prototype.createPost = function(event) {
|
||||
var anonymous, anonymous_to_peers, body, follow, group, thread_type, title, topicId, url,
|
||||
self = this;
|
||||
event.preventDefault();
|
||||
thread_type = this.$(".post-type-input:checked").val();
|
||||
title = this.$(".js-post-title").val();
|
||||
body = this.$(".js-post-body").find(".wmd-input").val();
|
||||
group = this.$(".js-group-select option:selected").attr("value");
|
||||
anonymous = false || this.$(".js-anon").is(":checked");
|
||||
anonymous_to_peers = false || this.$(".js-anon-peers").is(":checked");
|
||||
follow = false || this.$(".js-follow").is(":checked");
|
||||
topicId = this.isTabMode() ? this.topicView.getCurrentTopicId() : this.topicId;
|
||||
url = DiscussionUtil.urlFor('create_thread', topicId);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $(event.target),
|
||||
$loading: event ? $(event.target) : void 0,
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: 'json',
|
||||
data: {
|
||||
thread_type: thread_type,
|
||||
title: title,
|
||||
body: body,
|
||||
anonymous: anonymous,
|
||||
anonymous_to_peers: anonymous_to_peers,
|
||||
auto_subscribe: follow,
|
||||
group_id: group
|
||||
},
|
||||
error: DiscussionUtil.formErrorHandler(this.$(".post-errors")),
|
||||
success: function(response) {
|
||||
var thread;
|
||||
thread = new Thread(response.content);
|
||||
self.$el.hide();
|
||||
self.resetForm();
|
||||
return self.collection.add(thread);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
NewPostView.prototype.cancel = function(event) {
|
||||
event.preventDefault();
|
||||
if (!confirm(gettext("Your post will be discarded."))) {
|
||||
return;
|
||||
}
|
||||
this.trigger('newPost:cancel');
|
||||
return this.resetForm();
|
||||
};
|
||||
|
||||
NewPostView.prototype.resetForm = function() {
|
||||
this.$(".forum-new-post-form")[0].reset();
|
||||
DiscussionUtil.clearFormErrors(this.$(".post-errors"));
|
||||
this.$(".wmd-preview p").html("");
|
||||
if (this.isTabMode()) {
|
||||
return this.topicView.setTopic(this.$("a.topic-title").first());
|
||||
}
|
||||
};
|
||||
|
||||
NewPostView.prototype.updateStyles = function() {
|
||||
var self = this;
|
||||
return setTimeout(function() {return self.$(".post-option-input").trigger("change");}, 1);
|
||||
};
|
||||
|
||||
return NewPostView;
|
||||
|
||||
})(Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,64 @@
|
||||
/* globals DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.ResponseCommentEditView = (function(_super) {
|
||||
|
||||
__extends(ResponseCommentEditView, _super);
|
||||
|
||||
function ResponseCommentEditView() {
|
||||
return ResponseCommentEditView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
ResponseCommentEditView.prototype.events = {
|
||||
"click .post-update": "update",
|
||||
"click .post-cancel": "cancel_edit"
|
||||
};
|
||||
|
||||
ResponseCommentEditView.prototype.$ = function(selector) {
|
||||
return this.$el.find(selector);
|
||||
};
|
||||
|
||||
ResponseCommentEditView.prototype.initialize = function() {
|
||||
return ResponseCommentEditView.__super__.initialize.call(this);
|
||||
};
|
||||
|
||||
ResponseCommentEditView.prototype.render = function() {
|
||||
this.template = _.template($("#response-comment-edit-template").html());
|
||||
this.$el.html(this.template(this.model.toJSON()));
|
||||
this.delegateEvents();
|
||||
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), "edit-comment-body");
|
||||
return this;
|
||||
};
|
||||
|
||||
ResponseCommentEditView.prototype.update = function(event) {
|
||||
return this.trigger("comment:update", event);
|
||||
};
|
||||
|
||||
ResponseCommentEditView.prototype.cancel_edit = function(event) {
|
||||
return this.trigger("comment:cancel_edit", event);
|
||||
};
|
||||
|
||||
return ResponseCommentEditView;
|
||||
|
||||
})(Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,86 @@
|
||||
/* globals DiscussionContentShowView, DiscussionUtil, MathJax */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.ResponseCommentShowView = (function(_super) {
|
||||
|
||||
__extends(ResponseCommentShowView, _super);
|
||||
|
||||
function ResponseCommentShowView() {
|
||||
var self = this;
|
||||
this.edit = function() {
|
||||
return ResponseCommentShowView.prototype.edit.apply(self, arguments);
|
||||
};
|
||||
this._delete = function() {
|
||||
return ResponseCommentShowView.prototype._delete.apply(self, arguments);
|
||||
};
|
||||
return ResponseCommentShowView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
ResponseCommentShowView.prototype.tagName = "li";
|
||||
|
||||
ResponseCommentShowView.prototype.render = function() {
|
||||
this.template = _.template($("#response-comment-show-template").html());
|
||||
this.$el.html(this.template(_.extend({
|
||||
cid: this.model.cid,
|
||||
author_display: this.getAuthorDisplay(),
|
||||
readOnly: $('.discussion-module').data('read-only')
|
||||
}, this.model.attributes)));
|
||||
this.delegateEvents();
|
||||
this.renderAttrs();
|
||||
this.$el.find(".timeago").timeago();
|
||||
this.convertMath();
|
||||
this.addReplyLink();
|
||||
return this;
|
||||
};
|
||||
|
||||
ResponseCommentShowView.prototype.addReplyLink = function() {
|
||||
var html, name, p, _ref;
|
||||
if (this.model.hasOwnProperty('parent')) {
|
||||
name = (_ref = this.model.parent.get('username')) !== null ? _ref : gettext("anonymous");
|
||||
html = "<a href='#comment_" + this.model.parent.id + "'>@" + name + "</a>: ";
|
||||
p = this.$('.response-body p:first');
|
||||
return p.prepend(html);
|
||||
}
|
||||
};
|
||||
|
||||
ResponseCommentShowView.prototype.convertMath = function() {
|
||||
var body;
|
||||
body = this.$el.find(".response-body");
|
||||
body.html(DiscussionUtil.postMathJaxProcessor(DiscussionUtil.markdownWithHighlight(body.text())));
|
||||
if (typeof MathJax !== "undefined" && MathJax !== null) {
|
||||
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, body[0]]);
|
||||
}
|
||||
};
|
||||
|
||||
ResponseCommentShowView.prototype._delete = function(event) {
|
||||
return this.trigger("comment:_delete", event);
|
||||
};
|
||||
|
||||
ResponseCommentShowView.prototype.edit = function(event) {
|
||||
return this.trigger("comment:edit", event);
|
||||
};
|
||||
|
||||
return ResponseCommentShowView;
|
||||
|
||||
})(DiscussionContentShowView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,162 @@
|
||||
/* globals DiscussionContentView, DiscussionUtil, ResponseCommentEditView, ResponseCommentShowView */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.ResponseCommentView = (function(_super) {
|
||||
|
||||
__extends(ResponseCommentView, _super);
|
||||
|
||||
function ResponseCommentView() {
|
||||
var self = this;
|
||||
this.update = function() {
|
||||
return ResponseCommentView.prototype.update.apply(self, arguments);
|
||||
};
|
||||
this.edit = function() {
|
||||
return ResponseCommentView.prototype.edit.apply(self, arguments);
|
||||
};
|
||||
this.cancelEdit = function() {
|
||||
return ResponseCommentView.prototype.cancelEdit.apply(self, arguments);
|
||||
};
|
||||
this._delete = function() {
|
||||
return ResponseCommentView.prototype._delete.apply(self, arguments);
|
||||
};
|
||||
return ResponseCommentView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
ResponseCommentView.prototype.tagName = "li";
|
||||
|
||||
ResponseCommentView.prototype.$ = function(selector) {
|
||||
return this.$el.find(selector);
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.initialize = function() {
|
||||
return ResponseCommentView.__super__.initialize.call(this);
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.render = function() {
|
||||
this.renderShowView();
|
||||
return this;
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.renderSubView = function(view) {
|
||||
view.setElement(this.$el);
|
||||
view.render();
|
||||
return view.delegateEvents();
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.renderShowView = function() {
|
||||
if (!this.showView) {
|
||||
if (this.editView) {
|
||||
this.editView.undelegateEvents();
|
||||
this.editView.$el.empty();
|
||||
this.editView = null;
|
||||
}
|
||||
this.showView = new ResponseCommentShowView({
|
||||
model: this.model
|
||||
});
|
||||
this.showView.bind("comment:_delete", this._delete);
|
||||
this.showView.bind("comment:edit", this.edit);
|
||||
return this.renderSubView(this.showView);
|
||||
}
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.renderEditView = function() {
|
||||
if (!this.editView) {
|
||||
if (this.showView) {
|
||||
this.showView.undelegateEvents();
|
||||
this.showView.$el.empty();
|
||||
this.showView = null;
|
||||
}
|
||||
this.editView = new ResponseCommentEditView({
|
||||
model: this.model
|
||||
});
|
||||
this.editView.bind("comment:update", this.update);
|
||||
this.editView.bind("comment:cancel_edit", this.cancelEdit);
|
||||
return this.renderSubView(this.editView);
|
||||
}
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype._delete = function(event) {
|
||||
var $elem, url,
|
||||
self = this;
|
||||
event.preventDefault();
|
||||
if (!this.model.can('can_delete')) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(gettext("Are you sure you want to delete this comment?"))) {
|
||||
return;
|
||||
}
|
||||
url = this.model.urlFor('_delete');
|
||||
$elem = $(event.target);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $elem,
|
||||
url: url,
|
||||
type: "POST",
|
||||
success: function() {
|
||||
self.model.remove();
|
||||
return self.$el.remove();
|
||||
},
|
||||
error: function() {
|
||||
return DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble deleting this comment. Please try again.")
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.cancelEdit = function(event) {
|
||||
this.trigger("comment:cancel_edit", event);
|
||||
return this.renderShowView();
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.edit = function(event) {
|
||||
this.trigger("comment:edit", event);
|
||||
return this.renderEditView();
|
||||
};
|
||||
|
||||
ResponseCommentView.prototype.update = function(event) {
|
||||
var newBody, url,
|
||||
self = this;
|
||||
newBody = this.editView.$(".edit-comment-body textarea").val();
|
||||
url = DiscussionUtil.urlFor("update_comment", this.model.id);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $(event.target),
|
||||
$loading: $(event.target),
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
body: newBody
|
||||
},
|
||||
error: DiscussionUtil.formErrorHandler(this.$(".edit-comment-form-errors")),
|
||||
success: function() {
|
||||
self.model.set("body", newBody);
|
||||
return self.cancelEdit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return ResponseCommentView;
|
||||
|
||||
})(DiscussionContentView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,64 @@
|
||||
/* globals DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.ThreadResponseEditView = (function(_super) {
|
||||
|
||||
__extends(ThreadResponseEditView, _super);
|
||||
|
||||
function ThreadResponseEditView() {
|
||||
return ThreadResponseEditView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
ThreadResponseEditView.prototype.events = {
|
||||
"click .post-update": "update",
|
||||
"click .post-cancel": "cancel_edit"
|
||||
};
|
||||
|
||||
ThreadResponseEditView.prototype.$ = function(selector) {
|
||||
return this.$el.find(selector);
|
||||
};
|
||||
|
||||
ThreadResponseEditView.prototype.initialize = function() {
|
||||
return ThreadResponseEditView.__super__.initialize.call(this);
|
||||
};
|
||||
|
||||
ThreadResponseEditView.prototype.render = function() {
|
||||
this.template = _.template($("#thread-response-edit-template").html());
|
||||
this.$el.html(this.template(this.model.toJSON()));
|
||||
this.delegateEvents();
|
||||
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), "edit-post-body");
|
||||
return this;
|
||||
};
|
||||
|
||||
ThreadResponseEditView.prototype.update = function(event) {
|
||||
return this.trigger("response:update", event);
|
||||
};
|
||||
|
||||
ThreadResponseEditView.prototype.cancel_edit = function(event) {
|
||||
return this.trigger("response:cancel_edit", event);
|
||||
};
|
||||
|
||||
return ThreadResponseEditView;
|
||||
|
||||
})(Backbone.View);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -0,0 +1,78 @@
|
||||
/* globals DiscussionContentShowView, DiscussionUtil, MathJax */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.ThreadResponseShowView = (function(_super) {
|
||||
|
||||
__extends(ThreadResponseShowView, _super);
|
||||
|
||||
function ThreadResponseShowView() {
|
||||
return ThreadResponseShowView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
ThreadResponseShowView.prototype.initialize = function() {
|
||||
ThreadResponseShowView.__super__.initialize.call(this);
|
||||
return this.listenTo(this.model, "change", this.render);
|
||||
};
|
||||
|
||||
ThreadResponseShowView.prototype.renderTemplate = function() {
|
||||
var context;
|
||||
this.template = _.template($("#thread-response-show-template").html());
|
||||
context = _.extend({
|
||||
cid: this.model.cid,
|
||||
author_display: this.getAuthorDisplay(),
|
||||
endorser_display: this.getEndorserDisplay(),
|
||||
readOnly: $('.discussion-module').data('read-only')
|
||||
}, this.model.attributes);
|
||||
return this.template(context);
|
||||
};
|
||||
|
||||
ThreadResponseShowView.prototype.render = function() {
|
||||
this.$el.html(this.renderTemplate());
|
||||
this.delegateEvents();
|
||||
this.renderAttrs();
|
||||
this.$el.find(".posted-details .timeago").timeago();
|
||||
this.convertMath();
|
||||
return this;
|
||||
};
|
||||
|
||||
ThreadResponseShowView.prototype.convertMath = function() {
|
||||
var element;
|
||||
element = this.$(".response-body");
|
||||
element.html(DiscussionUtil.postMathJaxProcessor(DiscussionUtil.markdownWithHighlight(element.text())));
|
||||
if (typeof MathJax !== "undefined" && MathJax !== null) {
|
||||
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element[0]]);
|
||||
}
|
||||
};
|
||||
|
||||
ThreadResponseShowView.prototype.edit = function(event) {
|
||||
return this.trigger("response:edit", event);
|
||||
};
|
||||
|
||||
ThreadResponseShowView.prototype._delete = function(event) {
|
||||
return this.trigger("response:_delete", event);
|
||||
};
|
||||
|
||||
return ThreadResponseShowView;
|
||||
|
||||
})(DiscussionContentShowView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
347
common/static/common/js/discussion/views/thread_response_view.js
Normal file
347
common/static/common/js/discussion/views/thread_response_view.js
Normal file
@@ -0,0 +1,347 @@
|
||||
/* globals
|
||||
Comments, ResponseCommentView, DiscussionUtil, ThreadResponseEditView,
|
||||
ThreadResponseShowView, DiscussionContentView
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
var __hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) {
|
||||
child[key] = parent[key];
|
||||
}
|
||||
}
|
||||
function ctor() {
|
||||
this.constructor = child;
|
||||
}
|
||||
|
||||
ctor.prototype = parent.prototype;
|
||||
child.prototype = new ctor();
|
||||
child.__super__ = parent.prototype;
|
||||
return child;
|
||||
};
|
||||
|
||||
if (typeof Backbone !== "undefined" && Backbone !== null) {
|
||||
this.ThreadResponseView = (function(_super) {
|
||||
|
||||
__extends(ThreadResponseView, _super);
|
||||
|
||||
function ThreadResponseView() {
|
||||
var self = this;
|
||||
this.update = function() {
|
||||
return ThreadResponseView.prototype.update.apply(self, arguments);
|
||||
};
|
||||
this.edit = function() {
|
||||
return ThreadResponseView.prototype.edit.apply(self, arguments);
|
||||
};
|
||||
this.cancelEdit = function() {
|
||||
return ThreadResponseView.prototype.cancelEdit.apply(self, arguments);
|
||||
};
|
||||
this._delete = function() {
|
||||
return ThreadResponseView.prototype._delete.apply(self, arguments);
|
||||
};
|
||||
this.renderComment = function() {
|
||||
return ThreadResponseView.prototype.renderComment.apply(self, arguments);
|
||||
};
|
||||
return ThreadResponseView.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
ThreadResponseView.prototype.tagName = "li";
|
||||
|
||||
ThreadResponseView.prototype.className = "forum-response";
|
||||
|
||||
ThreadResponseView.prototype.events = {
|
||||
"click .discussion-submit-comment": "submitComment",
|
||||
"focus .wmd-input": "showEditorChrome"
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.$ = function(selector) {
|
||||
return this.$el.find(selector);
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.initialize = function(options) {
|
||||
this.collapseComments = options.collapseComments;
|
||||
this.createShowView();
|
||||
this.readOnly = $('.discussion-module').data('read-only');
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.renderTemplate = function() {
|
||||
var container, templateData, _ref;
|
||||
this.template = _.template($("#thread-response-template").html());
|
||||
container = $("#discussion-container");
|
||||
if (!container.length) {
|
||||
container = $(".discussion-module");
|
||||
}
|
||||
templateData = _.extend(this.model.toJSON(), {
|
||||
wmdId: (_ref = this.model.id) !== null ? _ref : (new Date()).getTime(),
|
||||
create_sub_comment: container.data("user-create-subcomment"),
|
||||
readOnly: this.readOnly
|
||||
});
|
||||
return this.template(templateData);
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.render = function() {
|
||||
this.$el.addClass("response_" + this.model.get("id"));
|
||||
this.$el.html(this.renderTemplate());
|
||||
this.delegateEvents();
|
||||
this.renderShowView();
|
||||
this.renderAttrs();
|
||||
if (this.model.get("thread").get("closed")) {
|
||||
this.hideCommentForm();
|
||||
}
|
||||
this.renderComments();
|
||||
return this;
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.afterInsert = function() {
|
||||
this.makeWmdEditor("comment-body");
|
||||
return this.hideEditorChrome();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.hideEditorChrome = function() {
|
||||
this.$('.wmd-button-row').hide();
|
||||
this.$('.wmd-preview-container').hide();
|
||||
this.$('.wmd-input').css({
|
||||
height: '35px',
|
||||
padding: '5px'
|
||||
});
|
||||
return this.$('.comment-post-control').hide();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.showEditorChrome = function() {
|
||||
this.$('.wmd-button-row').show();
|
||||
this.$('.wmd-preview-container').show();
|
||||
this.$('.comment-post-control').show();
|
||||
return this.$('.wmd-input').css({
|
||||
height: '125px',
|
||||
padding: '10px'
|
||||
});
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.renderComments = function() {
|
||||
var collectComments, comments,
|
||||
self = this;
|
||||
comments = new Comments();
|
||||
this.commentViews = [];
|
||||
comments.comparator = function(comment) {
|
||||
return comment.get('created_at');
|
||||
};
|
||||
collectComments = function(comment) {
|
||||
var children;
|
||||
comments.add(comment);
|
||||
children = new Comments(comment.get('children'));
|
||||
return children.each(function(child) {
|
||||
child.parent = comment;
|
||||
return collectComments(child);
|
||||
});
|
||||
};
|
||||
this.model.get('comments').each(collectComments);
|
||||
comments.each(function(comment) {
|
||||
return self.renderComment(comment, false, null);
|
||||
});
|
||||
if (this.collapseComments && comments.length) {
|
||||
this.$(".comments").hide();
|
||||
return this.$(".action-show-comments").on("click", function(event) {
|
||||
event.preventDefault();
|
||||
self.$(".action-show-comments").hide();
|
||||
return self.$(".comments").show();
|
||||
});
|
||||
} else {
|
||||
return this.$(".action-show-comments").hide();
|
||||
}
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.renderComment = function(comment) {
|
||||
var view,
|
||||
self = this;
|
||||
comment.set('thread', this.model.get('thread'));
|
||||
view = new ResponseCommentView({
|
||||
model: comment
|
||||
});
|
||||
view.render();
|
||||
if (this.readOnly) {
|
||||
this.$el.find('.comments').append(view.el);
|
||||
} else {
|
||||
this.$el.find(".comments .new-comment").before(view.el);
|
||||
}
|
||||
view.bind("comment:edit", function(event) {
|
||||
if (self.editView) {
|
||||
self.cancelEdit(event);
|
||||
}
|
||||
self.cancelCommentEdits();
|
||||
return self.hideCommentForm();
|
||||
});
|
||||
view.bind("comment:cancel_edit", function() {
|
||||
return self.showCommentForm();
|
||||
});
|
||||
this.commentViews.push(view);
|
||||
return view;
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.submitComment = function(event) {
|
||||
var body, comment, url, view;
|
||||
event.preventDefault();
|
||||
url = this.model.urlFor('reply');
|
||||
body = this.getWmdContent("comment-body");
|
||||
if (!body.trim().length) {
|
||||
return;
|
||||
}
|
||||
this.setWmdContent("comment-body", "");
|
||||
comment = new Comment({
|
||||
body: body,
|
||||
created_at: (new Date()).toISOString(),
|
||||
username: window.user.get("username"),
|
||||
abuse_flaggers: [],
|
||||
user_id: window.user.get("id"),
|
||||
id: "unsaved"
|
||||
});
|
||||
view = this.renderComment(comment);
|
||||
this.hideEditorChrome();
|
||||
this.trigger("comment:add", comment);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $(event.target),
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: 'json',
|
||||
data: {
|
||||
body: body
|
||||
},
|
||||
success: function(response) {
|
||||
comment.set(response.content);
|
||||
comment.updateInfo(response.annotated_content_info);
|
||||
return view.render();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype._delete = function(event) {
|
||||
var $elem, url;
|
||||
event.preventDefault();
|
||||
if (!this.model.can('can_delete')) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(gettext("Are you sure you want to delete this response?"))) {
|
||||
return;
|
||||
}
|
||||
url = this.model.urlFor('_delete');
|
||||
this.model.remove();
|
||||
this.$el.remove();
|
||||
$elem = $(event.target);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $elem,
|
||||
url: url,
|
||||
type: "POST"
|
||||
});
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.createEditView = function() {
|
||||
if (this.showView) {
|
||||
this.showView.$el.empty();
|
||||
}
|
||||
if (this.editView) {
|
||||
this.editView.model = this.model;
|
||||
} else {
|
||||
this.editView = new ThreadResponseEditView({
|
||||
model: this.model
|
||||
});
|
||||
this.editView.bind("response:update", this.update);
|
||||
return this.editView.bind("response:cancel_edit", this.cancelEdit);
|
||||
}
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.renderSubView = function(view) {
|
||||
view.setElement(this.$('.discussion-response'));
|
||||
view.render();
|
||||
return view.delegateEvents();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.renderEditView = function() {
|
||||
return this.renderSubView(this.editView);
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.cancelCommentEdits = function() {
|
||||
return _.each(this.commentViews, function(view) {
|
||||
return view.cancelEdit();
|
||||
});
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.hideCommentForm = function() {
|
||||
return this.$('.comment-form').closest('li').hide();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.showCommentForm = function() {
|
||||
return this.$('.comment-form').closest('li').show();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.createShowView = function() {
|
||||
var self = this;
|
||||
|
||||
if (this.editView) {
|
||||
this.editView.$el.empty();
|
||||
}
|
||||
if (this.showView) {
|
||||
this.showView.model = this.model;
|
||||
} else {
|
||||
this.showView = new ThreadResponseShowView({
|
||||
model: this.model
|
||||
});
|
||||
this.showView.bind("response:_delete", this._delete);
|
||||
this.showView.bind("response:edit", this.edit);
|
||||
return this.showView.on("comment:endorse", function() {
|
||||
return self.trigger("comment:endorse");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.renderShowView = function() {
|
||||
return this.renderSubView(this.showView);
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.cancelEdit = function(event) {
|
||||
event.preventDefault();
|
||||
this.createShowView();
|
||||
this.renderShowView();
|
||||
return this.showCommentForm();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.edit = function() {
|
||||
this.createEditView();
|
||||
this.renderEditView();
|
||||
this.cancelCommentEdits();
|
||||
return this.hideCommentForm();
|
||||
};
|
||||
|
||||
ThreadResponseView.prototype.update = function(event) {
|
||||
var newBody, url,
|
||||
self = this;
|
||||
newBody = this.editView.$(".edit-post-body textarea").val();
|
||||
url = DiscussionUtil.urlFor('update_comment', this.model.id);
|
||||
return DiscussionUtil.safeAjax({
|
||||
$elem: $(event.target),
|
||||
$loading: event ? $(event.target) : void 0,
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: 'json',
|
||||
data: {
|
||||
body: newBody
|
||||
},
|
||||
error: DiscussionUtil.formErrorHandler(this.$(".edit-post-form-errors")),
|
||||
success: function() {
|
||||
self.editView.$(".edit-post-body textarea").val("").attr("prev-text", "");
|
||||
self.editView.$(".wmd-preview p").html("");
|
||||
self.model.set({
|
||||
body: newBody
|
||||
});
|
||||
self.createShowView();
|
||||
self.renderShowView();
|
||||
return self.showCommentForm();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return ThreadResponseView;
|
||||
|
||||
})(DiscussionContentView);
|
||||
}
|
||||
|
||||
}).call(window);
|
||||
@@ -49,6 +49,7 @@ var commonFiles = {
|
||||
{pattern: 'edx-pattern-library/js/**/*.js'},
|
||||
{pattern: 'edx-ui-toolkit/js/**/*.js'},
|
||||
{pattern: 'xmodule_js/common_static/coffee/src/**/!(*spec).js'},
|
||||
{pattern: 'xmodule_js/common_static/common/js/**/!(*spec).js'},
|
||||
{pattern: 'xmodule_js/common_static/js/**/!(*spec).js'},
|
||||
{pattern: 'xmodule_js/src/**/*.js'}
|
||||
],
|
||||
|
||||
158
common/static/common/js/spec/discussion/content_spec.js
Normal file
158
common/static/common/js/spec/discussion/content_spec.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* globals Comments, Content, DiscussionSpecHelper, DiscussionUser, DiscussionUtil, Thread */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe('All Content', function() {
|
||||
beforeEach(function() {
|
||||
return DiscussionSpecHelper.setUpGlobals();
|
||||
});
|
||||
describe('Staff and TA Content', function() {
|
||||
beforeEach(function() {
|
||||
return DiscussionUtil.loadRoles({
|
||||
"Moderator": [567],
|
||||
"Administrator": [567],
|
||||
"Community TA": [567]
|
||||
});
|
||||
});
|
||||
it('anonymous thread should not include login role label', function() {
|
||||
var anon_content;
|
||||
anon_content = new Content();
|
||||
anon_content.initialize();
|
||||
expect(anon_content.get('staff_authored')).toBe(false);
|
||||
return expect(anon_content.get('community_ta_authored')).toBe(false);
|
||||
});
|
||||
return it('general thread should include login role label', function() {
|
||||
var anon_content;
|
||||
anon_content = new Content({
|
||||
user_id: '567'
|
||||
});
|
||||
anon_content.initialize();
|
||||
expect(anon_content.get('staff_authored')).toBe(true);
|
||||
return expect(anon_content.get('community_ta_authored')).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('Content', function() {
|
||||
beforeEach(function() {
|
||||
this.content = new Content({
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
body: 'this is some content',
|
||||
abuse_flaggers: ['123']
|
||||
});
|
||||
});
|
||||
it('should exist', function() {
|
||||
return expect(Content).toBeDefined();
|
||||
});
|
||||
it('is initialized correctly', function() {
|
||||
this.content.initialize();
|
||||
expect(Content.contents['01234567']).toEqual(this.content);
|
||||
expect(this.content.get('id')).toEqual('01234567');
|
||||
expect(this.content.get('user_url')).toEqual('/courses/edX/999/test/discussion/forum/users/567');
|
||||
expect(this.content.get('children')).toEqual([]);
|
||||
return expect(this.content.get('comments')).toEqual(jasmine.any(Comments));
|
||||
});
|
||||
it('can update info', function() {
|
||||
this.content.updateInfo({
|
||||
ability: {
|
||||
'can_edit': true
|
||||
},
|
||||
voted: true,
|
||||
subscribed: true
|
||||
});
|
||||
expect(this.content.get('ability')).toEqual({
|
||||
'can_edit': true
|
||||
});
|
||||
expect(this.content.get('voted')).toEqual(true);
|
||||
return expect(this.content.get('subscribed')).toEqual(true);
|
||||
});
|
||||
it('can be flagged for abuse', function() {
|
||||
this.content.flagAbuse();
|
||||
return expect(this.content.get('abuse_flaggers')).toEqual(['123', '567']);
|
||||
});
|
||||
return it('can be unflagged for abuse', function() {
|
||||
var temp_array;
|
||||
temp_array = [];
|
||||
temp_array.push(window.user.get('id'));
|
||||
this.content.set("abuse_flaggers", temp_array);
|
||||
this.content.unflagAbuse();
|
||||
return expect(this.content.get('abuse_flaggers')).toEqual([]);
|
||||
});
|
||||
});
|
||||
return describe('Comments', function() {
|
||||
beforeEach(function() {
|
||||
this.comment1 = new Comment({
|
||||
id: '123'
|
||||
});
|
||||
this.comment2 = new Comment({
|
||||
id: '345'
|
||||
});
|
||||
});
|
||||
it('can contain multiple comments', function() {
|
||||
var myComments;
|
||||
myComments = new Comments();
|
||||
expect(myComments.length).toEqual(0);
|
||||
myComments.add(this.comment1);
|
||||
expect(myComments.length).toEqual(1);
|
||||
myComments.add(this.comment2);
|
||||
return expect(myComments.length).toEqual(2);
|
||||
});
|
||||
it('returns results to the find method', function() {
|
||||
var myComments;
|
||||
myComments = new Comments();
|
||||
myComments.add(this.comment1);
|
||||
return expect(myComments.find('123')).toBe(this.comment1);
|
||||
});
|
||||
return it('can be endorsed', function() {
|
||||
DiscussionUtil.loadRoles({
|
||||
"Moderator": [111],
|
||||
"Administrator": [222],
|
||||
"Community TA": [333]
|
||||
});
|
||||
this.discussionThread = new Thread({
|
||||
id: 1,
|
||||
thread_type: "discussion",
|
||||
user_id: 99
|
||||
});
|
||||
this.discussionResponse = new Comment({
|
||||
id: 1,
|
||||
thread: this.discussionThread
|
||||
});
|
||||
this.questionThread = new Thread({
|
||||
id: 1,
|
||||
thread_type: "question",
|
||||
user_id: 99
|
||||
});
|
||||
this.questionResponse = new Comment({
|
||||
id: 1,
|
||||
thread: this.questionThread
|
||||
});
|
||||
window.user = new DiscussionUser({
|
||||
id: 111
|
||||
});
|
||||
expect(this.discussionResponse.canBeEndorsed()).toBe(true);
|
||||
expect(this.questionResponse.canBeEndorsed()).toBe(true);
|
||||
window.user = new DiscussionUser({
|
||||
id: 222
|
||||
});
|
||||
expect(this.discussionResponse.canBeEndorsed()).toBe(true);
|
||||
expect(this.questionResponse.canBeEndorsed()).toBe(true);
|
||||
window.user = new DiscussionUser({
|
||||
id: 333
|
||||
});
|
||||
expect(this.discussionResponse.canBeEndorsed()).toBe(true);
|
||||
expect(this.questionResponse.canBeEndorsed()).toBe(true);
|
||||
window.user = new DiscussionUser({
|
||||
id: 99
|
||||
});
|
||||
expect(this.discussionResponse.canBeEndorsed()).toBe(false);
|
||||
expect(this.questionResponse.canBeEndorsed()).toBe(true);
|
||||
window.user = new DiscussionUser({
|
||||
id: 999
|
||||
});
|
||||
expect(this.discussionResponse.canBeEndorsed()).toBe(false);
|
||||
return expect(this.questionResponse.canBeEndorsed()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
69
common/static/common/js/spec/discussion/utils_spec.js
Normal file
69
common/static/common/js/spec/discussion/utils_spec.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/* globals DiscussionSpecHelper, DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe('DiscussionUtil', function() {
|
||||
beforeEach(function() {
|
||||
return DiscussionSpecHelper.setUpGlobals();
|
||||
});
|
||||
return describe("updateWithUndo", function() {
|
||||
it("calls through to safeAjax with correct params, and reverts the model in case of failure", function() {
|
||||
var deferred, model, res, updates;
|
||||
deferred = $.Deferred();
|
||||
spyOn($, "ajax").and.returnValue(deferred);
|
||||
spyOn(DiscussionUtil, "safeAjax").and.callThrough();
|
||||
model = new Backbone.Model({
|
||||
hello: false,
|
||||
number: 42
|
||||
});
|
||||
updates = {
|
||||
hello: "world"
|
||||
};
|
||||
res = DiscussionUtil.updateWithUndo(model, updates, {
|
||||
foo: "bar"
|
||||
}, "error message");
|
||||
expect(DiscussionUtil.safeAjax).toHaveBeenCalled();
|
||||
expect(model.attributes).toEqual({
|
||||
hello: "world",
|
||||
number: 42
|
||||
});
|
||||
spyOn(DiscussionUtil, "discussionAlert");
|
||||
DiscussionUtil.safeAjax.calls.mostRecent().args[0].error();
|
||||
expect(DiscussionUtil.discussionAlert).toHaveBeenCalledWith("Sorry", "error message");
|
||||
deferred.reject();
|
||||
return expect(model.attributes).toEqual({
|
||||
hello: false,
|
||||
number: 42
|
||||
});
|
||||
});
|
||||
return it("rolls back the changes if the associated element is disabled", function() {
|
||||
var $elem, failed, model, res, updates;
|
||||
spyOn(DiscussionUtil, "safeAjax").and.callThrough();
|
||||
model = new Backbone.Model({
|
||||
hello: false,
|
||||
number: 42
|
||||
});
|
||||
updates = {
|
||||
hello: "world"
|
||||
};
|
||||
$elem = jasmine.createSpyObj('$elem', ['attr']);
|
||||
$elem.attr.and.returnValue(true);
|
||||
res = DiscussionUtil.updateWithUndo(model, updates, {
|
||||
foo: "bar",
|
||||
$elem: $elem
|
||||
}, "error message");
|
||||
expect($elem.attr).toHaveBeenCalledWith("disabled");
|
||||
expect(DiscussionUtil.safeAjax).toHaveBeenCalled();
|
||||
expect(model.attributes).toEqual({
|
||||
hello: false,
|
||||
number: 42
|
||||
});
|
||||
failed = false;
|
||||
res.fail(function() {
|
||||
failed = true;
|
||||
});
|
||||
return expect(failed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,57 @@
|
||||
/* globals DiscussionSpecHelper, DiscussionContentView, Thread */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe("DiscussionContentView", function() {
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.threadData = {
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
body: 'this is a thread',
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
abuse_flaggers: ['123'],
|
||||
votes: {
|
||||
up_count: '42'
|
||||
},
|
||||
type: "thread",
|
||||
roles: []
|
||||
};
|
||||
this.thread = new Thread(this.threadData);
|
||||
this.view = new DiscussionContentView({
|
||||
model: this.thread
|
||||
});
|
||||
this.view.setElement($('#fixture-element'));
|
||||
return this.view.render();
|
||||
});
|
||||
|
||||
it('defines the tag', function() {
|
||||
expect($('#jasmine-fixtures')).toExist();
|
||||
expect(this.view.tagName).toBeDefined();
|
||||
return expect(this.view.el.tagName.toLowerCase()).toBe('div');
|
||||
});
|
||||
|
||||
it("defines the class", function() {
|
||||
return expect(this.view.model).toBeDefined();
|
||||
});
|
||||
|
||||
it('is tied to the model', function() {
|
||||
return expect(this.view.model).toBeDefined();
|
||||
});
|
||||
|
||||
it('can be flagged for abuse', function() {
|
||||
this.thread.flagAbuse();
|
||||
return expect(this.thread.get('abuse_flaggers')).toEqual(['123', '567']);
|
||||
});
|
||||
|
||||
it('can be unflagged for abuse', function() {
|
||||
var temp_array;
|
||||
temp_array = [];
|
||||
temp_array.push(window.user.get('id'));
|
||||
this.thread.set("abuse_flaggers", temp_array);
|
||||
this.thread.unflagAbuse();
|
||||
return expect(this.thread.get('abuse_flaggers')).toEqual([]);
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
@@ -1,3 +1,7 @@
|
||||
/* globals
|
||||
DiscussionCourseSettings, DiscussionSpecHelper, DiscussionThreadEditView, DiscussionUtil,
|
||||
DiscussionViewSpecHelper, Thread
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
describe('DiscussionThreadEditView', function() {
|
||||
@@ -14,7 +18,7 @@
|
||||
this.thread = new Thread(this.threadData);
|
||||
this.course_settings = DiscussionSpecHelper.makeCourseSettings();
|
||||
|
||||
this.createEditView = function (options) {
|
||||
this.createEditView = function(options) {
|
||||
options = _.extend({
|
||||
container: $('#fixture-element'),
|
||||
model: this.thread,
|
||||
@@ -33,10 +37,13 @@
|
||||
expect(params.data.commentable_id).toBe(newTopicId);
|
||||
expect(params.data.title).toBe('changed thread title');
|
||||
params.success();
|
||||
return {always: function() {}};
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
view.$el.find('a.topic-title').filter(function (idx, el) {
|
||||
view.$el.find('a.topic-title').filter(function(idx, el) {
|
||||
return $(el).data('discussionId') === newTopicId;
|
||||
}).click(); // set new topic
|
||||
view.$('.edit-post-title').val('changed thread title'); // set new title
|
||||
@@ -76,6 +83,7 @@
|
||||
this.createEditView({"mode": "inline"});
|
||||
testCancel(this.view);
|
||||
});
|
||||
|
||||
describe('renderComments', function() {
|
||||
beforeEach(function() {
|
||||
this.course_settings = new DiscussionCourseSettings({
|
||||
@@ -100,21 +108,21 @@
|
||||
'is_cohorted': true
|
||||
});
|
||||
});
|
||||
it('can save new data correctly for current discussion id without dots', function () {
|
||||
|
||||
it('can save new data correctly for current discussion id without dots', function() {
|
||||
this.createEditView({topicId: "topic"});
|
||||
testUpdate(this.view, this.thread, "6.00.1x_General", "General");
|
||||
});
|
||||
|
||||
it('can save new data correctly for current discussion id with dots', function () {
|
||||
it('can save new data correctly for current discussion id with dots', function() {
|
||||
this.createEditView({topicId: "6.00.1x_General"});
|
||||
testUpdate(this.view, this.thread, "6>00\'1x\"Basic_Question", "Basic Question");
|
||||
});
|
||||
|
||||
it('can save new data correctly for current discussion id with special characters', function () {
|
||||
it('can save new data correctly for current discussion id with special characters', function() {
|
||||
this.createEditView({topicId: "6>00\'1x\"Basic_Question"});
|
||||
testUpdate(this.view, this.thread, "6.00.1x_General", "General");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}).call(this);
|
||||
@@ -0,0 +1,694 @@
|
||||
/* globals
|
||||
Discussion, DiscussionCourseSettings, DiscussionViewSpecHelper, DiscussionSpecHelper,
|
||||
DiscussionThreadListView, DiscussionUtil, Thread
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
describe("DiscussionThreadListView", function() {
|
||||
var checkThreadsOrdering, expectFilter, makeView, renderSingleThreadWithProps, setupAjax;
|
||||
|
||||
beforeEach(function() {
|
||||
var deferred;
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
// suppressing Line is too long (4272 characters!)
|
||||
/* jshint -W101 */
|
||||
appendSetFixtures("<script type=\"text/template\" id=\"thread-list-template\">\n <div class=\"forum-nav-header\">\n <button type=\"button\" class=\"forum-nav-browse\" id=\"forum-nav-browse\" aria-haspopup=\"true\">\n <span class=\"icon fa fa-bars\" aria-hidden=\"true\"></span>\n <span class=\"sr\">Discussion topics; currently listing: </span>\n <span class=\"forum-nav-browse-current\">All Discussions</span>\n ▾\n </button>\n <form class=\"forum-nav-search\">\n <label>\n <span class=\"sr\">Search all posts</span>\n <input class=\"forum-nav-search-input\" id=\"forum-nav-search\" type=\"text\" placeholder=\"Search all posts\">\n <span class=\"icon fa fa-search\" aria-hidden=\"true\"></span>\n </label>\n </form>\n </div>\n <div class=\"forum-nav-browse-menu-wrapper\" style=\"display: none\">\n <form class=\"forum-nav-browse-filter\">\n <label>\n <span class=\"sr\">Filter Topics</span>\n <input type=\"text\" class=\"forum-nav-browse-filter-input\" placeholder=\"filter topics\">\n </label>\n </form>\n <ul class=\"forum-nav-browse-menu\">\n <li class=\"forum-nav-browse-menu-item forum-nav-browse-menu-all\">\n <a href=\"#\" class=\"forum-nav-browse-title\">All Discussions</a>\n </li>\n <li class=\"forum-nav-browse-menu-item forum-nav-browse-menu-following\">\n <a href=\"#\" class=\"forum-nav-browse-title\"><span class=\"icon fa fa-star\" aria-hidden=\"true\"></span>Posts I'm Following</a>\n </li>\n <li class=\"forum-nav-browse-menu-item\">\n <a href=\"#\" class=\"forum-nav-browse-title\">Parent</a>\n <ul class=\"forum-nav-browse-submenu\">\n <li class=\"forum-nav-browse-menu-item\">\n <a href=\"#\" class=\"forum-nav-browse-title\">Target</a>\n <ul class=\"forum-nav-browse-submenu\">\n <li\n class=\"forum-nav-browse-menu-item\"\n data-discussion-id=\"child\"\n data-cohorted=\"false\"\n >\n <a href=\"#\" class=\"forum-nav-browse-title\">Child</a>\n </li>\n </ul>\n <li\n class=\"forum-nav-browse-menu-item\"\n data-discussion-id=\"sibling\"\n data-cohorted=\"false\"\n >\n <a href=\"#\" class=\"forum-nav-browse-title\">Sibling</a>\n </li>\n </ul>\n </li>\n <li\n class=\"forum-nav-browse-menu-item\"\n data-discussion-id=\"other\"\n data-cohorted=\"true\"\n >\n <a href=\"#\" class=\"forum-nav-browse-title\">Other Category</a>\n </li>\n </ul>\n </div>\n <div class=\"forum-nav-thread-list-wrapper\" id=\"sort-filter-wrapper\" tabindex=\"-1\">\n <div class=\"forum-nav-refine-bar\">\n <label class=\"forum-nav-filter-main\">\n <select class=\"forum-nav-filter-main-control\">\n <option value=\"all\">Show all</option>\n <option value=\"unread\">Unread</option>\n <option value=\"unanswered\">Unanswered</option>\n <option value=\"flagged\">Flagged</option>\n </select>\n </label>\n <% if (isCohorted && isPrivilegedUser) { %>\n <label class=\"forum-nav-filter-cohort\">\n <span class=\"sr\">Cohort:</span>\n <select class=\"forum-nav-filter-cohort-control\">\n <option value=\"\">in all cohorts</option>\n <option value=\"1\">Cohort1</option>\n <option value=\"2\">Cohort2</option>\n </select>\n </label>\n <% } %>\n <label class=\"forum-nav-sort\">\n <select class=\"forum-nav-sort-control\">\n <option value=\"activity\">by recent activity</option>\n <option value=\"comments\">by most activity</option>\n <option value=\"votes\">by most votes</option>\n </select>\n </label>\n </div>\n </div>\n <div class=\"search-alerts\"></div>\n <ul class=\"forum-nav-thread-list\"></ul>\n</script>");
|
||||
/* jshint +W101 */
|
||||
this.threads = [
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "1",
|
||||
title: "Thread1",
|
||||
votes: {
|
||||
up_count: '20'
|
||||
},
|
||||
pinned: true,
|
||||
comments_count: 1,
|
||||
created_at: '2013-04-03T20:08:39Z'
|
||||
}), DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "2",
|
||||
title: "Thread2",
|
||||
votes: {
|
||||
up_count: '42'
|
||||
},
|
||||
comments_count: 2,
|
||||
created_at: '2013-04-03T20:07:39Z'
|
||||
}), DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "3",
|
||||
title: "Thread3",
|
||||
votes: {
|
||||
up_count: '12'
|
||||
},
|
||||
comments_count: 3,
|
||||
created_at: '2013-04-03T20:06:39Z'
|
||||
}), DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "4",
|
||||
title: "Thread4",
|
||||
votes: {
|
||||
up_count: '25'
|
||||
},
|
||||
comments_count: 0,
|
||||
pinned: true,
|
||||
created_at: '2013-04-03T20:05:39Z'
|
||||
})
|
||||
];
|
||||
deferred = $.Deferred();
|
||||
spyOn($, "ajax").and.returnValue(deferred);
|
||||
this.discussion = new Discussion([]);
|
||||
this.view = new DiscussionThreadListView({
|
||||
collection: this.discussion,
|
||||
el: $("#fixture-element"),
|
||||
courseSettings: new DiscussionCourseSettings({
|
||||
is_cohorted: true
|
||||
})
|
||||
});
|
||||
return this.view.render();
|
||||
});
|
||||
setupAjax = function(callback) {
|
||||
return $.ajax.and.callFake(function(params) {
|
||||
if (callback) {
|
||||
callback(params);
|
||||
}
|
||||
params.success({
|
||||
discussion_data: [],
|
||||
page: 1,
|
||||
num_pages: 1
|
||||
});
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
renderSingleThreadWithProps = function(props) {
|
||||
return makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render();
|
||||
};
|
||||
makeView = function(discussion) {
|
||||
return new DiscussionThreadListView({
|
||||
el: $("#fixture-element"),
|
||||
collection: discussion,
|
||||
courseSettings: new DiscussionCourseSettings({
|
||||
is_cohorted: true
|
||||
})
|
||||
});
|
||||
};
|
||||
expectFilter = function(filterVal) {
|
||||
return $.ajax.and.callFake(function(params) {
|
||||
_.each(["unread", "unanswered", "flagged"], function(paramName) {
|
||||
if (paramName === filterVal) {
|
||||
return expect(params.data[paramName]).toEqual(true);
|
||||
} else {
|
||||
return expect(params.data[paramName]).toBeUndefined();
|
||||
}
|
||||
});
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
describe("should filter correctly", function() {
|
||||
return _.each(["all", "unread", "unanswered", "flagged"], function(filterVal) {
|
||||
it("for " + filterVal, function() {
|
||||
expectFilter(filterVal);
|
||||
this.view.$(".forum-nav-filter-main-control").val(filterVal).change();
|
||||
return expect($.ajax).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cohort selector", function() {
|
||||
it("should not be visible to students", function() {
|
||||
return expect(this.view.$(".forum-nav-filter-cohort-control:visible")).not.toExist();
|
||||
});
|
||||
it("should allow moderators to select visibility", function() {
|
||||
var expectedGroupId,
|
||||
self = this;
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.render();
|
||||
expectedGroupId = null;
|
||||
setupAjax(function(params) {
|
||||
return expect(params.data.group_id).toEqual(expectedGroupId);
|
||||
});
|
||||
return _.each([
|
||||
{
|
||||
val: "",
|
||||
expectedGroupId: void 0
|
||||
}, {
|
||||
val: "1",
|
||||
expectedGroupId: "1"
|
||||
}, {
|
||||
val: "2",
|
||||
expectedGroupId: "2"
|
||||
}
|
||||
], function(optionInfo) {
|
||||
expectedGroupId = optionInfo.expectedGroupId;
|
||||
self.view.$(".forum-nav-filter-cohort-control").val(optionInfo.val).change();
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
return $.ajax.calls.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("search should clear filter", function() {
|
||||
expectFilter(null);
|
||||
this.view.$(".forum-nav-filter-main-control").val("flagged");
|
||||
this.view.searchFor("foobar");
|
||||
return expect(this.view.$(".forum-nav-filter-main-control").val()).toEqual("all");
|
||||
});
|
||||
|
||||
checkThreadsOrdering = function(view, sort_order, type) {
|
||||
expect(view.$el.find(".forum-nav-thread").children().length).toEqual(4);
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text())
|
||||
.toEqual(sort_order[0]);
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(2) .forum-nav-thread-title").text())
|
||||
.toEqual(sort_order[1]);
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(3) .forum-nav-thread-title").text())
|
||||
.toEqual(sort_order[2]);
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(4) .forum-nav-thread-title").text())
|
||||
.toEqual(sort_order[3]);
|
||||
return expect(view.$el.find(".forum-nav-sort-control").val()).toEqual(type);
|
||||
};
|
||||
|
||||
describe("thread rendering should be correct", function() {
|
||||
var checkRender;
|
||||
checkRender = function(threads, type, sort_order) {
|
||||
var discussion, view;
|
||||
discussion = new Discussion(_.map(threads, function(thread) {
|
||||
return new Thread(thread);
|
||||
}), {
|
||||
pages: 1,
|
||||
sort: type
|
||||
});
|
||||
view = makeView(discussion);
|
||||
view.render();
|
||||
checkThreadsOrdering(view, sort_order, type);
|
||||
expect(view.$el.find(".forum-nav-thread-comments-count:visible").length)
|
||||
.toEqual(type === "votes" ? 0 : 4);
|
||||
expect(view.$el.find(".forum-nav-thread-votes-count:visible").length)
|
||||
.toEqual(type === "votes" ? 4 : 0);
|
||||
if (type === "votes") {
|
||||
return expect(_.map(view.$el.find(".forum-nav-thread-votes-count"), function(element) {
|
||||
return $(element).text().trim();
|
||||
})).toEqual(["+25 votes", "+20 votes", "+42 votes", "+12 votes"]);
|
||||
}
|
||||
};
|
||||
|
||||
it("with sort preference activity", function() {
|
||||
return checkRender(this.threads, "activity", ["Thread1", "Thread2", "Thread3", "Thread4"]);
|
||||
});
|
||||
|
||||
it("with sort preference votes", function() {
|
||||
return checkRender(this.threads, "votes", ["Thread4", "Thread1", "Thread2", "Thread3"]);
|
||||
});
|
||||
|
||||
it("with sort preference comments", function() {
|
||||
return checkRender(this.threads, "comments", ["Thread1", "Thread4", "Thread3", "Thread2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sort change should be correct", function() {
|
||||
var changeSorting;
|
||||
changeSorting = function(threads, selected_type, new_type, sort_order) {
|
||||
var discussion, sortControl, sorted_threads, view;
|
||||
discussion = new Discussion(_.map(threads, function(thread) {
|
||||
return new Thread(thread);
|
||||
}), {
|
||||
pages: 1,
|
||||
sort: selected_type
|
||||
});
|
||||
view = makeView(discussion);
|
||||
view.render();
|
||||
sortControl = view.$el.find(".forum-nav-sort-control");
|
||||
expect(sortControl.val()).toEqual(selected_type);
|
||||
sorted_threads = [];
|
||||
if (new_type === 'activity') {
|
||||
sorted_threads = [threads[0], threads[3], threads[1], threads[2]];
|
||||
} else if (new_type === 'comments') {
|
||||
sorted_threads = [threads[0], threads[3], threads[2], threads[1]];
|
||||
} else if (new_type === 'votes') {
|
||||
sorted_threads = [threads[3], threads[0], threads[1], threads[2]];
|
||||
}
|
||||
$.ajax.and.callFake(function(params) {
|
||||
params.success({
|
||||
"discussion_data": sorted_threads,
|
||||
page: 1,
|
||||
num_pages: 1
|
||||
});
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
sortControl.val(new_type).change();
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
checkThreadsOrdering(view, sort_order, new_type);
|
||||
};
|
||||
|
||||
it("with sort preference activity", function() {
|
||||
changeSorting(
|
||||
this.threads, "comments", "activity", ["Thread1", "Thread4", "Thread3", "Thread2"]
|
||||
);
|
||||
});
|
||||
|
||||
it("with sort preference votes", function() {
|
||||
changeSorting(this.threads, "activity", "votes", ["Thread4", "Thread1", "Thread2", "Thread3"]);
|
||||
});
|
||||
|
||||
it("with sort preference comments", function() {
|
||||
changeSorting(this.threads, "votes", "comments", ["Thread1", "Thread4", "Thread3", "Thread2"]);
|
||||
});
|
||||
});
|
||||
describe("search alerts", function() {
|
||||
var testAlertMessages;
|
||||
|
||||
testAlertMessages = function(expectedMessages) {
|
||||
return expect($(".search-alert .message").map(function() {
|
||||
return $(this).html();
|
||||
}).get()).toEqual(expectedMessages);
|
||||
};
|
||||
|
||||
it("renders and removes search alerts", function() {
|
||||
var bar, foo;
|
||||
testAlertMessages([]);
|
||||
foo = this.view.addSearchAlert("foo");
|
||||
testAlertMessages(["foo"]);
|
||||
bar = this.view.addSearchAlert("bar");
|
||||
testAlertMessages(["foo", "bar"]);
|
||||
this.view.removeSearchAlert(foo.cid);
|
||||
testAlertMessages(["bar"]);
|
||||
this.view.removeSearchAlert(bar.cid);
|
||||
return testAlertMessages([]);
|
||||
});
|
||||
|
||||
it("clears all search alerts", function() {
|
||||
this.view.addSearchAlert("foo");
|
||||
this.view.addSearchAlert("bar");
|
||||
this.view.addSearchAlert("baz");
|
||||
testAlertMessages(["foo", "bar", "baz"]);
|
||||
this.view.clearSearchAlerts();
|
||||
return testAlertMessages([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search spell correction", function() {
|
||||
var testCorrection;
|
||||
|
||||
beforeEach(function() {
|
||||
return spyOn(this.view, "searchForUser");
|
||||
});
|
||||
|
||||
testCorrection = function(view, correctedText) {
|
||||
spyOn(view, "addSearchAlert");
|
||||
$.ajax.and.callFake(function(params) {
|
||||
params.success({
|
||||
discussion_data: [],
|
||||
page: 42,
|
||||
num_pages: 99,
|
||||
corrected_text: correctedText
|
||||
}, 'success');
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
view.searchFor("dummy");
|
||||
return expect($.ajax).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
it("adds a search alert when an alternate term was searched", function() {
|
||||
testCorrection(this.view, "foo");
|
||||
expect(this.view.addSearchAlert.calls.count()).toEqual(1);
|
||||
return expect(this.view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/foo/);
|
||||
});
|
||||
|
||||
it("does not add a search alert when no alternate term was searched", function() {
|
||||
testCorrection(this.view, null);
|
||||
expect(this.view.addSearchAlert.calls.count()).toEqual(1);
|
||||
return expect(this.view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/no threads matched/i);
|
||||
});
|
||||
|
||||
it("clears search alerts when a new search is performed", function() {
|
||||
spyOn(this.view, "clearSearchAlerts");
|
||||
spyOn(DiscussionUtil, "safeAjax");
|
||||
this.view.searchFor("dummy");
|
||||
return expect(this.view.clearSearchAlerts).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears search alerts when the underlying collection changes", function() {
|
||||
spyOn(this.view, "clearSearchAlerts");
|
||||
spyOn(this.view, "renderThread");
|
||||
this.view.collection.trigger("change", new Thread({
|
||||
id: 1
|
||||
}));
|
||||
return expect(this.view.clearSearchAlerts).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Search events", function() {
|
||||
it("perform search when enter pressed inside search textfield", function() {
|
||||
setupAjax();
|
||||
spyOn(this.view, "searchFor");
|
||||
this.view.$el.find(".forum-nav-search-input").trigger($.Event("keydown", {
|
||||
which: 13
|
||||
}));
|
||||
return expect(this.view.searchFor).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("perform search when search icon is clicked", function() {
|
||||
setupAjax();
|
||||
spyOn(this.view, "searchFor");
|
||||
this.view.$el.find(".fa-search").click();
|
||||
return expect(this.view.searchFor).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("username search", function() {
|
||||
var setAjaxResults;
|
||||
|
||||
it("makes correct ajax calls", function() {
|
||||
$.ajax.and.callFake(function(params) {
|
||||
expect(params.data.username).toEqual("testing-username");
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("users"));
|
||||
params.success({
|
||||
users: []
|
||||
}, 'success');
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
this.view.searchForUser("testing-username");
|
||||
return expect($.ajax).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
setAjaxResults = function(threadSuccess, userResult) {
|
||||
return $.ajax.and.callFake(function(params) {
|
||||
if (params.data.text && threadSuccess) {
|
||||
params.success({
|
||||
discussion_data: [],
|
||||
page: 42,
|
||||
num_pages: 99,
|
||||
corrected_text: "dummy"
|
||||
}, "success");
|
||||
} else if (params.data.username) {
|
||||
params.success({
|
||||
users: userResult
|
||||
}, "success");
|
||||
}
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
it("gets called after a thread search succeeds", function() {
|
||||
spyOn(this.view, "searchForUser").and.callThrough();
|
||||
setAjaxResults(true, []);
|
||||
this.view.searchFor("gizmo");
|
||||
expect(this.view.searchForUser).toHaveBeenCalled();
|
||||
return expect($.ajax.calls.mostRecent().args[0].data.username).toEqual("gizmo");
|
||||
});
|
||||
|
||||
it("does not get called after a thread search fails", function() {
|
||||
spyOn(this.view, "searchForUser").and.callThrough();
|
||||
setAjaxResults(false, []);
|
||||
this.view.searchFor("gizmo");
|
||||
return expect(this.view.searchForUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds a search alert when an username was matched", function() {
|
||||
spyOn(this.view, "addSearchAlert");
|
||||
setAjaxResults(true, [
|
||||
{
|
||||
username: "gizmo",
|
||||
id: "1"
|
||||
}
|
||||
]);
|
||||
this.view.searchForUser("dummy");
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
expect(this.view.addSearchAlert).toHaveBeenCalled();
|
||||
return expect(this.view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/gizmo/);
|
||||
});
|
||||
|
||||
it("does not add a search alert when no username was matched", function() {
|
||||
spyOn(this.view, "addSearchAlert");
|
||||
setAjaxResults(true, []);
|
||||
this.view.searchForUser("dummy");
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
return expect(this.view.addSearchAlert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("post type renders correctly", function() {
|
||||
it("for discussion", function() {
|
||||
renderSingleThreadWithProps({
|
||||
thread_type: "discussion"
|
||||
});
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-comments");
|
||||
return expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("discussion");
|
||||
});
|
||||
|
||||
it("for answered question", function() {
|
||||
renderSingleThreadWithProps({
|
||||
thread_type: "question",
|
||||
endorsed: true
|
||||
});
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-check-square-o");
|
||||
return expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("answered question");
|
||||
});
|
||||
|
||||
it("for unanswered question", function() {
|
||||
renderSingleThreadWithProps({
|
||||
thread_type: "question",
|
||||
endorsed: false
|
||||
});
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-question");
|
||||
return expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("unanswered question");
|
||||
});
|
||||
});
|
||||
|
||||
describe("post labels render correctly", function() {
|
||||
beforeEach(function() {
|
||||
this.moderatorId = "42";
|
||||
this.administratorId = "43";
|
||||
this.communityTaId = "44";
|
||||
return DiscussionUtil.loadRoles({
|
||||
"Moderator": [parseInt(this.moderatorId)],
|
||||
"Administrator": [parseInt(this.administratorId)],
|
||||
"Community TA": [parseInt(this.communityTaId)]
|
||||
});
|
||||
});
|
||||
|
||||
it("for pinned", function() {
|
||||
renderSingleThreadWithProps({
|
||||
pinned: true
|
||||
});
|
||||
return expect($(".post-label-pinned").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("for following", function() {
|
||||
renderSingleThreadWithProps({
|
||||
subscribed: true
|
||||
});
|
||||
return expect($(".post-label-following").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("for moderator", function() {
|
||||
renderSingleThreadWithProps({
|
||||
user_id: this.moderatorId
|
||||
});
|
||||
return expect($(".post-label-by-staff").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("for administrator", function() {
|
||||
renderSingleThreadWithProps({
|
||||
user_id: this.administratorId
|
||||
});
|
||||
return expect($(".post-label-by-staff").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("for community TA", function() {
|
||||
renderSingleThreadWithProps({
|
||||
user_id: this.communityTaId
|
||||
});
|
||||
return expect($(".post-label-by-community-ta").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("when none should be present", function() {
|
||||
renderSingleThreadWithProps({});
|
||||
return expect($(".forum-nav-thread-labels").length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browse menu", function() {
|
||||
var expectBrowseMenuVisible;
|
||||
afterEach(function() {
|
||||
return $("body").unbind("click");
|
||||
});
|
||||
|
||||
expectBrowseMenuVisible = function(isVisible) {
|
||||
expect($(".forum-nav-browse-menu:visible").length).toEqual(isVisible ? 1 : 0);
|
||||
return expect($(".forum-nav-thread-list-wrapper:visible").length).toEqual(isVisible ? 0 : 1);
|
||||
};
|
||||
|
||||
it("should not be visible by default", function() {
|
||||
return expectBrowseMenuVisible(false);
|
||||
});
|
||||
|
||||
it("should show when header button is clicked", function() {
|
||||
$(".forum-nav-browse").click();
|
||||
return expectBrowseMenuVisible(true);
|
||||
});
|
||||
|
||||
describe("when shown", function() {
|
||||
beforeEach(function() {
|
||||
return $(".forum-nav-browse").click();
|
||||
});
|
||||
|
||||
it("should hide when header button is clicked", function() {
|
||||
$(".forum-nav-browse").click();
|
||||
return expectBrowseMenuVisible(false);
|
||||
});
|
||||
|
||||
it("should hide when a click outside the menu occurs", function() {
|
||||
$(".forum-nav-search-input").click();
|
||||
return expectBrowseMenuVisible(false);
|
||||
});
|
||||
|
||||
it("should hide when a search is executed", function() {
|
||||
setupAjax();
|
||||
$(".forum-nav-search-input").trigger($.Event("keydown", {
|
||||
which: 13
|
||||
}));
|
||||
return expectBrowseMenuVisible(false);
|
||||
});
|
||||
|
||||
it("should hide when a category is clicked", function() {
|
||||
$(".forum-nav-browse-title")[0].click();
|
||||
return expectBrowseMenuVisible(false);
|
||||
});
|
||||
|
||||
it("should still be shown when filter input is clicked", function() {
|
||||
$(".forum-nav-browse-filter-input").click();
|
||||
return expectBrowseMenuVisible(true);
|
||||
});
|
||||
|
||||
describe("filtering", function() {
|
||||
var checkFilter;
|
||||
checkFilter = function(filterText, expectedItems) {
|
||||
var visibleItems;
|
||||
$(".forum-nav-browse-filter-input").val(filterText).keyup();
|
||||
visibleItems = $(".forum-nav-browse-title:visible").map(function(i, elem) {
|
||||
return $(elem).text();
|
||||
}).get();
|
||||
return expect(visibleItems).toEqual(expectedItems);
|
||||
};
|
||||
|
||||
it("should be case-insensitive", function() {
|
||||
return checkFilter("other", ["Other Category"]);
|
||||
});
|
||||
|
||||
it("should match partial words", function() {
|
||||
return checkFilter("ateg", ["Other Category"]);
|
||||
});
|
||||
|
||||
it("should show ancestors and descendants of matches", function() {
|
||||
return checkFilter("Target", ["Parent", "Target", "Child"]);
|
||||
});
|
||||
|
||||
it("should handle multiple words regardless of order", function() {
|
||||
return checkFilter("Following Posts", ["Posts I'm Following"]);
|
||||
});
|
||||
|
||||
it("should handle multiple words in different depths", function() {
|
||||
return checkFilter("Parent Child", ["Parent", "Target", "Child"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("selecting an item", function() {
|
||||
var testSelectionRequest;
|
||||
|
||||
it("should clear the search box", function() {
|
||||
setupAjax();
|
||||
$(".forum-nav-search-input").val("foobar");
|
||||
$(".forum-nav-browse-menu-following .forum-nav-browse-title").click();
|
||||
return expect($(".forum-nav-search-input").val()).toEqual("");
|
||||
});
|
||||
|
||||
it("should change the button text", function() {
|
||||
setupAjax();
|
||||
$(".forum-nav-browse-menu-following .forum-nav-browse-title").click();
|
||||
return expect($(".forum-nav-browse-current").text()).toEqual("Posts I'm Following");
|
||||
});
|
||||
|
||||
it("should show/hide the cohort selector", function() {
|
||||
var self = this;
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.render();
|
||||
setupAjax();
|
||||
return _.each([
|
||||
{
|
||||
selector: ".forum-nav-browse-menu-all",
|
||||
cohortVisibility: true
|
||||
}, {
|
||||
selector: ".forum-nav-browse-menu-following",
|
||||
cohortVisibility: false
|
||||
}, {
|
||||
selector: ".forum-nav-browse-menu-item:" +
|
||||
"has(.forum-nav-browse-menu-item .forum-nav-browse-menu-item)",
|
||||
cohortVisibility: false
|
||||
}, {
|
||||
selector: "[data-discussion-id=child]",
|
||||
cohortVisibility: false
|
||||
}, {
|
||||
selector: "[data-discussion-id=other]",
|
||||
cohortVisibility: true
|
||||
}
|
||||
], function(itemInfo) {
|
||||
self.view.$("" + itemInfo.selector + " > .forum-nav-browse-title").click();
|
||||
return expect(self.view.$(".forum-nav-filter-cohort").is(":visible"))
|
||||
.toEqual(itemInfo.cohortVisibility);
|
||||
});
|
||||
});
|
||||
|
||||
testSelectionRequest = function(callback, itemText) {
|
||||
setupAjax(callback);
|
||||
$(".forum-nav-browse-title:contains(" + itemText + ")").click();
|
||||
return expect($.ajax).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
it("should get all discussions", function() {
|
||||
return testSelectionRequest(function(params) {
|
||||
return expect(params.url.path()).toEqual(DiscussionUtil.urlFor("threads"));
|
||||
}, "All");
|
||||
});
|
||||
|
||||
it("should get followed threads", function() {
|
||||
testSelectionRequest(function(params) {
|
||||
return expect(params.url.path())
|
||||
.toEqual(DiscussionUtil.urlFor("followed_threads", window.user.id));
|
||||
}, "Following");
|
||||
return expect($.ajax.calls.mostRecent().args[0].data.group_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should get threads for the selected leaf", function() {
|
||||
return testSelectionRequest(function(params) {
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"));
|
||||
return expect(params.data.commentable_ids).toEqual("child");
|
||||
}, "Child");
|
||||
});
|
||||
|
||||
it("should get threads for children of the selected intermediate node", function() {
|
||||
return testSelectionRequest(function(params) {
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"));
|
||||
return expect(params.data.commentable_ids).toEqual("child,sibling");
|
||||
}, "Parent");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
@@ -0,0 +1,156 @@
|
||||
/* globals Discussion, DiscussionSpecHelper, DiscussionThreadProfileView, Thread */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe("DiscussionThreadProfileView", function() {
|
||||
var checkBody, checkPostWithImages, makeThread, makeView, spyConvertMath;
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.threadData = {
|
||||
id: "1",
|
||||
body: "dummy body",
|
||||
discussion: new Discussion(),
|
||||
abuse_flaggers: [],
|
||||
commentable_id: 'dummy_discussion',
|
||||
votes: {
|
||||
up_count: "42"
|
||||
},
|
||||
created_at: "2014-09-09T20:11:08Z"
|
||||
};
|
||||
this.imageTag = '<img src="https://www.google.com.pk/images/srpr/logo11w.png">';
|
||||
window.MathJax = {
|
||||
Hub: {
|
||||
Queue: function() {
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
makeView = function(thread) {
|
||||
var view;
|
||||
view = new DiscussionThreadProfileView({
|
||||
model: thread
|
||||
});
|
||||
spyConvertMath(view);
|
||||
return view;
|
||||
};
|
||||
makeThread = function(threadData) {
|
||||
var thread;
|
||||
thread = new Thread(threadData);
|
||||
thread.discussion = new Discussion();
|
||||
return thread;
|
||||
};
|
||||
spyConvertMath = function(view) {
|
||||
return spyOn(view, "convertMath").and.callFake(function() {
|
||||
return this.model.set('markdownBody', this.model.get('body'));
|
||||
});
|
||||
};
|
||||
checkPostWithImages = function(numberOfImages, truncatedText, threadData, imageTag) {
|
||||
var expectedHtml, expectedText, i, testText, view, _i, _ref;
|
||||
expectedHtml = '<p>';
|
||||
threadData.body = '<p>';
|
||||
testText = '';
|
||||
expectedText = '';
|
||||
if (truncatedText) {
|
||||
testText = new Array(100).join('test ');
|
||||
expectedText = testText.substring(0, 139) + '…';
|
||||
} else {
|
||||
testText = 'Test body';
|
||||
expectedText = 'Test body';
|
||||
// I really have no idea what it is supposed to mean - probably just iteration, but better be safe
|
||||
for (
|
||||
i = _i = 0, _ref = numberOfImages - 1;
|
||||
0 <= _ref ? _i <= _ref : _i >= _ref;
|
||||
i = 0 <= _ref ? ++_i : --_i
|
||||
) {
|
||||
threadData.body = threadData.body + imageTag;
|
||||
if (i === 0) {
|
||||
expectedHtml = expectedHtml + imageTag;
|
||||
} else {
|
||||
expectedHtml = expectedHtml + '<em>image omitted</em>';
|
||||
}
|
||||
}
|
||||
}
|
||||
threadData.body = threadData.body + '<em>' + testText + '</em></p>';
|
||||
if (numberOfImages > 1) {
|
||||
expectedHtml = expectedHtml + '<em>' + expectedText +
|
||||
'</em></p><p><em>Some images in this post have been omitted</em></p>';
|
||||
} else {
|
||||
expectedHtml = expectedHtml + '<em>' + expectedText + '</em></p>';
|
||||
}
|
||||
view = makeView(makeThread(threadData));
|
||||
view.render();
|
||||
return expect(view.$el.find(".post-body").html()).toEqual(expectedHtml);
|
||||
};
|
||||
checkBody = function(truncated, view, threadData) {
|
||||
var expectedOutput, inputHtmlStripped, outputHtmlStripped;
|
||||
view.render();
|
||||
if (!truncated) {
|
||||
expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody"));
|
||||
return expect(view.$el.find(".post-body").html()).toEqual(threadData.body);
|
||||
} else {
|
||||
expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody"));
|
||||
expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body);
|
||||
outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig, "");
|
||||
outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted", "");
|
||||
outputHtmlStripped = outputHtmlStripped.replace("image omitted", "");
|
||||
inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig, "");
|
||||
expectedOutput = inputHtmlStripped.substring(0, 139) + '…';
|
||||
expect(outputHtmlStripped).toEqual(expectedOutput);
|
||||
return expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0);
|
||||
}
|
||||
};
|
||||
describe("Body markdown should be correct", function() {
|
||||
var numImages, truncatedText, _i, _j, _len, _len1, _ref, _ref1;
|
||||
it("untruncated text without markdown body", function() {
|
||||
var view;
|
||||
this.threadData.body = "Test body";
|
||||
view = makeView(makeThread(this.threadData));
|
||||
return checkBody(false, view, this.threadData);
|
||||
});
|
||||
it("truncated text without markdown body", function() {
|
||||
var view;
|
||||
this.threadData.body = new Array(100).join("test ");
|
||||
view = makeView(makeThread(this.threadData));
|
||||
return checkBody(true, view, this.threadData);
|
||||
});
|
||||
it("untruncated text with markdown body", function() {
|
||||
var view;
|
||||
this.threadData.body = '<p>' + this.imageTag + '<em>Google top search engine</em></p>';
|
||||
view = makeView(makeThread(this.threadData));
|
||||
return checkBody(false, view, this.threadData);
|
||||
});
|
||||
it("truncated text with markdown body", function() {
|
||||
var testText, view;
|
||||
testText = new Array(100).join("test ");
|
||||
this.threadData.body = '<p>' + this.imageTag + this.imageTag + '<em>' + testText + '</em></p>';
|
||||
view = makeView(makeThread(this.threadData));
|
||||
return checkBody(true, view, this.threadData);
|
||||
});
|
||||
_ref = [1, 2, 10];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
numImages = _ref[_i];
|
||||
_ref1 = [true, false];
|
||||
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
|
||||
truncatedText = _ref1[_j];
|
||||
it(
|
||||
"body with " + numImages + " images and " + (truncatedText ? "truncated" : "untruncated") +
|
||||
" text",
|
||||
// suppressing Don't make functions within a loop.
|
||||
/* jshint -W083 */
|
||||
function() {
|
||||
return checkPostWithImages(numImages, truncatedText, this.threadData, this.imageTag);
|
||||
}
|
||||
/* jshint +W083 */
|
||||
);
|
||||
}
|
||||
}
|
||||
it("check the thread retrieve url", function() {
|
||||
var thread;
|
||||
thread = makeThread(this.threadData);
|
||||
return expect(thread.urlFor('retrieve'))
|
||||
.toBe('/courses/edX/999/test/discussion/forum/dummy_discussion/threads/1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,191 @@
|
||||
/* globals DiscussionSpecHelper, DiscussionThreadShowView, DiscussionUtil, DiscussionViewSpecHelper, Thread */
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var $$course_id = "$$course_id";
|
||||
describe("DiscussionThreadShowView", function() {
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.user = DiscussionUtil.getUser();
|
||||
this.threadData = {
|
||||
id: "dummy",
|
||||
user_id: this.user.id,
|
||||
username: this.user.get('username'),
|
||||
course_id: $$course_id,
|
||||
title: "dummy title",
|
||||
body: "this is a thread",
|
||||
created_at: "2013-04-03T20:08:39Z",
|
||||
abuse_flaggers: [],
|
||||
votes: {
|
||||
up_count: 42
|
||||
},
|
||||
thread_type: "discussion",
|
||||
closed: false,
|
||||
pinned: false,
|
||||
type: "thread"
|
||||
};
|
||||
this.thread = new Thread(this.threadData);
|
||||
this.view = new DiscussionThreadShowView({
|
||||
model: this.thread
|
||||
});
|
||||
this.view.setElement($("#fixture-element"));
|
||||
return spyOn(this.view, "convertMath");
|
||||
});
|
||||
describe("voting", function() {
|
||||
it("renders the vote state correctly", function() {
|
||||
return DiscussionViewSpecHelper.checkRenderVote(this.view, this.thread);
|
||||
});
|
||||
it("votes correctly via click", function() {
|
||||
return DiscussionViewSpecHelper.checkUpvote(this.view, this.thread, this.user, $.Event("click"));
|
||||
});
|
||||
it("votes correctly via spacebar", function() {
|
||||
return DiscussionViewSpecHelper.checkUpvote(this.view, this.thread, this.user, $.Event("keydown", {
|
||||
which: 32
|
||||
}));
|
||||
});
|
||||
it("unvotes correctly via click", function() {
|
||||
return DiscussionViewSpecHelper.checkUnvote(this.view, this.thread, this.user, $.Event("click"));
|
||||
});
|
||||
it("unvotes correctly via spacebar", function() {
|
||||
return DiscussionViewSpecHelper.checkUnvote(this.view, this.thread, this.user, $.Event("keydown", {
|
||||
which: 32
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe("pinning", function() {
|
||||
var expectPinnedRendered;
|
||||
expectPinnedRendered = function(view, model) {
|
||||
var button, pinned;
|
||||
pinned = model.get('pinned');
|
||||
button = view.$el.find(".action-pin");
|
||||
expect(button.hasClass("is-checked")).toBe(pinned);
|
||||
return expect(button.attr("aria-checked")).toEqual(pinned.toString());
|
||||
};
|
||||
it("renders the pinned state correctly", function() {
|
||||
this.view.render();
|
||||
expectPinnedRendered(this.view, this.thread);
|
||||
this.thread.set('pinned', false);
|
||||
this.view.render();
|
||||
expectPinnedRendered(this.view, this.thread);
|
||||
this.thread.set('pinned', true);
|
||||
this.view.render();
|
||||
return expectPinnedRendered(this.view, this.thread);
|
||||
});
|
||||
it("exposes the pinning control only to authorized users", function() {
|
||||
this.thread.updateInfo({
|
||||
ability: {
|
||||
can_openclose: false
|
||||
}
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$el.find(".action-pin").closest(".is-hidden")).toExist();
|
||||
this.thread.updateInfo({
|
||||
ability: {
|
||||
can_openclose: true
|
||||
}
|
||||
});
|
||||
this.view.render();
|
||||
return expect(this.view.$el.find(".action-pin").closest(".is-hidden")).not.toExist();
|
||||
});
|
||||
it("handles events correctly", function() {
|
||||
this.view.render();
|
||||
return DiscussionViewSpecHelper.checkButtonEvents(this.view, "togglePin", ".action-pin");
|
||||
});
|
||||
});
|
||||
describe("labels", function() {
|
||||
var expectOneElement;
|
||||
expectOneElement = function(view, selector, visible) {
|
||||
var elements;
|
||||
if (typeof visible === "undefined" || visible === null) {
|
||||
visible = true;
|
||||
}
|
||||
view.render();
|
||||
elements = view.$el.find(selector);
|
||||
expect(elements.length).toEqual(1);
|
||||
if (visible) {
|
||||
return expect(elements).not.toHaveClass("is-hidden");
|
||||
} else {
|
||||
return expect(elements).toHaveClass("is-hidden");
|
||||
}
|
||||
};
|
||||
it('displays the closed label when appropriate', function() {
|
||||
expectOneElement(this.view, '.post-label-closed', false);
|
||||
this.thread.set('closed', true);
|
||||
return expectOneElement(this.view, '.post-label-closed');
|
||||
});
|
||||
it('displays the pinned label when appropriate', function() {
|
||||
expectOneElement(this.view, '.post-label-pinned', false);
|
||||
this.thread.set('pinned', true);
|
||||
return expectOneElement(this.view, '.post-label-pinned');
|
||||
});
|
||||
it('displays the reported label when appropriate for a non-staff user', function() {
|
||||
expectOneElement(this.view, '.post-label-reported', false);
|
||||
this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]);
|
||||
expectOneElement(this.view, '.post-label-reported');
|
||||
this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]);
|
||||
return expectOneElement(this.view, '.post-label-reported', false);
|
||||
});
|
||||
it('displays the reported label when appropriate for a flag moderator', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
expectOneElement(this.view, '.post-label-reported', false);
|
||||
this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]);
|
||||
expectOneElement(this.view, '.post-label-reported');
|
||||
this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]);
|
||||
return expectOneElement(this.view, '.post-label-reported');
|
||||
});
|
||||
});
|
||||
describe("author display", function() {
|
||||
var checkUserLink;
|
||||
beforeEach(function() {
|
||||
return this.thread.set('user_url', 'test_user_url');
|
||||
});
|
||||
checkUserLink = function(element, is_ta, is_staff) {
|
||||
expect(element.find('a.username').length).toEqual(1);
|
||||
expect(element.find('a.username').text()).toEqual('test_user');
|
||||
expect(element.find('a.username').attr('href')).toEqual('test_user_url');
|
||||
expect(element.find('.user-label-community-ta').length).toEqual(is_ta ? 1 : 0);
|
||||
return expect(element.find('.user-label-staff').length).toEqual(is_staff ? 1 : 0);
|
||||
};
|
||||
it("renders correctly for a student-authored thread", function() {
|
||||
var $el;
|
||||
$el = $('#fixture-element').html(this.view.getAuthorDisplay());
|
||||
return checkUserLink($el, false, false);
|
||||
});
|
||||
it("renders correctly for a community TA-authored thread", function() {
|
||||
var $el;
|
||||
this.thread.set('community_ta_authored', true);
|
||||
$el = $('#fixture-element').html(this.view.getAuthorDisplay());
|
||||
return checkUserLink($el, true, false);
|
||||
});
|
||||
it("renders correctly for a staff-authored thread", function() {
|
||||
var $el;
|
||||
this.thread.set('staff_authored', true);
|
||||
$el = $('#fixture-element').html(this.view.getAuthorDisplay());
|
||||
return checkUserLink($el, false, true);
|
||||
});
|
||||
it("renders correctly for an anonymously-authored thread", function() {
|
||||
var $el;
|
||||
this.thread.set('username', null);
|
||||
$el = $('#fixture-element').html(this.view.getAuthorDisplay());
|
||||
expect($el.find('a.username').length).toEqual(0);
|
||||
return expect($el.text()).toMatch(/^(\s*)anonymous(\s*)$/);
|
||||
});
|
||||
});
|
||||
describe("cohorting", function() {
|
||||
it("renders correctly for an uncohorted thread", function() {
|
||||
this.view.render();
|
||||
return expect(this.view.$('.group-visibility-label').text().trim())
|
||||
.toEqual('This post is visible to everyone.');
|
||||
});
|
||||
it("renders correctly for a cohorted thread", function() {
|
||||
this.thread.set('group_id', '1');
|
||||
this.thread.set('group_name', 'Mock Cohort');
|
||||
this.view.render();
|
||||
return expect(this.view.$('.group-visibility-label').text().trim())
|
||||
.toEqual('This post is visible only to Mock Cohort.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,482 @@
|
||||
/* global
|
||||
Discussion, DiscussionThreadShowView, DiscussionViewSpecHelper, DiscussionSpecHelper, DiscussionThreadView,
|
||||
DiscussionUtil, Thread, DiscussionContentView, ThreadResponseShowView
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
describe("DiscussionThreadView", function() {
|
||||
var assertContentVisible, assertExpandedContentVisible, assertResponseCountAndPaginationCorrect,
|
||||
createAjaxResponseJson, createTestResponseJson, postResponse, renderWithContent, renderWithTestResponses;
|
||||
beforeEach(function() {
|
||||
var deferred;
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
jasmine.clock().install();
|
||||
this.threadData = DiscussionViewSpecHelper.makeThreadWithProps({});
|
||||
this.thread = new Thread(this.threadData);
|
||||
this.discussion = new Discussion(this.thread);
|
||||
deferred = $.Deferred();
|
||||
spyOn($, "ajax").and.returnValue(deferred);
|
||||
spyOn(DiscussionThreadShowView.prototype, "convertMath");
|
||||
spyOn(DiscussionContentView.prototype, "makeWmdEditor");
|
||||
spyOn(DiscussionUtil, "makeWmdEditor");
|
||||
spyOn(DiscussionUtil, "setWmdContent");
|
||||
return spyOn(ThreadResponseShowView.prototype, "convertMath");
|
||||
});
|
||||
afterEach(function() {
|
||||
$.ajax.calls.reset();
|
||||
return jasmine.clock().uninstall();
|
||||
});
|
||||
renderWithContent = function(view, content) {
|
||||
$.ajax.and.callFake(function(params) {
|
||||
params.success(createAjaxResponseJson(content, false), 'success');
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
view.render();
|
||||
return jasmine.clock().tick(100);
|
||||
};
|
||||
renderWithTestResponses = function(view, count, options) {
|
||||
var index;
|
||||
return renderWithContent(view, _.extend({
|
||||
resp_total: count,
|
||||
children: count > 0 ? (function() {
|
||||
var _i, _results;
|
||||
_results = [];
|
||||
for (index = _i = 1; 1 <= count ? _i <= count : _i >= count; index = 1 <= count ? ++_i : --_i) {
|
||||
_results.push(createTestResponseJson(index));
|
||||
}
|
||||
return _results;
|
||||
})() : []
|
||||
}, options));
|
||||
};
|
||||
createTestResponseJson = function(index) {
|
||||
return {
|
||||
user_id: window.user.id,
|
||||
body: "Response " + index,
|
||||
id: "id_" + index,
|
||||
created_at: "2015-01-01T22:20:28Z"
|
||||
};
|
||||
};
|
||||
assertContentVisible = function(view, selector, visible) {
|
||||
var content;
|
||||
content = view.$el.find(selector);
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
return content.each(function(i, elem) {
|
||||
return expect($(elem).is(":visible")).toEqual(visible);
|
||||
});
|
||||
};
|
||||
assertExpandedContentVisible = function(view, expanded) {
|
||||
expect(view.$el.hasClass("expanded")).toEqual(expanded);
|
||||
assertContentVisible(view, ".post-extended-content", expanded);
|
||||
assertContentVisible(view, ".forum-thread-expand", !expanded);
|
||||
return assertContentVisible(view, ".forum-thread-collapse", expanded);
|
||||
};
|
||||
assertResponseCountAndPaginationCorrect = function(view, countText, displayCountText, buttonText) {
|
||||
expect(view.$el.find(".response-count").text()).toEqual(countText);
|
||||
if (displayCountText) {
|
||||
expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText);
|
||||
} else {
|
||||
expect(view.$el.find(".response-display-count").length).toEqual(0);
|
||||
}
|
||||
if (buttonText) {
|
||||
return expect(view.$el.find(".load-response-button").text()).toEqual(buttonText);
|
||||
} else {
|
||||
return expect(view.$el.find(".load-response-button").length).toEqual(0);
|
||||
}
|
||||
};
|
||||
createAjaxResponseJson = function(content, can_act) {
|
||||
return {
|
||||
content: content,
|
||||
annotated_content_info: {
|
||||
ability: {
|
||||
editable: can_act,
|
||||
can_delete: can_act,
|
||||
can_reply: can_act,
|
||||
can_vote: can_act
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
postResponse = function(view, index) {
|
||||
var responseText, testResponseJson;
|
||||
testResponseJson = createTestResponseJson(index);
|
||||
responseText = testResponseJson.body;
|
||||
spyOn(view, "getWmdContent").and.returnValue(responseText);
|
||||
$.ajax.and.callFake(function(params) {
|
||||
expect(params.type).toEqual("POST");
|
||||
expect(params.data.body).toEqual(responseText);
|
||||
params.success(createAjaxResponseJson(testResponseJson, true), 'success');
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
return view.$(".discussion-submit-post").click();
|
||||
};
|
||||
describe("closed and open Threads", function() {
|
||||
var checkCommentForm, checkVoteDisplay, createDiscussionThreadView;
|
||||
createDiscussionThreadView = function(originallyClosed, mode) {
|
||||
var discussion, thread, threadData, view;
|
||||
threadData = DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
closed: originallyClosed
|
||||
});
|
||||
thread = new Thread(threadData);
|
||||
discussion = new Discussion(thread);
|
||||
view = new DiscussionThreadView({
|
||||
model: thread,
|
||||
el: $("#fixture-element"),
|
||||
mode: mode,
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
});
|
||||
renderWithTestResponses(view, 1);
|
||||
if (mode === "inline") {
|
||||
view.expand();
|
||||
}
|
||||
spyOn(DiscussionUtil, "updateWithUndo").and.callFake(function(model, updates) {
|
||||
return model.set(updates);
|
||||
});
|
||||
return view;
|
||||
};
|
||||
checkCommentForm = function(originallyClosed, mode) {
|
||||
var view;
|
||||
view = createDiscussionThreadView(originallyClosed, mode);
|
||||
expect(view.$('.comment-form').closest('li').is(":visible")).toBe(!originallyClosed);
|
||||
expect(view.$(".discussion-reply-new").is(":visible")).toBe(!originallyClosed);
|
||||
view.$(".action-close").click();
|
||||
expect(view.$('.comment-form').closest('li').is(":visible")).toBe(originallyClosed);
|
||||
return expect(view.$(".discussion-reply-new").is(":visible")).toBe(originallyClosed);
|
||||
};
|
||||
checkVoteDisplay = function(originallyClosed, mode) {
|
||||
var view;
|
||||
view = createDiscussionThreadView(originallyClosed, mode);
|
||||
expect(view.$('.thread-main-wrapper .action-vote').is(":visible")).toBe(!originallyClosed);
|
||||
expect(view.$('.thread-main-wrapper .display-vote').is(":visible")).toBe(originallyClosed);
|
||||
view.$(".action-close").click();
|
||||
expect(view.$('.action-vote').is(":visible")).toBe(originallyClosed);
|
||||
return expect(view.$('.display-vote').is(":visible")).toBe(!originallyClosed);
|
||||
};
|
||||
return _.each(["tab", "inline"], function(mode) {
|
||||
it(
|
||||
"Test that in " + mode + " mode when a closed thread is opened the comment form is displayed",
|
||||
function() { return checkCommentForm(true, mode); }
|
||||
);
|
||||
it(
|
||||
"Test that in " + mode + " mode when a open thread is closed the comment form is hidden",
|
||||
function() { return checkCommentForm(false, mode); }
|
||||
);
|
||||
it(
|
||||
"Test that in " + mode + " mode when a closed thread is opened the vote button is displayed and " +
|
||||
"vote count is hidden",
|
||||
function() { return checkVoteDisplay(true, mode); }
|
||||
);
|
||||
it(
|
||||
"Test that in " + mode + " mode when a open thread is closed the vote button is hidden and " +
|
||||
"vote count is displayed",
|
||||
function() { return checkVoteDisplay(false, mode); }
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("tab mode", function() {
|
||||
beforeEach(function() {
|
||||
this.view = new DiscussionThreadView({
|
||||
model: this.thread,
|
||||
el: $("#fixture-element"),
|
||||
mode: "tab",
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
});
|
||||
});
|
||||
describe("responses", function() {
|
||||
it("can post a first response", function() {
|
||||
renderWithTestResponses(this.view, 0);
|
||||
postResponse(this.view, 1);
|
||||
expect(this.view.$(".forum-response").length).toBe(1);
|
||||
expect(this.view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1);
|
||||
return expect(this.view.$(".response-actions-list").find(".action-edit")
|
||||
.parent().not(".is-hidden").length).toBe(1);
|
||||
});
|
||||
it("can post a second response", function() {
|
||||
renderWithTestResponses(this.view, 1);
|
||||
expect(this.view.$(".forum-response").length).toBe(1);
|
||||
expect(this.view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1);
|
||||
expect(this.view.$(".response-actions-list").find(".action-edit").parent()
|
||||
.not(".is-hidden").length).toBe(1);
|
||||
postResponse(this.view, 2);
|
||||
expect(this.view.$(".forum-response").length).toBe(2);
|
||||
expect(this.view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1);
|
||||
return expect(this.view.$(".response-actions-list").find(".action-edit").parent()
|
||||
.not(".is-hidden").length).toBe(2);
|
||||
});
|
||||
});
|
||||
describe("response count and pagination", function() {
|
||||
it("correctly render for a thread with no responses", function() {
|
||||
renderWithTestResponses(this.view, 0);
|
||||
return assertResponseCountAndPaginationCorrect(this.view, "0 responses", null, null);
|
||||
});
|
||||
it("correctly render for a thread with one response", function() {
|
||||
renderWithTestResponses(this.view, 1);
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "1 response", "Showing all responses", null
|
||||
);
|
||||
});
|
||||
it("correctly render for a thread with one additional page", function() {
|
||||
renderWithTestResponses(this.view, 1, {
|
||||
resp_total: 2
|
||||
});
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "2 responses", "Showing first response", "Load all responses"
|
||||
);
|
||||
});
|
||||
it("correctly render for a thread with multiple additional pages", function() {
|
||||
renderWithTestResponses(this.view, 2, {
|
||||
resp_total: 111
|
||||
});
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "111 responses", "Showing first 2 responses", "Load next 100 responses"
|
||||
);
|
||||
});
|
||||
describe("on clicking the load more button", function() {
|
||||
beforeEach(function() {
|
||||
renderWithTestResponses(this.view, 1, {
|
||||
resp_total: 5
|
||||
});
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "5 responses", "Showing first response", "Load all responses"
|
||||
);
|
||||
});
|
||||
it("correctly re-render when all threads have loaded", function() {
|
||||
renderWithTestResponses(this.view, 5, {
|
||||
resp_total: 5
|
||||
});
|
||||
this.view.$el.find(".load-response-button").click();
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "5 responses", "Showing all responses", null
|
||||
);
|
||||
});
|
||||
it("correctly re-render when one page remains", function() {
|
||||
renderWithTestResponses(this.view, 3, {
|
||||
resp_total: 42
|
||||
});
|
||||
this.view.$el.find(".load-response-button").click();
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "42 responses", "Showing first 3 responses", "Load all responses"
|
||||
);
|
||||
});
|
||||
it("correctly re-render when multiple pages remain", function() {
|
||||
renderWithTestResponses(this.view, 3, {
|
||||
resp_total: 111
|
||||
});
|
||||
this.view.$el.find(".load-response-button").click();
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "111 responses", "Showing first 3 responses", "Load next 100 responses"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("inline mode", function() {
|
||||
beforeEach(function() {
|
||||
this.view = new DiscussionThreadView({
|
||||
model: this.thread,
|
||||
el: $("#fixture-element"),
|
||||
mode: "inline",
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
});
|
||||
});
|
||||
describe("render", function() {
|
||||
it("shows content that should be visible when collapsed", function() {
|
||||
this.view.render();
|
||||
return assertExpandedContentVisible(this.view, false);
|
||||
});
|
||||
it("does not render any responses by default", function() {
|
||||
this.view.render();
|
||||
expect($.ajax).not.toHaveBeenCalled();
|
||||
return expect(this.view.$el.find(".responses li").length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe("focus", function() {
|
||||
it("sends focus to the conversation when opened", function(done) {
|
||||
var self;
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
resp_total: 0,
|
||||
children: []
|
||||
});
|
||||
this.view.render();
|
||||
this.view.expand();
|
||||
self = this;
|
||||
return jasmine.waitUntil(function() {
|
||||
var article;
|
||||
article = self.view.$el.find('.discussion-article');
|
||||
return article[0] === article[0].ownerDocument.activeElement;
|
||||
}).then(function() {
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("expand/collapse", function() {
|
||||
it("shows/hides appropriate content", function() {
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
resp_total: 0,
|
||||
children: []
|
||||
});
|
||||
this.view.render();
|
||||
this.view.expand();
|
||||
assertExpandedContentVisible(this.view, true);
|
||||
this.view.collapse();
|
||||
return assertExpandedContentVisible(this.view, false);
|
||||
});
|
||||
it("switches between the abbreviated and full body", function() {
|
||||
var expectedAbbreviation, longBody;
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
resp_total: 0,
|
||||
children: []
|
||||
});
|
||||
longBody = new Array(100).join("test ");
|
||||
expectedAbbreviation = DiscussionUtil.abbreviateString(longBody, 140);
|
||||
this.thread.set("body", longBody);
|
||||
this.view.render();
|
||||
expect($(".post-body").text()).toEqual(expectedAbbreviation);
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled();
|
||||
DiscussionThreadShowView.prototype.convertMath.calls.reset();
|
||||
this.view.expand();
|
||||
expect($(".post-body").text()).toEqual(longBody);
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled();
|
||||
DiscussionThreadShowView.prototype.convertMath.calls.reset();
|
||||
this.view.collapse();
|
||||
expect($(".post-body").text()).toEqual(expectedAbbreviation);
|
||||
return expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled();
|
||||
});
|
||||
it("strips script tags appropriately", function() {
|
||||
var longMaliciousBody, maliciousAbbreviation;
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
resp_total: 0,
|
||||
children: []
|
||||
});
|
||||
longMaliciousBody = new Array(100).join(
|
||||
"<script>alert('Until they think warm days will never cease');</script>\n"
|
||||
);
|
||||
this.thread.set("body", longMaliciousBody);
|
||||
maliciousAbbreviation = DiscussionUtil.abbreviateString(this.thread.get('body'), 140);
|
||||
this.view.render();
|
||||
expect($(".post-body").html()).not.toEqual(maliciousAbbreviation);
|
||||
expect($(".post-body").text()).toEqual(maliciousAbbreviation);
|
||||
expect($(".post-body").html()).not.toContain("<script");
|
||||
this.view.expand();
|
||||
expect($(".post-body").html()).not.toEqual(longMaliciousBody);
|
||||
expect($(".post-body").text()).toEqual(longMaliciousBody);
|
||||
expect($(".post-body").html()).not.toContain("<script");
|
||||
this.view.collapse();
|
||||
expect($(".post-body").html()).not.toEqual(maliciousAbbreviation);
|
||||
expect($(".post-body").text()).toEqual(maliciousAbbreviation);
|
||||
return expect($(".post-body").html()).not.toContain("<script");
|
||||
});
|
||||
it("re-renders the show view correctly when leaving the edit view", function() {
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
resp_total: 0,
|
||||
children: []
|
||||
});
|
||||
this.view.render();
|
||||
this.view.expand();
|
||||
assertExpandedContentVisible(this.view, true);
|
||||
this.view.edit();
|
||||
assertContentVisible(this.view, ".edit-post-body", true);
|
||||
expect(this.view.$el.find(".post-actions-list").length).toBe(0);
|
||||
this.view.closeEditView(DiscussionSpecHelper.makeEventSpy());
|
||||
expect(this.view.$el.find(".edit-post-body").length).toBe(0);
|
||||
return assertContentVisible(this.view, ".post-actions-list", true);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("for question threads", function() {
|
||||
var generateContent, renderTestCase;
|
||||
beforeEach(function() {
|
||||
this.thread.set("thread_type", "question");
|
||||
this.view = new DiscussionThreadView({
|
||||
model: this.thread,
|
||||
el: $("#fixture-element"),
|
||||
mode: "tab",
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
});
|
||||
});
|
||||
generateContent = function(idStart, idEnd) {
|
||||
return _.map(_.range(idStart, idEnd), function(i) {
|
||||
return createTestResponseJson(i);
|
||||
});
|
||||
};
|
||||
renderTestCase = function(view, numEndorsed, numNonEndorsed) {
|
||||
renderWithContent(view, {
|
||||
endorsed_responses: generateContent(0, numEndorsed),
|
||||
non_endorsed_responses: generateContent(numEndorsed, numEndorsed + numNonEndorsed),
|
||||
non_endorsed_resp_total: numNonEndorsed
|
||||
});
|
||||
expect(view.$(".js-marked-answer-list .discussion-response").length).toEqual(numEndorsed);
|
||||
expect(view.$(".js-response-list .discussion-response").length).toEqual(numNonEndorsed);
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
view, "" + numNonEndorsed + " " + (numEndorsed ? "other " : "") +
|
||||
(numNonEndorsed === 1 ? "response" : "responses"),
|
||||
numNonEndorsed ? "Showing all responses" : null, null
|
||||
);
|
||||
};
|
||||
_.each({
|
||||
"no": 0,
|
||||
"one": 1,
|
||||
"many": 5
|
||||
}, function(numEndorsed, endorsedDesc) {
|
||||
return _.each({
|
||||
"no": 0,
|
||||
"one": 1,
|
||||
"many": 5
|
||||
}, function(numNonEndorsed, nonEndorsedDesc) {
|
||||
it(
|
||||
"renders correctly with " + endorsedDesc + " marked answer(s) and " + nonEndorsedDesc +
|
||||
" response(s)",
|
||||
function() { return renderTestCase(this.view, numEndorsed, numNonEndorsed); }
|
||||
);
|
||||
});
|
||||
});
|
||||
it("handles pagination correctly", function() {
|
||||
renderWithContent(this.view, {
|
||||
endorsed_responses: generateContent(0, 2),
|
||||
non_endorsed_responses: generateContent(3, 6),
|
||||
non_endorsed_resp_total: 42
|
||||
});
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
endorsed_responses: generateContent(0, 3),
|
||||
non_endorsed_responses: generateContent(6, 9),
|
||||
non_endorsed_resp_total: 41
|
||||
});
|
||||
this.view.$el.find(".load-response-button").click();
|
||||
expect($(".js-marked-answer-list .discussion-response").length).toEqual(3);
|
||||
expect($(".js-response-list .discussion-response").length).toEqual(6);
|
||||
return assertResponseCountAndPaginationCorrect(
|
||||
this.view, "41 other responses", "Showing first 6 responses", "Load all responses"
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("post restrictions", function() {
|
||||
beforeEach(function() {
|
||||
this.thread.attributes.ability = _.extend(this.thread.attributes.ability, {
|
||||
can_report: false,
|
||||
can_vote: false
|
||||
});
|
||||
this.view = new DiscussionThreadView({
|
||||
model: this.thread,
|
||||
el: $("#fixture-element"),
|
||||
mode: "tab",
|
||||
course_settings: DiscussionSpecHelper.makeCourseSettings()
|
||||
});
|
||||
});
|
||||
it("doesn't show report option if can_report ability is disabled", function() {
|
||||
this.view.render();
|
||||
return expect(this.view.$el.find(".action-report").closest(".actions-item")).toHaveClass('is-hidden');
|
||||
});
|
||||
it("doesn't show voting button if can_vote ability is disabled", function() {
|
||||
this.view.render();
|
||||
return expect(this.view.$el.find(".action-vote").closest(".actions-item")).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1,8 +1,9 @@
|
||||
/* globals DiscussionTopicMenuView, DiscussionSpecHelper, DiscussionCourseSettings */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe('DiscussionTopicMenuView', function() {
|
||||
beforeEach(function() {
|
||||
this.createTopicView = function (options) {
|
||||
this.createTopicView = function(options) {
|
||||
options = _.extend({
|
||||
course_settings: this.course_settings,
|
||||
topicId: void 0
|
||||
@@ -12,14 +13,14 @@
|
||||
this.defaultTextWidth = this.completeText.length;
|
||||
};
|
||||
|
||||
this.openMenu = function () {
|
||||
this.openMenu = function() {
|
||||
var menuWrapper = this.view.$('.topic-menu-wrapper');
|
||||
expect(menuWrapper).toBeHidden();
|
||||
this.view.$el.find('.post-topic-button').first().click();
|
||||
expect(menuWrapper).toBeVisible();
|
||||
};
|
||||
|
||||
this.closeMenu = function () {
|
||||
this.closeMenu = function() {
|
||||
var menuWrapper = this.view.$('.topic-menu-wrapper');
|
||||
expect(menuWrapper).toBeVisible();
|
||||
this.view.$el.find('.post-topic-button').first().click();
|
||||
@@ -76,7 +77,7 @@
|
||||
expect(dropdownText.indexOf('/ span>')).toEqual(-1);
|
||||
});
|
||||
|
||||
it('appropriate topic is selected if `topicId` is passed', function () {
|
||||
it('appropriate topic is selected if `topicId` is passed', function() {
|
||||
var completeText = this.parentCategoryText + ' / Numerical Input',
|
||||
dropdownText;
|
||||
this.createTopicView({
|
||||
@@ -88,14 +89,14 @@
|
||||
expect(completeText).toEqual(dropdownText);
|
||||
});
|
||||
|
||||
it('click outside of the dropdown close it', function () {
|
||||
it('click outside of the dropdown close it', function() {
|
||||
this.createTopicView();
|
||||
this.openMenu();
|
||||
$(document.body).click();
|
||||
expect(this.view.$('.topic-menu-wrapper')).toBeHidden();
|
||||
});
|
||||
|
||||
it('can toggle the menu', function () {
|
||||
it('can toggle the menu', function() {
|
||||
this.createTopicView();
|
||||
this.openMenu();
|
||||
this.closeMenu();
|
||||
@@ -0,0 +1,257 @@
|
||||
/* globals DiscussionSpecHelper, DiscussionThreadProfileView, DiscussionUserProfileView, URI, DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe("DiscussionUserProfileView", function() {
|
||||
var makeThreads, makeView;
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
return spyOn(DiscussionThreadProfileView.prototype, "render");
|
||||
});
|
||||
makeThreads = function(numThreads) {
|
||||
return _.map(_.range(numThreads), function(i) {
|
||||
return {
|
||||
id: i.toString(),
|
||||
body: "dummy body"
|
||||
};
|
||||
});
|
||||
};
|
||||
makeView = function(threads, page, numPages) {
|
||||
return new DiscussionUserProfileView({
|
||||
collection: threads,
|
||||
page: page,
|
||||
numPages: numPages
|
||||
});
|
||||
};
|
||||
describe("thread rendering should be correct", function() {
|
||||
var checkRender;
|
||||
checkRender = function(numThreads) {
|
||||
var threads, view;
|
||||
threads = makeThreads(numThreads);
|
||||
view = makeView(threads, 1, 1);
|
||||
expect(view.$(".discussion").children().length).toEqual(numThreads);
|
||||
return _.each(threads, function(thread) {
|
||||
return expect(view.$("#thread_" + thread.id).length).toEqual(1);
|
||||
});
|
||||
};
|
||||
it("with no threads", function() {
|
||||
return checkRender(0);
|
||||
});
|
||||
it("with one thread", function() {
|
||||
return checkRender(1);
|
||||
});
|
||||
it("with several threads", function() {
|
||||
return checkRender(5);
|
||||
});
|
||||
});
|
||||
describe("pagination rendering should be correct", function() {
|
||||
var baseUri, checkRender, pageInfo;
|
||||
baseUri = URI(window.location);
|
||||
pageInfo = function(page) {
|
||||
return {
|
||||
url: baseUri.clone().addSearch("page", page).toString(),
|
||||
number: page
|
||||
};
|
||||
};
|
||||
checkRender = function(params) {
|
||||
var get_page_number, paginator, view;
|
||||
view = makeView([], params.page, params.numPages);
|
||||
paginator = view.$(".discussion-paginator");
|
||||
expect(paginator.find(".current-page").text()).toEqual(params.page.toString());
|
||||
expect(paginator.find(".first-page").length).toBe(params.first ? 1 : 0);
|
||||
expect(paginator.find(".previous-page").length).toBe(params.previous ? 1 : 0);
|
||||
expect(paginator.find(".previous-ellipses").length).toBe(params.leftdots ? 1 : 0);
|
||||
expect(paginator.find(".next-page").length).toBe(params.next ? 1 : 0);
|
||||
expect(paginator.find(".next-ellipses").length).toBe(params.rightdots ? 1 : 0);
|
||||
expect(paginator.find(".last-page").length).toBe(params.last ? 1 : 0);
|
||||
get_page_number = function(element) {
|
||||
return parseInt($(element).text());
|
||||
};
|
||||
expect(_.map(paginator.find(".lower-page a"), get_page_number)).toEqual(params.lowPages);
|
||||
return expect(_.map(paginator.find(".higher-page a"), get_page_number)).toEqual(params.highPages);
|
||||
};
|
||||
it("for one page", function() {
|
||||
return checkRender({
|
||||
page: 1,
|
||||
numPages: 1,
|
||||
previous: null,
|
||||
first: null,
|
||||
leftdots: false,
|
||||
lowPages: [],
|
||||
highPages: [],
|
||||
rightdots: false,
|
||||
last: null,
|
||||
next: null
|
||||
});
|
||||
});
|
||||
it("for first page of three (max with no last)", function() {
|
||||
return checkRender({
|
||||
page: 1,
|
||||
numPages: 3,
|
||||
previous: null,
|
||||
first: null,
|
||||
leftdots: false,
|
||||
lowPages: [],
|
||||
highPages: [2, 3],
|
||||
rightdots: false,
|
||||
last: null,
|
||||
next: 2
|
||||
});
|
||||
});
|
||||
it("for first page of four (has last but no dots)", function() {
|
||||
return checkRender({
|
||||
page: 1,
|
||||
numPages: 4,
|
||||
previous: null,
|
||||
first: null,
|
||||
leftdots: false,
|
||||
lowPages: [],
|
||||
highPages: [2, 3],
|
||||
rightdots: false,
|
||||
last: 4,
|
||||
next: 2
|
||||
});
|
||||
});
|
||||
it("for first page of five (has dots)", function() {
|
||||
return checkRender({
|
||||
page: 1,
|
||||
numPages: 5,
|
||||
previous: null,
|
||||
first: null,
|
||||
leftdots: false,
|
||||
lowPages: [],
|
||||
highPages: [2, 3],
|
||||
rightdots: true,
|
||||
last: 5,
|
||||
next: 2
|
||||
});
|
||||
});
|
||||
it("for last page of three (max with no first)", function() {
|
||||
return checkRender({
|
||||
page: 3,
|
||||
numPages: 3,
|
||||
previous: 2,
|
||||
first: null,
|
||||
leftdots: false,
|
||||
lowPages: [1, 2],
|
||||
highPages: [],
|
||||
rightdots: false,
|
||||
last: null,
|
||||
next: null
|
||||
});
|
||||
});
|
||||
it("for last page of four (has first but no dots)", function() {
|
||||
return checkRender({
|
||||
page: 4,
|
||||
numPages: 4,
|
||||
previous: 3,
|
||||
first: 1,
|
||||
leftdots: false,
|
||||
lowPages: [2, 3],
|
||||
highPages: [],
|
||||
rightdots: false,
|
||||
last: null,
|
||||
next: null
|
||||
});
|
||||
});
|
||||
it("for last page of five (has dots)", function() {
|
||||
return checkRender({
|
||||
page: 5,
|
||||
numPages: 5,
|
||||
previous: 4,
|
||||
first: 1,
|
||||
leftdots: true,
|
||||
lowPages: [3, 4],
|
||||
highPages: [],
|
||||
rightdots: false,
|
||||
last: null,
|
||||
next: null
|
||||
});
|
||||
});
|
||||
it("for middle page of five (max with no first/last)", function() {
|
||||
return checkRender({
|
||||
page: 3,
|
||||
numPages: 5,
|
||||
previous: 2,
|
||||
first: null,
|
||||
leftdots: false,
|
||||
lowPages: [1, 2],
|
||||
highPages: [4, 5],
|
||||
rightdots: false,
|
||||
last: null,
|
||||
next: 4
|
||||
});
|
||||
});
|
||||
it("for middle page of seven (has first/last but no dots)", function() {
|
||||
return checkRender({
|
||||
page: 4,
|
||||
numPages: 7,
|
||||
previous: 3,
|
||||
first: 1,
|
||||
leftdots: false,
|
||||
lowPages: [2, 3],
|
||||
highPages: [5, 6],
|
||||
rightdots: false,
|
||||
last: 7,
|
||||
next: 5
|
||||
});
|
||||
});
|
||||
it("for middle page of nine (has dots)", function() {
|
||||
return checkRender({
|
||||
page: 5,
|
||||
numPages: 9,
|
||||
previous: 4,
|
||||
first: 1,
|
||||
leftdots: true,
|
||||
lowPages: [3, 4],
|
||||
highPages: [6, 7],
|
||||
rightdots: true,
|
||||
last: 9,
|
||||
next: 6
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("pagination interaction", function() {
|
||||
beforeEach(function() {
|
||||
var deferred;
|
||||
this.view = makeView(makeThreads(3), 1, 2);
|
||||
deferred = $.Deferred();
|
||||
return spyOn($, "ajax").and.returnValue(deferred);
|
||||
});
|
||||
it("causes updated rendering", function() {
|
||||
$.ajax.and.callFake(function(params) {
|
||||
params.success({
|
||||
discussion_data: [
|
||||
{
|
||||
id: "on_page_42",
|
||||
body: "dummy body"
|
||||
}
|
||||
],
|
||||
page: 42,
|
||||
num_pages: 99
|
||||
});
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
this.view.$(".discussion-pagination a").first().click();
|
||||
expect(this.view.$(".current-page").text()).toEqual("42");
|
||||
return expect(this.view.$(".last-page").text()).toEqual("99");
|
||||
});
|
||||
it("handles AJAX errors", function() {
|
||||
spyOn(DiscussionUtil, "discussionAlert");
|
||||
$.ajax.and.callFake(function(params) {
|
||||
params.error();
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
this.view.$(".discussion-pagination a").first().click();
|
||||
return expect(DiscussionUtil.discussionAlert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,148 @@
|
||||
/* globals DiscussionUtil */
|
||||
(function() {
|
||||
'use strict';
|
||||
var __indexOf = [].indexOf || function(item) {
|
||||
for (var i = 0, l = this.length; i < l; i++) {
|
||||
if (i in this && this[i] === item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
this.DiscussionViewSpecHelper = (function() {
|
||||
var triggerVoteEvent;
|
||||
|
||||
function DiscussionViewSpecHelper() {
|
||||
}
|
||||
|
||||
DiscussionViewSpecHelper.makeThreadWithProps = function(props) {
|
||||
var thread;
|
||||
thread = {
|
||||
id: "dummy_id",
|
||||
thread_type: "discussion",
|
||||
pinned: false,
|
||||
endorsed: false,
|
||||
votes: {
|
||||
up_count: '0'
|
||||
},
|
||||
read: false,
|
||||
unread_comments_count: 0,
|
||||
comments_count: 0,
|
||||
abuse_flaggers: [],
|
||||
body: "",
|
||||
title: "dummy title",
|
||||
created_at: "2014-08-18T01:02:03Z",
|
||||
ability: {
|
||||
can_delete: false,
|
||||
can_reply: true,
|
||||
can_vote: false,
|
||||
editable: false
|
||||
}
|
||||
};
|
||||
return $.extend(thread, props);
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.checkVoteClasses = function(view) {
|
||||
var action_button, display_button;
|
||||
view.render();
|
||||
display_button = view.$el.find(".display-vote");
|
||||
expect(display_button.hasClass("is-hidden")).toBe(true);
|
||||
action_button = view.$el.find(".action-vote");
|
||||
return expect(action_button).not.toHaveAttr('style', 'display: inline; ');
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.expectVoteRendered = function(view, model, user) {
|
||||
var button;
|
||||
button = view.$el.find(".action-vote");
|
||||
expect(button.hasClass("is-checked")).toBe(user.voted(model));
|
||||
expect(button.attr("aria-checked")).toEqual(user.voted(model).toString());
|
||||
expect(button.find(".vote-count").text()).toMatch("^" + (model.get('votes').up_count) + " Votes?$");
|
||||
return expect(button.find(".sr.js-sr-vote-count").text())
|
||||
.toMatch("^there are currently " + (model.get('votes').up_count) + " votes?$");
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.checkRenderVote = function(view, model) {
|
||||
view.render();
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user);
|
||||
window.user.vote(model);
|
||||
view.render();
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user);
|
||||
window.user.unvote(model);
|
||||
view.render();
|
||||
return DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user);
|
||||
};
|
||||
|
||||
triggerVoteEvent = function(view, event, expectedUrl) {
|
||||
var deferred;
|
||||
deferred = $.Deferred();
|
||||
spyOn($, "ajax").and.callFake(function(params) {
|
||||
expect(params.url.toString()).toEqual(expectedUrl);
|
||||
return deferred;
|
||||
});
|
||||
view.render();
|
||||
view.$el.find(".action-vote").trigger(event);
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
return deferred.resolve();
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.checkUpvote = function(view, model, user, event) {
|
||||
var initialVoteCount, _ref, _ref1;
|
||||
expect((_ref = model.id, __indexOf.call(user.get('upvoted_ids'), _ref) >= 0)).toBe(false);
|
||||
initialVoteCount = model.get('votes').up_count;
|
||||
triggerVoteEvent(view, event, DiscussionUtil.urlFor("upvote_" + (model.get('type')), model.id) + "?ajax=1");
|
||||
expect((_ref1 = model.id, __indexOf.call(user.get('upvoted_ids'), _ref1) >= 0)).toBe(true);
|
||||
return expect(model.get('votes').up_count).toEqual(initialVoteCount + 1);
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.checkUnvote = function(view, model, user, event) {
|
||||
var initialVoteCount, _ref;
|
||||
user.vote(model);
|
||||
expect((_ref = model.id, __indexOf.call(user.get('upvoted_ids'), _ref) >= 0)).toBe(true);
|
||||
initialVoteCount = model.get('votes').up_count;
|
||||
triggerVoteEvent(
|
||||
view, event, DiscussionUtil.urlFor("undo_vote_for_" + (model.get('type')), model.id) + "?ajax=1"
|
||||
);
|
||||
expect(user.get('upvoted_ids')).toEqual([]);
|
||||
return expect(model.get('votes').up_count).toEqual(initialVoteCount - 1);
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.checkButtonEvents = function(view, viewFunc, buttonSelector) {
|
||||
var button, spy;
|
||||
spy = spyOn(view, viewFunc);
|
||||
button = view.$el.find(buttonSelector);
|
||||
button.click();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.calls.reset();
|
||||
button.trigger($.Event("keydown", {
|
||||
which: 13
|
||||
}));
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
spy.calls.reset();
|
||||
button.trigger($.Event("keydown", {
|
||||
which: 32
|
||||
}));
|
||||
return expect(spy).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.checkVoteButtonEvents = function(view) {
|
||||
return this.checkButtonEvents(view, "toggleVote", ".action-vote");
|
||||
};
|
||||
|
||||
DiscussionViewSpecHelper.setNextResponseContent = function(content) {
|
||||
return $.ajax.and.callFake(function(params) {
|
||||
params.success({
|
||||
"content": content
|
||||
});
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return DiscussionViewSpecHelper;
|
||||
|
||||
})();
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,255 @@
|
||||
/* globals Discussion, DiscussionCourseSettings, DiscussionSpecHelper, DiscussionUtil, NewPostView */
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe("NewPostView", function() {
|
||||
var checkVisibility;
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
window.$$course_id = "edX/999/test";
|
||||
spyOn(DiscussionUtil, "makeWmdEditor").and.callFake(function($content, $local, cls_identifier) {
|
||||
return $local("." + cls_identifier).html("<textarea></textarea>");
|
||||
});
|
||||
this.discussion = new Discussion([], {
|
||||
pages: 1
|
||||
});
|
||||
});
|
||||
checkVisibility = function(view, expectedVisible, expectedDisabled, render) {
|
||||
var disabled, group_disabled;
|
||||
if (render) {
|
||||
view.render();
|
||||
}
|
||||
expect(view.$('.group-selector-wrapper').is(":visible") || false).toEqual(expectedVisible);
|
||||
disabled = view.$(".js-group-select").prop("disabled") || false;
|
||||
group_disabled = view.$('.group-selector-wrapper').hasClass('disabled');
|
||||
if (expectedVisible && !expectedDisabled) {
|
||||
expect(disabled).toEqual(false);
|
||||
return expect(group_disabled).toEqual(false);
|
||||
} else if (expectedDisabled) {
|
||||
expect(disabled).toEqual(true);
|
||||
return expect(group_disabled).toEqual(true);
|
||||
}
|
||||
};
|
||||
describe("cohort selector", function() {
|
||||
beforeEach(function() {
|
||||
this.course_settings = new DiscussionCourseSettings({
|
||||
"category_map": {
|
||||
"children": ["Topic", "General"],
|
||||
"entries": {
|
||||
"Topic": {
|
||||
"is_cohorted": true,
|
||||
"id": "topic"
|
||||
},
|
||||
"General": {
|
||||
"is_cohorted": false,
|
||||
"id": "general"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allow_anonymous": false,
|
||||
"allow_anonymous_to_peers": false,
|
||||
"is_cohorted": true,
|
||||
"cohorts": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Cohort1"
|
||||
}, {
|
||||
"id": 2,
|
||||
"name": "Cohort2"
|
||||
}
|
||||
]
|
||||
});
|
||||
this.view = new NewPostView({
|
||||
el: $("#fixture-element"),
|
||||
collection: this.discussion,
|
||||
course_settings: this.course_settings,
|
||||
is_commententable_cohorted: true,
|
||||
mode: "tab"
|
||||
});
|
||||
});
|
||||
it("is not visible to students", function() {
|
||||
return checkVisibility(this.view, false, false, true);
|
||||
});
|
||||
it("allows TAs to see the cohort selector", function() {
|
||||
DiscussionSpecHelper.makeTA();
|
||||
return checkVisibility(this.view, true, false, true);
|
||||
});
|
||||
it("allows moderators to see the cohort selector", function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
return checkVisibility(this.view, true, false, true);
|
||||
});
|
||||
it("only enables the cohort selector when applicable", function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
checkVisibility(this.view, true, false, true);
|
||||
$('.topic-title:contains(General)').click();
|
||||
checkVisibility(this.view, true, true, false);
|
||||
$('.topic-title:contains(Topic)').click();
|
||||
return checkVisibility(this.view, true, false, false);
|
||||
});
|
||||
it("allows the user to make a cohort selection", function() {
|
||||
var expectedGroupId,
|
||||
self = this;
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.render();
|
||||
expectedGroupId = null;
|
||||
DiscussionSpecHelper.makeAjaxSpy(function(params) {
|
||||
return expect(params.data.group_id).toEqual(expectedGroupId);
|
||||
});
|
||||
return _.each(["1", "2", ""], function(groupIdStr) {
|
||||
expectedGroupId = groupIdStr;
|
||||
self.view.$(".js-group-select").val(groupIdStr);
|
||||
self.view.$(".js-post-title").val("dummy title");
|
||||
self.view.$(".js-post-body textarea").val("dummy body");
|
||||
self.view.$(".forum-new-post-form").submit();
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
return $.ajax.calls.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("always cohort inline discussions ", function() {
|
||||
beforeEach(function() {
|
||||
this.course_settings = new DiscussionCourseSettings({
|
||||
"category_map": {
|
||||
"children": [],
|
||||
"entries": {}
|
||||
},
|
||||
"allow_anonymous": false,
|
||||
"allow_anonymous_to_peers": false,
|
||||
"is_cohorted": true,
|
||||
"cohorts": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Cohort1"
|
||||
}, {
|
||||
"id": 2,
|
||||
"name": "Cohort2"
|
||||
}
|
||||
]
|
||||
});
|
||||
this.view = new NewPostView({
|
||||
el: $("#fixture-element"),
|
||||
collection: this.discussion,
|
||||
course_settings: this.course_settings,
|
||||
mode: "tab"
|
||||
});
|
||||
});
|
||||
it("disables the cohort menu if it is set false", function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.is_commentable_cohorted = false;
|
||||
return checkVisibility(this.view, true, true, true);
|
||||
});
|
||||
it("enables the cohort menu if it is set true", function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.is_commentable_cohorted = true;
|
||||
return checkVisibility(this.view, true, false, true);
|
||||
});
|
||||
it("is not visible to students when set false", function() {
|
||||
this.view.is_commentable_cohorted = false;
|
||||
return checkVisibility(this.view, false, false, true);
|
||||
});
|
||||
it("is not visible to students when set true", function() {
|
||||
this.view.is_commentable_cohorted = true;
|
||||
return checkVisibility(this.view, false, false, true);
|
||||
});
|
||||
});
|
||||
describe("cancel post resets form ", function() {
|
||||
var checkPostCancelReset;
|
||||
beforeEach(function() {
|
||||
this.course_settings = new DiscussionCourseSettings({
|
||||
"allow_anonymous_to_peers": true,
|
||||
"allow_anonymous": true,
|
||||
"category_map": {
|
||||
"subcategories": {
|
||||
"Week 1": {
|
||||
"subcategories": {},
|
||||
"children": ["Topic-Level Student-Visible Label"],
|
||||
"entries": {
|
||||
"Topic-Level Student-Visible Label": {
|
||||
"sort_key": null,
|
||||
"is_cohorted": false,
|
||||
"id": "2b3a858d0c884eb4b272dbbe3f2ffddd"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": ["General", "Week 1"],
|
||||
"entries": {
|
||||
"General": {
|
||||
"sort_key": "General",
|
||||
"is_cohorted": false,
|
||||
"id": "i4x-waqastest-waqastest-course-waqastest"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
checkPostCancelReset = function(mode, discussion, course_settings) {
|
||||
var eventSpy, view;
|
||||
view = new NewPostView({
|
||||
el: $("#fixture-element"),
|
||||
collection: discussion,
|
||||
course_settings: course_settings,
|
||||
mode: mode
|
||||
});
|
||||
view.render();
|
||||
eventSpy = jasmine.createSpy('eventSpy');
|
||||
view.listenTo(view, "newPost:cancel", eventSpy);
|
||||
view.$(".post-errors").html("<li class='post-error'>Title can't be empty</li>");
|
||||
view.$("label[for$='post-type-question']").click();
|
||||
view.$(".js-post-title").val("Test Title");
|
||||
view.$(".js-post-body textarea").val("Test body");
|
||||
view.$(".wmd-preview p").html("Test body");
|
||||
view.$(".js-follow").prop("checked", false);
|
||||
view.$(".js-anon").prop("checked", true);
|
||||
view.$(".js-anon-peers").prop("checked", true);
|
||||
if (mode === "tab") {
|
||||
view.$("a[data-discussion-id='2b3a858d0c884eb4b272dbbe3f2ffddd']").click();
|
||||
}
|
||||
view.$(".cancel").click();
|
||||
expect(eventSpy).toHaveBeenCalled();
|
||||
expect(view.$(".post-errors").html()).toEqual("");
|
||||
expect($("input[id$='post-type-discussion']")).toBeChecked();
|
||||
expect($("input[id$='post-type-question']")).not.toBeChecked();
|
||||
expect(view.$(".js-post-title").val()).toEqual("");
|
||||
expect(view.$(".js-post-body textarea").val()).toEqual("");
|
||||
expect(view.$(".js-follow")).toBeChecked();
|
||||
expect(view.$(".js-anon")).not.toBeChecked();
|
||||
expect(view.$(".js-anon-peers")).not.toBeChecked();
|
||||
if (mode === "tab") {
|
||||
return expect(view.$(".js-selected-topic").text()).toEqual("General");
|
||||
}
|
||||
};
|
||||
return _.each(["tab", "inline"], function(mode) {
|
||||
it("resets the form in " + mode + " mode", function() {
|
||||
return checkPostCancelReset(mode, this.discussion, this.course_settings);
|
||||
});
|
||||
});
|
||||
});
|
||||
it("posts to the correct URL", function() {
|
||||
var topicId, view;
|
||||
topicId = "test_topic";
|
||||
spyOn($, "ajax").and.callFake(function(params) {
|
||||
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("create_thread", topicId));
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
view = new NewPostView({
|
||||
el: $("#fixture-element"),
|
||||
collection: this.discussion,
|
||||
course_settings: new DiscussionCourseSettings({
|
||||
allow_anonymous: false,
|
||||
allow_anonymous_to_peers: false
|
||||
}),
|
||||
mode: "inline",
|
||||
topicId: topicId
|
||||
});
|
||||
view.render();
|
||||
view.$(".forum-new-post-form").submit();
|
||||
return expect($.ajax).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,121 @@
|
||||
/* globals DiscussionSpecHelper, DiscussionUtil, DiscussionViewSpecHelper, ResponseCommentShowView */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe('ResponseCommentShowView', function() {
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.comment = new Comment({
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
body: 'this is a response',
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
abuse_flaggers: ['123'],
|
||||
roles: []
|
||||
});
|
||||
this.view = new ResponseCommentShowView({
|
||||
model: this.comment
|
||||
});
|
||||
return spyOn(this.view, "convertMath");
|
||||
});
|
||||
it('defines the tag', function() {
|
||||
expect($('#jasmine-fixtures')).toExist();
|
||||
expect(this.view.tagName).toBeDefined();
|
||||
return expect(this.view.el.tagName.toLowerCase()).toBe('li');
|
||||
});
|
||||
it('is tied to the model', function() {
|
||||
return expect(this.view.model).toBeDefined();
|
||||
});
|
||||
describe('rendering', function() {
|
||||
beforeEach(function() {
|
||||
return spyOn(this.view, 'renderAttrs');
|
||||
});
|
||||
it('can be flagged for abuse', function() {
|
||||
this.comment.flagAbuse();
|
||||
return expect(this.comment.get('abuse_flaggers')).toEqual(['123', '567']);
|
||||
});
|
||||
it('can be unflagged for abuse', function() {
|
||||
var temp_array;
|
||||
temp_array = [];
|
||||
temp_array.push(window.user.get('id'));
|
||||
this.comment.set("abuse_flaggers", temp_array);
|
||||
this.comment.unflagAbuse();
|
||||
return expect(this.comment.get('abuse_flaggers')).toEqual([]);
|
||||
});
|
||||
});
|
||||
describe('_delete', function() {
|
||||
it('triggers on the correct events', function() {
|
||||
DiscussionUtil.loadRoles([]);
|
||||
this.comment.updateInfo({
|
||||
ability: {
|
||||
'can_delete': true
|
||||
}
|
||||
});
|
||||
this.view.render();
|
||||
return DiscussionViewSpecHelper.checkButtonEvents(this.view, "_delete", ".action-delete");
|
||||
});
|
||||
it('triggers the delete event', function() {
|
||||
var triggerTarget;
|
||||
triggerTarget = jasmine.createSpy();
|
||||
this.view.bind("comment:_delete", triggerTarget);
|
||||
this.view._delete();
|
||||
return expect(triggerTarget).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('edit', function() {
|
||||
it('triggers on the correct events', function() {
|
||||
DiscussionUtil.loadRoles([]);
|
||||
this.comment.updateInfo({
|
||||
ability: {
|
||||
'can_edit': true
|
||||
}
|
||||
});
|
||||
this.view.render();
|
||||
return DiscussionViewSpecHelper.checkButtonEvents(this.view, "edit", ".action-edit");
|
||||
});
|
||||
it('triggers comment:edit when the edit button is clicked', function() {
|
||||
var triggerTarget;
|
||||
triggerTarget = jasmine.createSpy();
|
||||
this.view.bind("comment:edit", triggerTarget);
|
||||
this.view.edit();
|
||||
return expect(triggerTarget).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("labels", function() {
|
||||
var expectOneElement;
|
||||
expectOneElement = function(view, selector, visible) {
|
||||
var elements;
|
||||
if (typeof visible === "undefined" || visible === null) {
|
||||
visible = true;
|
||||
}
|
||||
view.render();
|
||||
elements = view.$el.find(selector);
|
||||
expect(elements.length).toEqual(1);
|
||||
if (visible) {
|
||||
return expect(elements).not.toHaveClass("is-hidden");
|
||||
} else {
|
||||
return expect(elements).toHaveClass("is-hidden");
|
||||
}
|
||||
};
|
||||
it('displays the reported label when appropriate for a non-staff user', function() {
|
||||
this.comment.set('abuse_flaggers', []);
|
||||
expectOneElement(this.view, '.post-label-reported', false);
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id]);
|
||||
expectOneElement(this.view, '.post-label-reported');
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]);
|
||||
return expectOneElement(this.view, '.post-label-reported', false);
|
||||
});
|
||||
it('displays the reported label when appropriate for a flag moderator', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.comment.set('abuse_flaggers', []);
|
||||
expectOneElement(this.view, '.post-label-reported', false);
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id]);
|
||||
expectOneElement(this.view, '.post-label-reported');
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]);
|
||||
return expectOneElement(this.view, '.post-label-reported');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,195 @@
|
||||
/* globals DiscussionSpecHelper, DiscussionUtil, ResponseCommentView, ResponseCommentShowView, user */
|
||||
(function() {
|
||||
'use strict';
|
||||
var $$course_id = "$$course_id";
|
||||
|
||||
describe('ResponseCommentView', function() {
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
this.comment = new Comment({
|
||||
id: '01234567',
|
||||
user_id: user.id,
|
||||
course_id: $$course_id,
|
||||
body: 'this is a response',
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
abuse_flaggers: ['123'],
|
||||
roles: ['Student']
|
||||
});
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.view = new ResponseCommentView({
|
||||
model: this.comment,
|
||||
el: $("#fixture-element")
|
||||
});
|
||||
spyOn(ResponseCommentShowView.prototype, "convertMath");
|
||||
spyOn(DiscussionUtil, "makeWmdEditor");
|
||||
return this.view.render();
|
||||
});
|
||||
describe('_delete', function() {
|
||||
var setAjaxResult;
|
||||
beforeEach(function() {
|
||||
this.comment.updateInfo({
|
||||
ability: {
|
||||
can_delete: true
|
||||
}
|
||||
});
|
||||
this.event = DiscussionSpecHelper.makeEventSpy();
|
||||
spyOn(this.comment, "remove");
|
||||
return spyOn(this.view.$el, "remove");
|
||||
});
|
||||
setAjaxResult = function(isSuccess) {
|
||||
return spyOn($, "ajax").and.callFake(function(params) {
|
||||
(isSuccess ? params.success : params.error)({});
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
it('requires confirmation before deleting', function() {
|
||||
spyOn(window, "confirm").and.returnValue(false);
|
||||
setAjaxResult(true);
|
||||
this.view._delete(this.event);
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect($.ajax).not.toHaveBeenCalled();
|
||||
return expect(this.comment.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
it('removes the deleted comment object', function() {
|
||||
setAjaxResult(true);
|
||||
this.view._delete(this.event);
|
||||
expect(this.comment.remove).toHaveBeenCalled();
|
||||
return expect(this.view.$el.remove).toHaveBeenCalled();
|
||||
});
|
||||
it('calls the ajax comment deletion endpoint', function() {
|
||||
setAjaxResult(true);
|
||||
this.view._delete(this.event);
|
||||
expect(this.event.preventDefault).toHaveBeenCalled();
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
return expect($.ajax.calls.mostRecent().args[0].url._parts.path)
|
||||
.toEqual('/courses/edX/999/test/discussion/comments/01234567/delete');
|
||||
});
|
||||
it('handles ajax errors', function() {
|
||||
spyOn(DiscussionUtil, "discussionAlert");
|
||||
setAjaxResult(false);
|
||||
this.view._delete(this.event);
|
||||
expect(this.event.preventDefault).toHaveBeenCalled();
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
expect(this.comment.remove).not.toHaveBeenCalled();
|
||||
expect(this.view.$el.remove).not.toHaveBeenCalled();
|
||||
return expect(DiscussionUtil.discussionAlert).toHaveBeenCalled();
|
||||
});
|
||||
it('does not delete a comment if the permission is false', function() {
|
||||
this.comment.updateInfo({
|
||||
ability: {
|
||||
'can_delete': false
|
||||
}
|
||||
});
|
||||
spyOn(window, "confirm");
|
||||
setAjaxResult(true);
|
||||
this.view._delete(this.event);
|
||||
expect(window.confirm).not.toHaveBeenCalled();
|
||||
expect($.ajax).not.toHaveBeenCalled();
|
||||
expect(this.comment.remove).not.toHaveBeenCalled();
|
||||
return expect(this.view.$el.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('renderShowView', function() {
|
||||
it('renders the show view, removes the edit view, and registers event handlers', function() {
|
||||
spyOn(this.view, "_delete");
|
||||
spyOn(this.view, "edit");
|
||||
this.view.renderEditView();
|
||||
this.view.renderShowView();
|
||||
this.view.showView.trigger("comment:_delete", DiscussionSpecHelper.makeEventSpy());
|
||||
expect(this.view._delete).toHaveBeenCalled();
|
||||
this.view.showView.trigger("comment:edit", DiscussionSpecHelper.makeEventSpy());
|
||||
expect(this.view.edit).toHaveBeenCalled();
|
||||
return expect(this.view.$(".edit-post-form#comment_" + this.comment.id))
|
||||
.not.toHaveClass("edit-post-form");
|
||||
});
|
||||
});
|
||||
describe('renderEditView', function() {
|
||||
it('renders the edit view, removes the show view, and registers event handlers', function() {
|
||||
spyOn(this.view, "update");
|
||||
spyOn(this.view, "cancelEdit");
|
||||
this.view.renderEditView();
|
||||
this.view.editView.trigger("comment:update", DiscussionSpecHelper.makeEventSpy());
|
||||
expect(this.view.update).toHaveBeenCalled();
|
||||
this.view.editView.trigger("comment:cancel_edit", DiscussionSpecHelper.makeEventSpy());
|
||||
expect(this.view.cancelEdit).toHaveBeenCalled();
|
||||
return expect(this.view.$(".edit-post-form#comment_" + this.comment.id)).toHaveClass("edit-post-form");
|
||||
});
|
||||
});
|
||||
describe('edit', function() {
|
||||
it('triggers the appropriate event and switches to the edit view', function() {
|
||||
var editTarget;
|
||||
spyOn(this.view, 'renderEditView');
|
||||
editTarget = jasmine.createSpy();
|
||||
this.view.bind("comment:edit", editTarget);
|
||||
this.view.edit();
|
||||
expect(this.view.renderEditView).toHaveBeenCalled();
|
||||
return expect(editTarget).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('with edit view displayed', function() {
|
||||
beforeEach(function() {
|
||||
return this.view.renderEditView();
|
||||
});
|
||||
describe('cancelEdit', function() {
|
||||
it('triggers the appropriate event and switches to the show view', function() {
|
||||
var cancelEditTarget;
|
||||
spyOn(this.view, 'renderShowView');
|
||||
cancelEditTarget = jasmine.createSpy();
|
||||
this.view.bind("comment:cancel_edit", cancelEditTarget);
|
||||
this.view.cancelEdit();
|
||||
expect(this.view.renderShowView).toHaveBeenCalled();
|
||||
return expect(cancelEditTarget).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('update', function() {
|
||||
beforeEach(function() {
|
||||
var self = this;
|
||||
this.updatedBody = "updated body";
|
||||
this.view.$el.find(".edit-comment-body").html($("<textarea></textarea>"));
|
||||
this.view.$el.find(".edit-comment-body textarea").val(this.updatedBody);
|
||||
spyOn(this.view, 'cancelEdit');
|
||||
return spyOn($, "ajax").and.callFake(function(params) {
|
||||
if (self.ajaxSucceed) {
|
||||
params.success();
|
||||
} else {
|
||||
params.error({
|
||||
status: 500
|
||||
});
|
||||
}
|
||||
return {
|
||||
always: function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
it('calls the update endpoint correctly and displays the show view on success', function() {
|
||||
this.ajaxSucceed = true;
|
||||
this.view.update(DiscussionSpecHelper.makeEventSpy());
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
expect($.ajax.calls.mostRecent().args[0].url._parts.path)
|
||||
.toEqual('/courses/edX/999/test/discussion/comments/01234567/update');
|
||||
expect($.ajax.calls.mostRecent().args[0].data.body).toEqual(this.updatedBody);
|
||||
expect(this.view.model.get("body")).toEqual(this.updatedBody);
|
||||
return expect(this.view.cancelEdit).toHaveBeenCalled();
|
||||
});
|
||||
it('handles AJAX errors', function() {
|
||||
var originalBody;
|
||||
originalBody = this.comment.get("body");
|
||||
this.ajaxSucceed = false;
|
||||
this.view.update(DiscussionSpecHelper.makeEventSpy());
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
expect($.ajax.calls.mostRecent().args[0].url._parts.path)
|
||||
.toEqual('/courses/edX/999/test/discussion/comments/01234567/update');
|
||||
expect($.ajax.calls.mostRecent().args[0].data.body).toEqual(this.updatedBody);
|
||||
expect(this.view.model.get("body")).toEqual(originalBody);
|
||||
expect(this.view.cancelEdit).not.toHaveBeenCalled();
|
||||
return expect(this.view.$(".edit-comment-form-errors *").length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,262 @@
|
||||
/* globals DiscussionViewSpecHelper, DiscussionSpecHelper, DiscussionUtil, Thread, ThreadResponseShowView */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe("ThreadResponseShowView", function() {
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.user = DiscussionUtil.getUser();
|
||||
this.thread = new Thread({
|
||||
"thread_type": "discussion"
|
||||
});
|
||||
this.commentData = {
|
||||
id: "dummy",
|
||||
user_id: "567",
|
||||
course_id: "TestOrg/TestCourse/TestRun",
|
||||
body: "this is a comment",
|
||||
created_at: "2013-04-03T20:08:39Z",
|
||||
endorsed: false,
|
||||
abuse_flaggers: [],
|
||||
votes: {
|
||||
up_count: 42
|
||||
},
|
||||
type: "comment"
|
||||
};
|
||||
this.comment = new Comment(this.commentData);
|
||||
this.comment.set("thread", this.thread);
|
||||
this.view = new ThreadResponseShowView({
|
||||
model: this.comment,
|
||||
$el: $("#fixture-element")
|
||||
});
|
||||
spyOn(ThreadResponseShowView.prototype, "convertMath");
|
||||
return this.view.render();
|
||||
});
|
||||
describe("voting", function() {
|
||||
it("renders the vote state correctly", function() {
|
||||
return DiscussionViewSpecHelper.checkRenderVote(this.view, this.comment);
|
||||
});
|
||||
it("check the vote classes after renders", function() {
|
||||
return DiscussionViewSpecHelper.checkVoteClasses(this.view);
|
||||
});
|
||||
it("votes correctly via click", function() {
|
||||
return DiscussionViewSpecHelper.checkUpvote(this.view, this.comment, this.user, $.Event("click"));
|
||||
});
|
||||
it("votes correctly via spacebar", function() {
|
||||
return DiscussionViewSpecHelper.checkUpvote(this.view, this.comment, this.user, $.Event("keydown", {
|
||||
which: 32
|
||||
}));
|
||||
});
|
||||
it("unvotes correctly via click", function() {
|
||||
return DiscussionViewSpecHelper.checkUnvote(this.view, this.comment, this.user, $.Event("click"));
|
||||
});
|
||||
it("unvotes correctly via spacebar", function() {
|
||||
return DiscussionViewSpecHelper.checkUnvote(this.view, this.comment, this.user, $.Event("keydown", {
|
||||
which: 32
|
||||
}));
|
||||
});
|
||||
});
|
||||
it("renders endorsement correctly for a marked answer in a question thread", function() {
|
||||
var endorsement;
|
||||
endorsement = {
|
||||
"username": "test_endorser",
|
||||
"user_id": "test_id",
|
||||
"time": new Date().toISOString()
|
||||
};
|
||||
this.thread.set("thread_type", "question");
|
||||
this.comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$(".posted-details").text().replace(/\s+/g, " "))
|
||||
.toMatch("marked as answer less than a minute ago by " + endorsement.username);
|
||||
return expect(this.view.$(".posted-details > a").attr('href'))
|
||||
.toEqual("/courses/edX/999/test/discussion/forum/users/test_id");
|
||||
});
|
||||
it("renders anonymous endorsement correctly for a marked answer in a question thread", function() {
|
||||
var endorsement;
|
||||
endorsement = {
|
||||
"username": null,
|
||||
"time": new Date().toISOString()
|
||||
};
|
||||
this.thread.set("thread_type", "question");
|
||||
this.comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$(".posted-details").text()).toMatch("marked as answer less than a minute ago");
|
||||
return expect(this.view.$(".posted-details").text()).not.toMatch("\sby\s");
|
||||
});
|
||||
it("renders endorsement correctly for an endorsed response in a discussion thread", function() {
|
||||
var endorsement;
|
||||
endorsement = {
|
||||
"username": "test_endorser",
|
||||
"user_id": "test_id",
|
||||
"time": new Date().toISOString()
|
||||
};
|
||||
this.thread.set("thread_type", "discussion");
|
||||
this.comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$(".posted-details").text().replace(/\s+/g, " "))
|
||||
.toMatch("endorsed less than a minute ago by " + endorsement.username);
|
||||
return expect(this.view.$(".posted-details > a").attr('href'))
|
||||
.toEqual("/courses/edX/999/test/discussion/forum/users/test_id");
|
||||
});
|
||||
it("renders anonymous endorsement correctly for an endorsed response in a discussion thread", function() {
|
||||
var endorsement;
|
||||
endorsement = {
|
||||
"username": null,
|
||||
"time": new Date().toISOString()
|
||||
};
|
||||
this.thread.set("thread_type", "discussion");
|
||||
this.comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$(".posted-details").text()).toMatch("endorsed less than a minute ago");
|
||||
return expect(this.view.$(".posted-details").text()).not.toMatch("\sby\s");
|
||||
});
|
||||
it("re-renders correctly when endorsement changes", function() {
|
||||
spyOn($, "ajax").and.returnValue($.Deferred());
|
||||
DiscussionUtil.loadRoles({
|
||||
"Moderator": [parseInt(window.user.id)]
|
||||
});
|
||||
this.thread.set("thread_type", "question");
|
||||
this.view.render();
|
||||
expect(this.view.$(".posted-details").text()).not.toMatch("marked as answer");
|
||||
this.view.$(".action-answer").click();
|
||||
expect(this.view.$(".posted-details").text()).toMatch("marked as answer");
|
||||
this.view.$(".action-answer").click();
|
||||
return expect(this.view.$(".posted-details").text()).not.toMatch("marked as answer");
|
||||
});
|
||||
it("allows a moderator to mark an answer in a question thread", function() {
|
||||
var endorseButton;
|
||||
spyOn($, "ajax").and.returnValue($.Deferred());
|
||||
DiscussionUtil.loadRoles({
|
||||
"Moderator": [parseInt(window.user.id)]
|
||||
});
|
||||
this.thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": (parseInt(window.user.id) + 1).toString()
|
||||
});
|
||||
this.view.render();
|
||||
endorseButton = this.view.$(".action-answer");
|
||||
expect(endorseButton.length).toEqual(1);
|
||||
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden");
|
||||
endorseButton.click();
|
||||
return expect(endorseButton).toHaveClass("is-checked");
|
||||
});
|
||||
it("allows the author of a question thread to mark an answer", function() {
|
||||
var endorseButton;
|
||||
spyOn($, "ajax").and.returnValue($.Deferred());
|
||||
this.thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": window.user.id
|
||||
});
|
||||
this.view.render();
|
||||
endorseButton = this.view.$(".action-answer");
|
||||
expect(endorseButton.length).toEqual(1);
|
||||
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden");
|
||||
endorseButton.click();
|
||||
return expect(endorseButton).toHaveClass("is-checked");
|
||||
});
|
||||
it("does not allow the author of a discussion thread to endorse", function() {
|
||||
var endorseButton;
|
||||
this.thread.set({
|
||||
"thread_type": "discussion",
|
||||
"user_id": window.user.id
|
||||
});
|
||||
this.view.render();
|
||||
endorseButton = this.view.$(".action-endorse");
|
||||
expect(endorseButton.length).toEqual(1);
|
||||
return expect(endorseButton.closest(".actions-item")).toHaveClass("is-hidden");
|
||||
});
|
||||
it("does not allow a student who is not the author of a question thread to mark an answer", function() {
|
||||
var endorseButton;
|
||||
this.thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": (parseInt(window.user.id) + 1).toString()
|
||||
});
|
||||
this.view.render();
|
||||
endorseButton = this.view.$(".action-answer");
|
||||
expect(endorseButton.length).toEqual(1);
|
||||
return expect(endorseButton.closest(".actions-item")).toHaveClass("is-hidden");
|
||||
});
|
||||
describe("labels", function() {
|
||||
var expectOneElement;
|
||||
expectOneElement = function(view, selector, visible) {
|
||||
var elements;
|
||||
if (typeof visible === "undefined" || visible === null) {
|
||||
visible = true;
|
||||
}
|
||||
view.render();
|
||||
elements = view.$el.find(selector);
|
||||
expect(elements.length).toEqual(1);
|
||||
if (visible) {
|
||||
return expect(elements).not.toHaveClass("is-hidden");
|
||||
} else {
|
||||
return expect(elements).toHaveClass("is-hidden");
|
||||
}
|
||||
};
|
||||
it('displays the reported label when appropriate for a non-staff user', function() {
|
||||
expectOneElement(this.view, '.post-label-reported', false);
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id]);
|
||||
expectOneElement(this.view, '.post-label-reported');
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]);
|
||||
return expectOneElement(this.view, '.post-label-reported', false);
|
||||
});
|
||||
it('displays the reported label when appropriate for a flag moderator', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
expectOneElement(this.view, '.post-label-reported', false);
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id]);
|
||||
expectOneElement(this.view, '.post-label-reported');
|
||||
this.comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]);
|
||||
return expectOneElement(this.view, '.post-label-reported');
|
||||
});
|
||||
});
|
||||
describe("endorser display", function() {
|
||||
var checkUserLink;
|
||||
beforeEach(function() {
|
||||
this.comment.set('endorsement', {
|
||||
"username": "test_endorser",
|
||||
"time": new Date().toISOString()
|
||||
});
|
||||
return spyOn(DiscussionUtil, 'urlFor').and.returnValue('test_endorser_url');
|
||||
});
|
||||
checkUserLink = function(element, is_ta, is_staff) {
|
||||
expect(element.find('a.username').length).toEqual(1);
|
||||
expect(element.find('a.username').text()).toEqual('test_endorser');
|
||||
expect(element.find('a.username').attr('href')).toEqual('test_endorser_url');
|
||||
expect(element.find('.user-label-community-ta').length).toEqual(is_ta ? 1 : 0);
|
||||
return expect(element.find('.user-label-staff').length).toEqual(is_staff ? 1 : 0);
|
||||
};
|
||||
it("renders nothing when the response has not been endorsed", function() {
|
||||
this.comment.set('endorsement', null);
|
||||
return expect(this.view.getEndorserDisplay()).toBeNull();
|
||||
});
|
||||
it("renders correctly for a student-endorsed response", function() {
|
||||
var $el;
|
||||
$el = $('#fixture-element').html(this.view.getEndorserDisplay());
|
||||
return checkUserLink($el, false, false);
|
||||
});
|
||||
it("renders correctly for a community TA-endorsed response", function() {
|
||||
var $el;
|
||||
spyOn(DiscussionUtil, 'isTA').and.returnValue(true);
|
||||
$el = $('#fixture-element').html(this.view.getEndorserDisplay());
|
||||
return checkUserLink($el, true, false);
|
||||
});
|
||||
it("renders correctly for a staff-endorsed response", function() {
|
||||
var $el;
|
||||
spyOn(DiscussionUtil, 'isStaff').and.returnValue(true);
|
||||
$el = $('#fixture-element').html(this.view.getEndorserDisplay());
|
||||
return checkUserLink($el, false, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -0,0 +1,123 @@
|
||||
/* globals DiscussionSpecHelper, ResponseCommentView, Thread, ThreadResponseView, ThreadResponseShowView */
|
||||
(function() {
|
||||
'use strict';
|
||||
describe('ThreadResponseView', function() {
|
||||
beforeEach(function() {
|
||||
DiscussionSpecHelper.setUpGlobals();
|
||||
DiscussionSpecHelper.setUnderscoreFixtures();
|
||||
this.thread = new Thread({
|
||||
"thread_type": "discussion"
|
||||
});
|
||||
this.response = new Comment({
|
||||
children: [{}, {}],
|
||||
thread: this.thread
|
||||
});
|
||||
this.view = new ThreadResponseView({
|
||||
model: this.response,
|
||||
el: $("#fixture-element")
|
||||
});
|
||||
spyOn(ThreadResponseShowView.prototype, "render");
|
||||
return spyOn(ResponseCommentView.prototype, "render");
|
||||
});
|
||||
describe('closed and open Threads', function() {
|
||||
var checkCommentForm;
|
||||
checkCommentForm = function(closed) {
|
||||
var comment, commentData, thread, view;
|
||||
thread = new Thread({
|
||||
"thread_type": "discussion",
|
||||
"closed": closed
|
||||
});
|
||||
commentData = {
|
||||
id: "dummy",
|
||||
user_id: "567",
|
||||
course_id: "TestOrg/TestCourse/TestRun",
|
||||
body: "this is a comment",
|
||||
created_at: "2013-04-03T20:08:39Z",
|
||||
abuse_flaggers: [],
|
||||
type: "comment",
|
||||
children: [],
|
||||
thread: thread
|
||||
};
|
||||
comment = new Comment(commentData);
|
||||
view = new ThreadResponseView({
|
||||
model: comment,
|
||||
el: $("#fixture-element")
|
||||
});
|
||||
view.render();
|
||||
return expect(view.$('.comment-form').closest('li').is(":visible")).toBe(!closed);
|
||||
};
|
||||
it('hides comment form when thread is closed', function() {
|
||||
return checkCommentForm(true);
|
||||
});
|
||||
it('show comment form when thread is open', function() {
|
||||
return checkCommentForm(false);
|
||||
});
|
||||
});
|
||||
describe('renderComments', function() {
|
||||
it('hides "show comments" link if collapseComments is not set', function() {
|
||||
this.view.render();
|
||||
expect(this.view.$(".comments")).toBeVisible();
|
||||
return expect(this.view.$(".action-show-comments")).not.toBeVisible();
|
||||
});
|
||||
it('hides "show comments" link if collapseComments is set but response has no comments', function() {
|
||||
this.response = new Comment({
|
||||
children: [],
|
||||
thread: this.thread
|
||||
});
|
||||
this.view = new ThreadResponseView({
|
||||
model: this.response,
|
||||
el: $("#fixture-element"),
|
||||
collapseComments: true
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$(".comments")).toBeVisible();
|
||||
return expect(this.view.$(".action-show-comments")).not.toBeVisible();
|
||||
});
|
||||
it(
|
||||
'hides comments if collapseComments is set and shows them when "show comments" link is clicked',
|
||||
function() {
|
||||
this.view = new ThreadResponseView({
|
||||
model: this.response,
|
||||
el: $("#fixture-element"),
|
||||
collapseComments: true
|
||||
});
|
||||
this.view.render();
|
||||
expect(this.view.$(".comments")).not.toBeVisible();
|
||||
expect(this.view.$(".action-show-comments")).toBeVisible();
|
||||
this.view.$(".action-show-comments").click();
|
||||
expect(this.view.$(".comments")).toBeVisible();
|
||||
return expect(this.view.$(".action-show-comments")).not.toBeVisible();
|
||||
}
|
||||
);
|
||||
it('populates commentViews and binds events', function() {
|
||||
this.view.createEditView();
|
||||
spyOn(this.view, 'cancelEdit');
|
||||
spyOn(this.view, 'cancelCommentEdits');
|
||||
spyOn(this.view, 'hideCommentForm');
|
||||
spyOn(this.view, 'showCommentForm');
|
||||
this.view.renderComments();
|
||||
expect(this.view.commentViews.length).toEqual(2);
|
||||
this.view.commentViews[0].trigger("comment:edit", jasmine.createSpyObj("event", ["preventDefault"]));
|
||||
expect(this.view.cancelEdit).toHaveBeenCalled();
|
||||
expect(this.view.cancelCommentEdits).toHaveBeenCalled();
|
||||
expect(this.view.hideCommentForm).toHaveBeenCalled();
|
||||
this.view.commentViews[0].trigger("comment:cancel_edit");
|
||||
return expect(this.view.showCommentForm).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('cancelCommentEdits', function() {
|
||||
it('calls cancelEdit on each comment view', function() {
|
||||
this.view.renderComments();
|
||||
expect(this.view.commentViews.length).toEqual(2);
|
||||
_.each(this.view.commentViews, function(commentView) {
|
||||
return spyOn(commentView, 'cancelEdit');
|
||||
});
|
||||
this.view.cancelCommentEdits();
|
||||
return _.each(this.view.commentViews, function(commentView) {
|
||||
return expect(commentView.cancelEdit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
113
common/static/common/js/spec_helpers/discussion_spec_helper.js
Normal file
113
common/static/common/js/spec_helpers/discussion_spec_helper.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/* global DiscussionCourseSettings, DiscussionUtil, DiscussionUser */
|
||||
(function () {
|
||||
'use strict';
|
||||
this.DiscussionSpecHelper = (function () {
|
||||
|
||||
function DiscussionSpecHelper() {
|
||||
}
|
||||
|
||||
DiscussionSpecHelper.setUpGlobals = function () {
|
||||
DiscussionUtil.loadRoles({
|
||||
"Moderator": [],
|
||||
"Administrator": [],
|
||||
"Community TA": []
|
||||
});
|
||||
window.$$course_id = "edX/999/test";
|
||||
window.user = new DiscussionUser({
|
||||
username: "test_user",
|
||||
id: "567",
|
||||
upvoted_ids: []
|
||||
});
|
||||
return DiscussionUtil.setUser(window.user);
|
||||
};
|
||||
|
||||
DiscussionSpecHelper.makeTA = function () {
|
||||
return DiscussionUtil.roleIds["Community TA"].push(parseInt(DiscussionUtil.getUser().id));
|
||||
};
|
||||
|
||||
DiscussionSpecHelper.makeModerator = function () {
|
||||
return DiscussionUtil.roleIds.Moderator.push(parseInt(DiscussionUtil.getUser().id));
|
||||
};
|
||||
|
||||
DiscussionSpecHelper.makeAjaxSpy = function (fakeAjax) {
|
||||
return spyOn($, "ajax").and.callFake(function (params) {
|
||||
fakeAjax(params);
|
||||
return {
|
||||
always: function () {
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionSpecHelper.makeEventSpy = function () {
|
||||
return jasmine.createSpyObj('event', ['preventDefault', 'target']);
|
||||
};
|
||||
|
||||
DiscussionSpecHelper.makeCourseSettings = function (is_cohorted) {
|
||||
if (typeof is_cohorted === 'undefined' || is_cohorted === null) {
|
||||
is_cohorted = true;
|
||||
}
|
||||
return new DiscussionCourseSettings({
|
||||
category_map: {
|
||||
children: ['Test Topic', 'Other Topic'],
|
||||
entries: {
|
||||
'Test Topic': {
|
||||
is_cohorted: is_cohorted,
|
||||
id: 'test_topic'
|
||||
},
|
||||
'Other Topic': {
|
||||
is_cohorted: is_cohorted,
|
||||
id: 'other_topic'
|
||||
}
|
||||
}
|
||||
},
|
||||
is_cohorted: is_cohorted
|
||||
});
|
||||
};
|
||||
|
||||
DiscussionSpecHelper.setUnderscoreFixtures = function () {
|
||||
var templateFixture, templateName, templateNames, templateNamesNoTrailingTemplate, _i, _j, _len, _len1;
|
||||
templateNames = [
|
||||
'thread', 'thread-show', 'thread-edit', 'thread-response', 'thread-response-show',
|
||||
'thread-response-edit', 'response-comment-show', 'response-comment-edit', 'thread-list-item',
|
||||
'discussion-home', 'search-alert', 'new-post', 'thread-type', 'new-post-menu-entry',
|
||||
'new-post-menu-category', 'topic', 'post-user-display', 'inline-discussion', 'pagination',
|
||||
'user-profile', 'profile-thread'
|
||||
];
|
||||
templateNamesNoTrailingTemplate = [
|
||||
'forum-action-endorse', 'forum-action-answer', 'forum-action-follow', 'forum-action-vote',
|
||||
'forum-action-report', 'forum-action-pin', 'forum-action-close', 'forum-action-edit',
|
||||
'forum-action-delete', 'forum-actions'
|
||||
];
|
||||
for (_i = 0, _len = templateNames.length; _i < _len; _i++) {
|
||||
templateName = templateNames[_i];
|
||||
templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore');
|
||||
appendSetFixtures($('<script>', {
|
||||
id: templateName + '-template',
|
||||
type: 'text/template'
|
||||
}).text(templateFixture));
|
||||
}
|
||||
for (_j = 0, _len1 = templateNamesNoTrailingTemplate.length; _j < _len1; _j++) {
|
||||
templateName = templateNamesNoTrailingTemplate[_j];
|
||||
templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore');
|
||||
appendSetFixtures($('<script>', {
|
||||
id: templateName,
|
||||
type: 'text/template'
|
||||
}).text(templateFixture));
|
||||
}
|
||||
return appendSetFixtures(
|
||||
"<div id=\"fixture-element\"></div>\n" +
|
||||
"<div id=\"discussion-container\"" +
|
||||
" data-course-name=\"Fake Course\"" +
|
||||
" data-user-create-comment=\"true\"" +
|
||||
" data-user-create-subcomment=\"true\"" +
|
||||
" data-read-only=\"false\"" +
|
||||
"></div>"
|
||||
);
|
||||
};
|
||||
|
||||
return DiscussionSpecHelper;
|
||||
|
||||
})();
|
||||
|
||||
}).call(this);
|
||||
@@ -1,7 +1,7 @@
|
||||
define([
|
||||
'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'teams/js/views/team_discussion',
|
||||
'teams/js/spec_helpers/team_spec_helpers',
|
||||
'xmodule_js/common_static/coffee/spec/discussion/discussion_spec_helper'
|
||||
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper'
|
||||
], function (_, AjaxHelpers, TeamDiscussionView, TeamSpecHelpers, DiscussionSpecHelper) {
|
||||
'use strict';
|
||||
xdescribe('TeamDiscussionView', function() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define([
|
||||
'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'teams/js/models/team',
|
||||
'teams/js/views/team_profile', 'teams/js/spec_helpers/team_spec_helpers',
|
||||
'xmodule_js/common_static/coffee/spec/discussion/discussion_spec_helper'
|
||||
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper'
|
||||
], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) {
|
||||
'use strict';
|
||||
describe('TeamProfileView', function () {
|
||||
|
||||
@@ -1296,7 +1296,7 @@ discussion_js = (
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/customwmd.js') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/mathjax_accessible.js') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/mathjax_delay_renderer.js') +
|
||||
sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
|
||||
sorted(rooted_glob(COMMON_ROOT / 'static', 'common/js/discussion/**/*.js'))
|
||||
)
|
||||
|
||||
discussion_vendor_js = [
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
'js/ccx/schedule': 'js/ccx/schedule',
|
||||
|
||||
// Discussion classes loaded explicitly until they are converted to use RequireJS
|
||||
'DiscussionModuleView': 'xmodule_js/common_static/coffee/src/discussion/discussion_module_view',
|
||||
'DiscussionModuleView': 'xmodule_js/common_static/common/js/discussion/discussion_module_view',
|
||||
|
||||
'js/bookmarks/collections/bookmarks': 'js/bookmarks/collections/bookmarks',
|
||||
'js/bookmarks/models/bookmark': 'js/bookmarks/models/bookmark',
|
||||
@@ -520,7 +520,7 @@
|
||||
exports: 'Slick'
|
||||
},
|
||||
// Discussions
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils': {
|
||||
'xmodule_js/common_static/common/js/discussion/utils': {
|
||||
deps: [
|
||||
'jquery',
|
||||
'jquery.timeago',
|
||||
@@ -539,102 +539,102 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/content': {
|
||||
'xmodule_js/common_static/common/js/discussion/content': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'Content'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/discussion': {
|
||||
'xmodule_js/common_static/common/js/discussion/discussion': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils',
|
||||
'xmodule_js/common_static/coffee/src/discussion/content'
|
||||
'xmodule_js/common_static/common/js/discussion/utils',
|
||||
'xmodule_js/common_static/common/js/discussion/content'
|
||||
],
|
||||
exports: 'Discussion'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/models/discussion_course_settings': {
|
||||
'xmodule_js/common_static/common/js/discussion/models/discussion_course_settings': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionCourseSettings'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/models/discussion_user': {
|
||||
'xmodule_js/common_static/common/js/discussion/models/discussion_user': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionUser'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_content_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_content_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionContentView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_edit_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_edit_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionThreadEditView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_list_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_list_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionThreadListView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_profile_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_profile_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionThreadProfileView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_show_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_show_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_content_view'
|
||||
'xmodule_js/common_static/common/js/discussion/utils',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_content_view'
|
||||
],
|
||||
exports: 'DiscussionThreadShowView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_content_view'
|
||||
'xmodule_js/common_static/common/js/discussion/utils',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_content_view'
|
||||
],
|
||||
exports: 'DiscussionThreadView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_topic_menu_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_topic_menu_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionTopicMenuView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_user_profile_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_user_profile_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionUserProfileView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/new_post_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/new_post_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'NewPostView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_edit_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/thread_response_edit_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'ThreadResponseEditView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_show_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/thread_response_show_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'ThreadResponseShowView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_view': {
|
||||
'xmodule_js/common_static/common/js/discussion/views/thread_response_view': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'ThreadResponseView'
|
||||
},
|
||||
@@ -645,29 +645,29 @@
|
||||
'backbone',
|
||||
'gettext',
|
||||
'URI',
|
||||
'xmodule_js/common_static/coffee/src/discussion/content',
|
||||
'xmodule_js/common_static/coffee/src/discussion/discussion',
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils',
|
||||
'xmodule_js/common_static/coffee/src/discussion/models/discussion_course_settings',
|
||||
'xmodule_js/common_static/coffee/src/discussion/models/discussion_user',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_content_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_edit_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_list_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_profile_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_show_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_thread_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_topic_menu_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/discussion_user_profile_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/new_post_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_edit_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_show_view',
|
||||
'xmodule_js/common_static/coffee/src/discussion/views/thread_response_view'
|
||||
'xmodule_js/common_static/common/js/discussion/content',
|
||||
'xmodule_js/common_static/common/js/discussion/discussion',
|
||||
'xmodule_js/common_static/common/js/discussion/utils',
|
||||
'xmodule_js/common_static/common/js/discussion/models/discussion_course_settings',
|
||||
'xmodule_js/common_static/common/js/discussion/models/discussion_user',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_content_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_edit_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_list_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_profile_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_show_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_thread_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_topic_menu_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/discussion_user_profile_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/new_post_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/thread_response_edit_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/thread_response_show_view',
|
||||
'xmodule_js/common_static/common/js/discussion/views/thread_response_view'
|
||||
],
|
||||
exports: 'DiscussionModuleView'
|
||||
},
|
||||
'xmodule_js/common_static/coffee/spec/discussion/discussion_spec_helper': {
|
||||
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper': {
|
||||
deps: [
|
||||
'xmodule_js/common_static/coffee/src/discussion/utils'
|
||||
'xmodule_js/common_static/common/js/discussion/utils'
|
||||
],
|
||||
exports: 'DiscussionSpecHelper'
|
||||
},
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"rules": {
|
||||
"javascript-concat-html": 205,
|
||||
"javascript-escape": 7,
|
||||
"javascript-interpolate": 51,
|
||||
"javascript-jquery-append": 110,
|
||||
"javascript-jquery-html": 274,
|
||||
"javascript-jquery-insert-into-target": 26,
|
||||
"javascript-jquery-insertion": 28,
|
||||
"javascript-jquery-prepend": 12,
|
||||
"javascript-interpolate": 49,
|
||||
"javascript-jquery-append": 104,
|
||||
"javascript-jquery-html": 275,
|
||||
"javascript-jquery-insert-into-target": 27,
|
||||
"javascript-jquery-insertion": 26,
|
||||
"javascript-jquery-prepend": 11,
|
||||
"mako-html-entities": 0,
|
||||
"mako-invalid-html-filter": 29,
|
||||
"mako-invalid-js-filter": 209,
|
||||
"mako-invalid-html-filter": 27,
|
||||
"mako-invalid-js-filter": 207,
|
||||
"mako-js-html-string": 0,
|
||||
"mako-js-missing-quotes": 0,
|
||||
"mako-missing-default": 215,
|
||||
"mako-missing-default": 213,
|
||||
"mako-multiple-page-tags": 0,
|
||||
"mako-unknown-context": 0,
|
||||
"mako-unparseable-expression": 0,
|
||||
@@ -21,12 +21,12 @@
|
||||
"python-close-before-format": 0,
|
||||
"python-concat-html": 27,
|
||||
"python-custom-escape": 13,
|
||||
"python-deprecated-display-name": 54,
|
||||
"python-interpolate-html": 68,
|
||||
"python-deprecated-display-name": 53,
|
||||
"python-interpolate-html": 66,
|
||||
"python-parse-error": 0,
|
||||
"python-requires-html-or-text": 0,
|
||||
"python-wrap-html": 266,
|
||||
"underscore-not-escaped": 659
|
||||
"python-wrap-html": 264,
|
||||
"underscore-not-escaped": 658
|
||||
},
|
||||
"total": 2237
|
||||
"total": 2232
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user