Commit 11fa2f37 by Andy Armstrong Committed by GitHub

Merge pull request #12782 from edx/ekolpakov/coffee_to_js_discussions

Hackathon project: burn coffee
parents 2fab4938 2b8c02a7
!view/discussion_thread_edit_view_spec.js
!view/discussion_topic_menu_view_spec.js
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)
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>
""")
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);
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 []
# -*- 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')
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.'
)
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()
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: ->}
)
# -*- 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()
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')
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)
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())
!views/discussion_thread_edit_view.js
!views/discussion_topic_menu_view.js
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)
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)
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.")
)
)
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()
)
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)
if Backbone?
class @DiscussionCourseSettings extends Backbone.Model
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()
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)
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>"))
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.")
)
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
)
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
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
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()
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
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
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()
/* 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);
/* 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);
/* 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);
(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);
(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);
(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
/* 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);
/* 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);
(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);
/* 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);
/* 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);
/* 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);
/* 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);
/* 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);
/* 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);
/* 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);
......@@ -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'}
],
......
/* 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);
/* 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);
/* 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);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment