Commit afaa463f by Greg Price

Merge pull request #2093 from edx/gprice/forum-vote-button-a11y

Improve accessibility of forum vote buttons
parents 1adc4fe2 b7d7751d
describe "DiscussionContentView", -> describe "DiscussionContentView", ->
beforeEach -> beforeEach ->
setFixtures setFixtures(
(
""" """
<div class="discussion-post"> <div class="discussion-post">
<header> <header>
<a data-tooltip="vote" data-role="discussion-vote" class="vote-btn discussion-vote discussion-vote-up" href="#"> <a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon">+</span> <span class="votes-count-number">0</span></a> <span class="plus-icon"/><span class='votes-count-number'>0</span> <span class="sr">votes (click to vote)</span></a>
<h1>Post Title</h1> <h1>Post Title</h1>
<p class="posted-details"> <p class="posted-details">
<a class="username" href="/courses/MITx/999/Robot_Super_Course/discussion/forum/users/1">robot</a> <a class="username" href="/courses/MITx/999/Robot_Super_Course/discussion/forum/users/1">robot</a>
...@@ -23,16 +22,21 @@ describe "DiscussionContentView", -> ...@@ -23,16 +22,21 @@ describe "DiscussionContentView", ->
""" """
) )
@thread = new Thread { @threadData = {
id: '01234567', id: '01234567',
user_id: '567', user_id: '567',
course_id: 'mitX/999/test', course_id: 'mitX/999/test',
body: 'this is a thread', body: 'this is a thread',
created_at: '2013-04-03T20:08:39Z', created_at: '2013-04-03T20:08:39Z',
abuse_flaggers: ['123'] abuse_flaggers: ['123'],
roles: [] votes: {up_count: '42'},
type: "thread",
roles: []
} }
@thread = new Thread(@threadData)
@view = new DiscussionContentView({ model: @thread }) @view = new DiscussionContentView({ model: @thread })
@view.setElement($('.discussion-post'))
window.user = new DiscussionUser({id: '567', upvoted_ids: []})
it 'defines the tag', -> it 'defines the tag', ->
expect($('#jasmine-fixtures')).toExist expect($('#jasmine-fixtures')).toExist
...@@ -56,3 +60,15 @@ describe "DiscussionContentView", -> ...@@ -56,3 +60,15 @@ describe "DiscussionContentView", ->
@thread.set("abuse_flaggers",temp_array) @thread.set("abuse_flaggers",temp_array)
@thread.unflagAbuse() @thread.unflagAbuse()
expect(@thread.get 'abuse_flaggers').toEqual [] expect(@thread.get 'abuse_flaggers').toEqual []
it 'renders the vote button properly', ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it 'votes correctly', ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, false)
it 'unvotes correctly', ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, false)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
describe "DiscussionThreadProfileView", ->
beforeEach ->
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
</div>
"""
)
@threadData = {
id: "dummy",
user_id: "567",
course_id: "TestOrg/TestCourse/TestRun",
body: "this is a thread",
created_at: "2013-04-03T20:08:39Z",
abuse_flaggers: [],
votes: {up_count: "42"}
}
@thread = new Thread(@threadData)
@view = new DiscussionThreadProfileView({ model: @thread })
@view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true)
it "toggles the vote correctly", ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
describe "DiscussionThreadShowView", ->
beforeEach ->
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
</div>
"""
)
@threadData = {
id: "dummy",
user_id: "567",
course_id: "TestOrg/TestCourse/TestRun",
body: "this is a thread",
created_at: "2013-04-03T20:08:39Z",
abuse_flaggers: [],
votes: {up_count: "42"}
}
@thread = new Thread(@threadData)
@view = new DiscussionThreadShowView({ model: @thread })
@view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
class @DiscussionViewSpecHelper
@expectVoteRendered = (view, voted) ->
button = view.$el.find(".vote-btn")
if voted
expect(button.hasClass("is-cast")).toBe(true)
expect(button.attr("aria-pressed")).toEqual("true")
expect(button.attr("data-tooltip")).toEqual("remove vote")
expect(button.find(".votes-count-number").html()).toEqual("43")
expect(button.find(".sr").html()).toEqual("votes (click to remove your vote)")
else
expect(button.hasClass("is-cast")).toBe(false)
expect(button.attr("aria-pressed")).toEqual("false")
expect(button.attr("data-tooltip")).toEqual("vote")
expect(button.find(".votes-count-number").html()).toEqual("42")
expect(button.find(".sr").html()).toEqual("votes (click to vote)")
@checkRenderVote = (view, model) ->
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, false)
window.user.vote(model)
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, true)
window.user.unvote(model)
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, false)
@checkVote = (view, model, modelData, checkRendering) ->
view.renderVote()
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
spyOn($, "ajax").andCallFake((params) =>
newModelData = {}
$.extend(newModelData, modelData, {votes: {up_count: "43"}})
params.success(newModelData, "success")
# Caller invokes always function on return value but it doesn't matter here
{always: ->}
)
view.vote()
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
expect($.ajax).toHaveBeenCalled()
$.ajax.reset()
# Check idempotence
view.vote()
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
expect($.ajax).toHaveBeenCalled()
@checkUnvote = (view, model, modelData, checkRendering) ->
window.user.vote(model)
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
spyOn($, "ajax").andCallFake((params) =>
newModelData = {}
$.extend(newModelData, modelData, {votes: {up_count: "42"}})
params.success(newModelData, "success")
# Caller invokes always function on return value but it doesn't matter here
{always: ->}
)
view.unvote()
expect(window.user.voted(model)).toBe(false)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
expect($.ajax).toHaveBeenCalled()
$.ajax.reset()
# Check idempotence
view.unvote()
expect(window.user.voted(model)).toBe(false)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
expect($.ajax).toHaveBeenCalled()
@checkToggleVote = (view, model) ->
event = {preventDefault: ->}
spyOn(event, "preventDefault")
spyOn(view, "vote").andCallFake(() -> window.user.vote(model))
spyOn(view, "unvote").andCallFake(() -> window.user.unvote(model))
expect(window.user.voted(model)).toBe(false)
view.toggleVote(event)
expect(view.vote).toHaveBeenCalled()
expect(view.unvote).not.toHaveBeenCalled()
expect(event.preventDefault.callCount).toEqual(1)
view.vote.reset()
view.unvote.reset()
expect(window.user.voted(model)).toBe(true)
view.toggleVote(event)
expect(view.vote).not.toHaveBeenCalled()
expect(view.unvote).toHaveBeenCalled()
expect(event.preventDefault.callCount).toEqual(2)
@checkVoteButtonEvents = (view) ->
spyOn(view, "toggleVote")
button = view.$el.find(".vote-btn")
button.click()
expect(view.toggleVote).toHaveBeenCalled()
view.toggleVote.reset()
button.trigger($.Event("keydown", {which: 13}))
expect(view.toggleVote).toHaveBeenCalled()
view.toggleVote.reset()
button.trigger($.Event("keydown", {which: 32}))
expect(view.toggleVote).not.toHaveBeenCalled()
describe "ThreadResponseShowView", ->
beforeEach ->
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
</div>
"""
)
@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: [],
votes: {up_count: "42"}
}
@comment = new Comment(@commentData)
@view = new ThreadResponseShowView({ model: @comment })
@view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @comment, @commentData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @commentData, true)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @comment)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
...@@ -99,6 +99,13 @@ if Backbone? ...@@ -99,6 +99,13 @@ if Backbone?
@get("abuse_flaggers").pop(window.user.get('id')) @get("abuse_flaggers").pop(window.user.get('id'))
@trigger "change", @ @trigger "change", @
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
class @Thread extends @Content class @Thread extends @Content
urlMappers: urlMappers:
...@@ -130,14 +137,6 @@ if Backbone? ...@@ -130,14 +137,6 @@ if Backbone?
unfollow: -> unfollow: ->
@set('subscribed', false) @set('subscribed', false)
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
display_body: -> display_body: ->
if @has("highlighted_body") if @has("highlighted_body")
String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>') String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
......
...@@ -91,7 +91,7 @@ class @DiscussionUtil ...@@ -91,7 +91,7 @@ class @DiscussionUtil
@activateOnEnter: (event, func) -> @activateOnEnter: (event, func) ->
if event.which == 13 if event.which == 13
e.preventDefault() event.preventDefault()
func(event) func(event)
@makeFocusTrap: (elem) -> @makeFocusTrap: (elem) ->
......
...@@ -159,3 +159,42 @@ if Backbone? ...@@ -159,3 +159,42 @@ if Backbone?
temp_array = [] temp_array = []
@model.set('abuse_flaggers', temp_array) @model.set('abuse_flaggers', temp_array)
renderVote: =>
button = @$el.find(".vote-btn")
voted = window.user.voted(@model)
voteNum = @model.get("votes")["up_count"]
button.toggleClass("is-cast", voted)
button.attr("aria-pressed", voted)
button.attr("data-tooltip", if voted then "remove vote" else "vote")
button.find(".votes-count-number").html(voteNum)
button.find(".sr").html(if voted then "votes (click to remove your vote)" else "votes (click to vote)")
toggleVote: (event) =>
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
vote: =>
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$el.find(".vote-btn")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: =>
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$el.find(".vote-btn")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
...@@ -2,7 +2,10 @@ if Backbone? ...@@ -2,7 +2,10 @@ if Backbone?
class @DiscussionThreadProfileView extends DiscussionContentView class @DiscussionThreadProfileView extends DiscussionContentView
expanded = false expanded = false
events: events:
"click .discussion-vote": "toggleVote" "click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnEnter(event, @toggleVote)
"click .action-follow": "toggleFollowing" "click .action-follow": "toggleFollowing"
"keypress .action-follow": "keypress .action-follow":
(event) -> DiscussionUtil.activateOnEnter(event, toggleFollowing) (event) -> DiscussionUtil.activateOnEnter(event, toggleFollowing)
...@@ -27,7 +30,7 @@ if Backbone? ...@@ -27,7 +30,7 @@ if Backbone?
@$el.html(Mustache.render(@template, params)) @$el.html(Mustache.render(@template, params))
@initLocal() @initLocal()
@delegateEvents() @delegateEvents()
@renderVoted() @renderVote()
@renderAttrs() @renderAttrs()
@$("span.timeago").timeago() @$("span.timeago").timeago()
@convertMath() @convertMath()
...@@ -35,15 +38,8 @@ if Backbone? ...@@ -35,15 +38,8 @@ if Backbone?
@renderResponses() @renderResponses()
@ @
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
updateModelDetails: => updateModelDetails: =>
@renderVoted() @renderVote()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: -> convertMath: ->
element = @$(".post-body") element = @$(".post-body")
...@@ -71,35 +67,6 @@ if Backbone? ...@@ -71,35 +67,6 @@ if Backbone?
addComment: => addComment: =>
@model.comment() @model.comment()
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: -> edit: ->
abbreviateBody: -> abbreviateBody: ->
......
...@@ -2,7 +2,10 @@ if Backbone? ...@@ -2,7 +2,10 @@ if Backbone?
class @DiscussionThreadShowView extends DiscussionContentView class @DiscussionThreadShowView extends DiscussionContentView
events: events:
"click .discussion-vote": "toggleVote" "click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnEnter(event, @toggleVote)
"click .discussion-flag-abuse": "toggleFlagAbuse" "click .discussion-flag-abuse": "toggleFlagAbuse"
"keypress .discussion-flag-abuse": "keypress .discussion-flag-abuse":
(event) -> DiscussionUtil.activateOnEnter(event, toggleFlagAbuse) (event) -> DiscussionUtil.activateOnEnter(event, toggleFlagAbuse)
...@@ -28,7 +31,7 @@ if Backbone? ...@@ -28,7 +31,7 @@ if Backbone?
render: -> render: ->
@$el.html(@renderTemplate()) @$el.html(@renderTemplate())
@delegateEvents() @delegateEvents()
@renderVoted() @renderVote()
@renderFlagged() @renderFlagged()
@renderPinned() @renderPinned()
@renderAttrs() @renderAttrs()
...@@ -38,14 +41,6 @@ if Backbone? ...@@ -38,14 +41,6 @@ if Backbone?
@highlight @$("h1,h3") @highlight @$("h1,h3")
@ @
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
@$("[data-role=discussion-vote] span.sr").html("votes (click to remove your vote)")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
@$("[data-role=discussion-vote] span.sr").html("votes (click to vote)")
renderFlagged: => renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0) if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged") @$("[data-role=thread-flag]").addClass("flagged")
...@@ -70,52 +65,15 @@ if Backbone? ...@@ -70,52 +65,15 @@ if Backbone?
updateModelDetails: => updateModelDetails: =>
@renderVoted() @renderVote()
@renderFlagged() @renderFlagged()
@renderPinned() @renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"] + '<span class ="sr"></span>')
if window.user.voted(@model)
@$("[data-role=discussion-vote] .votes-count-number span.sr").html("votes (click to remove your vote)")
else
@$("[data-role=discussion-vote] .votes-count-number span.sr").html("votes (click to vote)")
convertMath: -> convertMath: ->
element = @$(".post-body") element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
edit: (event) -> edit: (event) ->
@trigger "thread:edit", event @trigger "thread:edit", event
......
if Backbone? if Backbone?
class @ThreadResponseShowView extends DiscussionContentView class @ThreadResponseShowView extends DiscussionContentView
events: events:
"click .vote-btn": "toggleVote" "click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnEnter(event, @toggleVote)
"click .action-endorse": "toggleEndorse" "click .action-endorse": "toggleEndorse"
"click .action-delete": "_delete" "click .action-delete": "_delete"
"click .action-edit": "edit" "click .action-edit": "edit"
...@@ -23,9 +26,7 @@ if Backbone? ...@@ -23,9 +26,7 @@ if Backbone?
render: -> render: ->
@$el.html(@renderTemplate()) @$el.html(@renderTemplate())
@delegateEvents() @delegateEvents()
if window.user.voted(@model) @renderVote()
@$(".vote-btn").addClass("is-cast")
@$(".vote-btn span.sr").html("votes (click to remove your vote)")
@renderAttrs() @renderAttrs()
@renderFlagged() @renderFlagged()
@$el.find(".posted-details").timeago() @$el.find(".posted-details").timeago()
...@@ -46,39 +47,6 @@ if Backbone? ...@@ -46,39 +47,6 @@ if Backbone?
@$el.addClass("community-ta") @$el.addClass("community-ta")
@$el.prepend('<div class="community-ta-banner">Community TA</div>') @$el.prepend('<div class="community-ta-banner">Community TA</div>')
toggleVote: (event) ->
event.preventDefault()
@$(".vote-btn").toggleClass("is-cast")
if @$(".vote-btn").hasClass("is-cast")
@vote()
@$(".vote-btn span.sr").html("votes (click to remove your vote)")
else
@unvote()
@$(".vote-btn span.sr").html("votes (click to vote)")
vote: ->
url = @model.urlFor("upvote")
@$(".votes-count-number").html((parseInt(@$(".votes-count-number").html()) + 1) + '<span class="sr"></span>')
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
url = @model.urlFor("unvote")
@$(".votes-count-number").html((parseInt(@$(".votes-count-number").html()) - 1)+'<span class="sr"></span>')
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: (event) -> edit: (event) ->
@trigger "response:edit", event @trigger "response:edit", event
...@@ -115,4 +83,5 @@ if Backbone? ...@@ -115,4 +83,5 @@ if Backbone?
@$(".discussion-flag-abuse .flag-label").html("Report Misuse") @$(".discussion-flag-abuse .flag-label").html("Report Misuse")
updateModelDetails: => updateModelDetails: =>
@renderVote()
@renderFlagged() @renderFlagged()
...@@ -34,6 +34,7 @@ lib_paths: ...@@ -34,6 +34,7 @@ lib_paths:
- js/vendor/underscore-min.js - js/vendor/underscore-min.js
- js/vendor/backbone-min.js - js/vendor/backbone-min.js
- js/vendor/jquery.timeago.js - js/vendor/jquery.timeago.js
- js/vendor/URI.min.js
- coffee/src/ajax_prefix.js - coffee/src/ajax_prefix.js
- js/test/add_ajax_prefix.js - js/test/add_ajax_prefix.js
- coffee/src/jquery.immediateDescendents.js - coffee/src/jquery.immediateDescendents.js
......
...@@ -50,16 +50,14 @@ def permitted(fn): ...@@ -50,16 +50,14 @@ def permitted(fn):
return wrapper return wrapper
def ajax_content_response(request, course_id, content, template_name): def ajax_content_response(request, course_id, content):
context = { context = {
'course_id': course_id, 'course_id': course_id,
'content': content, 'content': content,
} }
html = render_to_string(template_name, context)
user_info = cc.User.from_django_user(request.user).to_dict() user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info) annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
return JsonResponse({ return JsonResponse({
'html': html,
'content': utils.safe_content(content), 'content': utils.safe_content(content),
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
}) })
...@@ -131,7 +129,7 @@ def create_thread(request, course_id, commentable_id): ...@@ -131,7 +129,7 @@ def create_thread(request, course_id, commentable_id):
data = thread.to_dict() data = thread.to_dict()
add_courseware_context([data], course) add_courseware_context([data], course)
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_id, data, 'discussion/ajax_create_thread.html') return ajax_content_response(request, course_id, data)
else: else:
return JsonResponse(utils.safe_content(data)) return JsonResponse(utils.safe_content(data))
...@@ -147,7 +145,7 @@ def update_thread(request, course_id, thread_id): ...@@ -147,7 +145,7 @@ def update_thread(request, course_id, thread_id):
thread.update_attributes(**extract(request.POST, ['body', 'title', 'tags'])) thread.update_attributes(**extract(request.POST, ['body', 'title', 'tags']))
thread.save() thread.save()
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_id, thread.to_dict(), 'discussion/ajax_update_thread.html') return ajax_content_response(request, course_id, thread.to_dict())
else: else:
return JsonResponse(utils.safe_content(thread.to_dict())) return JsonResponse(utils.safe_content(thread.to_dict()))
...@@ -184,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): ...@@ -184,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None):
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user.follow(comment.thread) user.follow(comment.thread)
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_create_comment.html') return ajax_content_response(request, course_id, comment.to_dict())
else: else:
return JsonResponse(utils.safe_content(comment.to_dict())) return JsonResponse(utils.safe_content(comment.to_dict()))
...@@ -228,7 +226,7 @@ def update_comment(request, course_id, comment_id): ...@@ -228,7 +226,7 @@ def update_comment(request, course_id, comment_id):
comment.update_attributes(**extract(request.POST, ['body'])) comment.update_attributes(**extract(request.POST, ['body']))
comment.save() comment.save()
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_update_comment.html') return ajax_content_response(request, course_id, comment.to_dict())
else: else:
return JsonResponse(utils.safe_content(comment.to_dict())) return JsonResponse(utils.safe_content(comment.to_dict()))
......
...@@ -248,13 +248,10 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -248,13 +248,10 @@ def single_thread(request, course_id, discussion_id, thread_id):
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info) annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
context = {'thread': thread.to_dict(), 'course_id': course_id} context = {'thread': thread.to_dict(), 'course_id': course_id}
# TODO: Remove completely or switch back to server side rendering
# html = render_to_string('discussion/_ajax_single_thread.html', context)
content = utils.safe_content(thread.to_dict()) content = utils.safe_content(thread.to_dict())
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context([content], course) add_courseware_context([content], course)
return utils.JsonResponse({ return utils.JsonResponse({
#'html': html,
'content': content, 'content': content,
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
}) })
......
...@@ -31,22 +31,3 @@ def include_mustache_templates(): ...@@ -31,22 +31,3 @@ def include_mustache_templates():
file_contents = map(read_file, filter(valid_file_name, os.listdir(mustache_dir))) file_contents = map(read_file, filter(valid_file_name, os.listdir(mustache_dir)))
return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents))) return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents)))
def render_content(content, additional_context={}):
context = {
'content': extend_content(content),
content['type']: True,
}
if cc_settings.MAX_COMMENT_DEPTH is not None:
if content['type'] == 'thread':
if cc_settings.MAX_COMMENT_DEPTH < 0:
context['max_depth'] = True
elif content['type'] == 'comment':
if cc_settings.MAX_COMMENT_DEPTH <= content['depth']:
context['max_depth'] = True
context = merge_dict(context, additional_context)
partial_mustache_helpers = {k: partial(v, content) for k, v in mustache_helpers.items()}
context = merge_dict(context, partial_mustache_helpers)
return render_mustache('discussion/mustache/_content.mustache', context)
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_comments(thread.get('children'))}
<%! import django_comment_client.helpers as helpers %>
<%def name="render_content(content, *args, **kwargs)">
${helpers.render_content(content, *args, **kwargs)}
</%def>
<%def name="render_content_with_comments(content, *args, **kwargs)">
<div class="${content['type'] | h}${' endorsed' if content.get('endorsed') else ''| h}" _id="${content['id'] | h}" _discussion_id="${content.get('commentable_id', '') | h}" _author_id="${content['user_id'] if (not content.get('anonymous')) else '' | h}">
${render_content(content, *args, **kwargs)}
${render_comments(content.get('children', []), *args, **kwargs)}
</div>
</%def>
<%def name="render_comments(comments, *args, **kwargs)">
<div class="comments">
% for comment in comments:
${render_content_with_comments(comment, *args, **kwargs)}
% endfor
</div>
</%def>
<%namespace name="renderer" file="_content_renderer.html"/>
<section class="discussion forum-discussion" _id="${discussion_id | h}">
<div class="discussion-non-content local">
<div class="search-wrapper">
<%include file="_search_bar.html" />
</div>
</div>
% if len(threads) == 0:
<div class="blank">
<%include file="_blank_slate.html" />
</div>
<div class="threads"></div>
% else:
<%include file="_sort.html" />
<div class="threads">
% for thread in threads:
${renderer.render_content_with_comments(thread)}
% endfor
</div>
<%include file="_paginator.html" />
% endif
</section>
<%include file="_js_data.html" />
<%namespace name="renderer" file="_content_renderer.html"/>
<section class="discussion inline-discussion" _id="${discussion_id | h}">
<div class="discussion-non-content local"></div>
<div class="threads">
% for thread in threads:
${renderer.render_content_with_comments(thread)}
% endfor
</div>
<%include file="_paginator.html" />
</section>
<%include file="_js_data.html" />
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script>
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/URI.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script>
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/URI.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
......
<%! from django.utils.translation import ugettext as _ %>
<%namespace name="renderer" file="_content_renderer.html"/>
<%! from django_comment_client.mustache_helpers import url_for_user %>
<article class="discussion-article" data-id="${discussion_id| h}">
<a href="#" class="dogear"></a>
<div class="discussion-post">
<header>
%if thread['group_id']:
<div class="group-visibility-label">${_("This post visible only to group {group}.").format(group=cohort_dictionary[thread['group_id']])} </div>
%endif
<a href="#" class="vote-btn discussion-vote discussion-vote-up"><span class="plus-icon">+</span> <span class='votes-count-number'>${thread['votes']['up_count']}<span class="sr">votes (click to vote)</span></span></a>
<h1>${thread['title']}</h1>
<p class="posted-details">
<span class="timeago" title="${thread['created_at'] | h}">sometime</span> by
<a href="${url_for_user(thread, thread['user_id'])}">${thread['username']}</a>
</p>
</header>
<div class="post-body">
${thread['body']}
</div>
</div>
<ol class="responses">
% for reply in thread.get("children", []):
<li>
<div class="response-body">${reply['body']}</div>
<ol class="comments">
% for comment in reply.get("children", []):
<li><div class="comment-body">${comment['body']}</div></li>
% endfor
</ol>
</li>
% endfor
</ol>
</article>
<%include file="_js_data.html" />
...@@ -31,8 +31,8 @@ ...@@ -31,8 +31,8 @@
<div class="group-visibility-label">${"<%- obj.group_string%>"}</div> <div class="group-visibility-label">${"<%- obj.group_string%>"}</div>
${"<% } %>"} ${"<% } %>"}
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"> <a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon">+</span> <span class='votes-count-number'>${'<%- votes["up_count"] %>'}<span class="sr">votes (click to vote)</span></span></a> <span class="plus-icon"/><span class='votes-count-number'>${'<%- votes["up_count"] %>'}</span> <span class="sr">votes (click to vote)</span></a>
<h1>${'<%- title %>'}</h1> <h1>${'<%- title %>'}</h1>
<p class="posted-details"> <p class="posted-details">
${"<% if (obj.username) { %>"} ${"<% if (obj.username) { %>"}
...@@ -123,7 +123,7 @@ ...@@ -123,7 +123,7 @@
<script type="text/template" id="thread-response-show-template"> <script type="text/template" id="thread-response-show-template">
<header class="response-local"> <header class="response-local">
<a href="javascript:void(0)" class="vote-btn" data-tooltip="vote"><span class="plus-icon"></span><span class="votes-count-number">${"<%- votes['up_count'] %>"}<span class="sr">votes (click to vote)</span></span></a> <a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class="votes-count-number">${"<%- votes['up_count'] %>"}</span> <span class="sr">votes (click to vote)</span></a>
<a href="javascript:void(0)" class="endorse-btn${'<% if (endorsed) { %> is-endorsed<% } %>'} action-endorse" style="cursor: default; display: none;" data-tooltip="endorse"><span class="check-icon" style="pointer-events: none; "></span></a> <a href="javascript:void(0)" class="endorse-btn${'<% if (endorsed) { %> is-endorsed<% } %>'} action-endorse" style="cursor: default; display: none;" data-tooltip="endorse"><span class="check-icon" style="pointer-events: none; "></span></a>
${"<% if (obj.username) { %>"} ${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a> <a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a>
......
<%namespace name="renderer" file="_content_renderer.html"/>
<section class="discussion user-active-discussion" _id="${user_id | h}">
<div class="discussion-non-content local"></div>
<div class="threads">
% for thread in threads:
${renderer.render_content_with_comments(thread, {'partial_comments': True})}
% endfor
</div>
<%include file="_paginator.html" />
</section>
<%include file="_js_data.html" />
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content_with_comments(content)}
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content_with_comments(content)}
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content(content)}
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content(content)}
<div class="discussion-content local{{#content.roles}} role-{{name}}{{/content.roles}}">
CONTENT MUSTACHE
<div class="discussion-content-wrapper">
<div class="discussion-votes">
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)" value="up">&#9650;</a>
<div class="discussion-votes-point">{{content.votes.point}}<span class="sr">votes (click to vote)</span></div>
<a class="discussion-vote discussion-vote-down" href="javascript:void(0)" value="down">&#9660;</a>
</div>
<div class="discussion-right-wrapper">
<ul class="admin-actions">
<li style="display: none;"><a href="javascript:void(0)" class="admin-endorse">Endorse</a></li>
<li style="display: none;"><a href="javascript:void(0)" class="admin-edit">Edit</a></li>
<li style="display: none;"><a href="javascript:void(0)" class="admin-delete">Delete</a></li>
{{#thread}}
<li style="display: none;"><a href="javascript:void(0)" class="admin-openclose">{{close_thread_text}}</a></li>
{{/thread}}
</ul>
{{#thread}}
<a class="thread-title" name="{{content.id}}" href="javascript:void(0)">{{content.displayed_title}}</a>
{{/thread}}
<div class="discussion-content-view">
<a name="{{content.id}}" style="width: 0; height: 0; padding: 0; border: none;"></a>
<div class="content-body {{content.type}}-body" id="content-body-{{content.id}}">{{content.displayed_body}}</div>
{{#thread}}
<div class="thread-tags">
{{#content.tags}}
<a class="thread-tag" href="{{##url_for_tags}}{{.}}{{/url_for_tags}}">{{.}}</a>
{{/content.tags}}
</div>
{{/thread}}
<div class="context">
{{#content.courseware_location}}
(this post is about <a href="../../jump_to/{{content.courseware_location}}">{{content.courseware_title}}</a>)
{{/content.courseware_location}}
</div>
<div class="info">
<div class="comment-time">
{{#content.updated}}
updated
{{/content.updated}}
<span class="timeago" title="{{content.updated_at}}">{{content.created_at}}</span> by
{{#content.anonymous}}
anonymous
{{/content.anonymous}}
{{^content.anonymous}}
<a href="{{##url_for_user}}{{content.user_id}}{{/url_for_user}}" class="{{#content.roles}}author-{{name}} {{/content.roles}}">{{content.username}}</a>
{{/content.anonymous}}
</div>
<div class="show-comments-wrapper">
{{#thread}}
{{#partial_comments}}
<a href="javascript:void(0)" class="discussion-show-comments first-time">Show all comments (<span class="comments-count">{{content.comments_count}}</span> total)</a>
{{/partial_comments}}
{{^partial_comments}}
<a href="javascript:void(0)" class="discussion-show-comments">Show <span class="comments-count">{{content.comments_count}}</span> {{##pluralize}}{{content.comments_count}} comment{{/pluralize}}</a>
{{/partial_comments}}
{{/thread}}
</div>
<ul class="discussion-actions">
{{^max_depth}}
<li><a class="discussion-link discussion-reply discussion-reply-{{content.type}}" href="javascript:void(0)">Reply</a></li>
{{/max_depth}}
{{#thread}}
<li><div class="follow-wrapper"><a class="discussion-link discussion-follow-thread" href="javascript:void(0)">Follow</a></div></li>
{{/thread}}
<li><a class="discussion-link discussion-permanent-link" href="{{content.permalink}}">Permanent Link</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<form class="discussion-content-edit discussion-comment-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<div class="comment-body-edit body-input">{{body}}</div>
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
<form class="discussion-content-edit discussion-thread-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<input type="text" class="thread-title-edit title-input" placeholder="Title" value="{{title}}"/>
<div class="thread-body-edit body-input">{{body}}</div>
<input class="thread-tags-edit" placeholder="Tags" value="{{tags}}" />
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
<div class="discussion-post local"> <div class="discussion-post local">
<div><a href="javascript:void(0)" class="dogear action-follow" data-tooltip="follow"></a></div> <div><a href="javascript:void(0)" class="dogear action-follow" data-tooltip="follow"></a></div>
<header> <header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span><span class="sr">votes (click to vote)</span></a> <a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class='votes-count-number'>{{votes.up_count}}</span> <span class="sr">votes (click to vote)</span></a>
<h3>{{title}}</h3> <h3>{{title}}</h3>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse"> <div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse">
<i class="icon icon-flag"></i><span class="flag-label">Flagged</span></div> <i class="icon icon-flag"></i><span class="flag-label">Flagged</span></div>
......
<form class="new-post-form collapsed" id="new-post-form" style="display: block; ">
<ul class="new-post-form-errors discussion-errors"></ul>
<input type="text" class="new-post-title title-input" placeholder="Title" />
<div class="new-post-similar-posts-wrapper" style="display: none"></div>
<div class="new-post-body reply-body"></div>
{{! TODO tags: Getting rid of tags for now. }}
{{! <input class="new-post-tags" placeholder="Tags" /> }}
<div class="post-options">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<label for="discussion-post-anonymously-${discussion_id}">post anonymously</label>
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-${discussion_id}" checked="">
<label for="discussion-auto-watch-${discussion_id}">follow this thread</label>
</div>
<div class="new-post-control post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="local"><a href="javascript:void(0)" class="dogear action-follow"></a></div> <div class="local"><a href="javascript:void(0)" class="dogear action-follow"></a></div>
<div class="discussion-post local"> <div class="discussion-post local">
<header> <header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span><span class="sr">votes (click to vote)</span></a> <a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class='votes-count-number'>{{votes.up_count}}</span> <span class="sr">votes (click to vote)</span></a>
<h3>{{title}}</h3> <h3>{{title}}</h3>
<p class="posted-details"> <p class="posted-details">
{{#user}} {{#user}}
......
<form class="discussion-reply-new">
<ul class="discussion-errors"></ul>
<div class="reply-body"></div>
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-{{id}}" />
<label for="discussion-post-anonymously-{{id}}">post anonymously</label>
{{#showWatchCheckbox}}
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-{{id}}" checked />
<label for="discussion-auto-watch-{{id}}">follow this thread</label>
{{/showWatchCheckbox}}
<br />
<div class="reply-post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
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