Merge pull request #12782 from edx/ekolpakov/coffee_to_js_discussions

Hackathon project: burn coffee
This commit is contained in:
Andy Armstrong
2016-06-23 12:55:16 -04:00
committed by GitHub
84 changed files with 8200 additions and 5668 deletions

View File

@@ -1,2 +0,0 @@
!view/discussion_thread_edit_view_spec.js
!view/discussion_topic_menu_view_spec.js

View File

@@ -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)

View File

@@ -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>
""")

View File

@@ -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);

View File

@@ -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 []

View File

@@ -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"
)

View File

@@ -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')

View File

@@ -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.'
)

View File

@@ -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')

View File

@@ -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()

View File

@@ -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: ->}
)

View File

@@ -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()

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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())

View File

@@ -1,2 +0,0 @@
!views/discussion_thread_edit_view.js
!views/discussion_topic_menu_view.js

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.")
)
)

View File

@@ -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()
)

View File

@@ -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)

View File

@@ -1,2 +0,0 @@
if Backbone?
class @DiscussionCourseSettings extends Backbone.Model

View File

@@ -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()

View File

@@ -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(/\&lt\;highlight\&gt\;/g, "<span class='search-highlight'>")
.replace(/\&lt\;\/highlight\&gt\;/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(/^\&gt\;/gm, ">")
converter = Markdown.getMathCompatibleConverter()
text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
return text.replace(/^>/gm,"&gt;")
@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

View File

@@ -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

View File

@@ -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')

View File

@@ -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)

View File

@@ -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(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))

View File

@@ -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) =>

View File

@@ -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.")
)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View 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);

View 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);

View 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);

View 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);

View 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);

View File

@@ -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);

View 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);

View 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(
/\&lt\;highlight\&gt\;/g,
"<span class='search-highlight'>").replace(/\&lt\;\/highlight\&gt\;/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(/^\&gt\;/gm, ">");
converter = Markdown.getMathCompatibleConverter();
text = this.unescapeHighlightTag(this.stripLatexHighlight(converter.makeHtml(text)));
return text.replace(/^>/gm, "&gt;");
};
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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"));
}
};
return DiscussionThreadShowView;
})(DiscussionContentShowView);
}
}).call(window);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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);

View File

@@ -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'}
],

View 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);

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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);

View File

@@ -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() {

View File

@@ -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 () {

View File

@@ -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 = [

View File

@@ -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'
},

View File

@@ -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
}