Unify forum thread views (and templates)

This commit is contained in:
Greg Price
2014-07-09 18:07:25 -04:00
parent ab2b7f344d
commit 7470e60004
16 changed files with 241 additions and 402 deletions

View File

@@ -150,21 +150,21 @@ describe "DiscussionThreadListView", ->
<div class="forum-nav"></div>
"""
@threads = [
makeThreadWithProps({
DiscussionViewSpecHelper.makeThreadWithProps({
id: "1",
title: "Thread1",
votes: {up_count: '20'},
comments_count: 1,
created_at: '2013-04-03T20:08:39Z',
}),
makeThreadWithProps({
DiscussionViewSpecHelper.makeThreadWithProps({
id: "2",
title: "Thread2",
votes: {up_count: '42'},
comments_count: 2,
created_at: '2013-04-03T20:07:39Z',
}),
makeThreadWithProps({
DiscussionViewSpecHelper.makeThreadWithProps({
id: "3",
title: "Thread3",
votes: {up_count: '12'},
@@ -179,20 +179,8 @@ describe "DiscussionThreadListView", ->
@view = new DiscussionThreadListView({collection: @discussion, el: $(".forum-nav")})
@view.render()
makeThreadWithProps = (props) ->
# Minimal set of properties necessary for rendering
thread = {
id: "dummy_id",
pinned: false,
endorsed: false,
votes: {up_count: '0'},
unread_comments_count: 0,
comments_count: 0,
}
$.extend(thread, props)
renderSingleThreadWithProps = (props) ->
makeView(new Discussion([new Thread(makeThreadWithProps(props))])).render()
makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render()
makeView = (discussion) ->
return new DiscussionThreadListView(

View File

@@ -1,106 +0,0 @@
describe "DiscussionThreadInlineView", ->
beforeEach ->
setFixtures(
"""
<script type="text/template" id="_inline_thread">
<article class="discussion-article">
<div class="non-cohorted-indicator"/>
<div class="post-body"/>
<div class="post-extended-content">
<div class="response-count"/>
<ol class="responses"/>
<div class="response-pagination"/>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="expand-post">Expand</a>
<a href="javascript:void(0)" class="collapse-post">Collapse</a>
</div>
</article>
</script>
<script type="text/template" id="_inline_thread_cohorted">
<article class="discussion-article">
<div class="cohorted-indicator"/>
<div class="post-body"/>
<div class="post-extended-content">
<div class="response-count"/>
<ol class="responses"/>
<div class="response-pagination"/>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="expand-post">Expand</a>
<a href="javascript:void(0)" class="collapse-post">Collapse</a>
</div>
</article>
</script>
<div class="thread-fixture"/>
"""
)
@threadData = {
id: "dummy",
body: "dummy body",
abuse_flaggers: [],
votes: {up_count: "42"}
}
@thread = new Thread(@threadData)
@view = new DiscussionThreadInlineView({ model: @thread })
@view.setElement($(".thread-fixture"))
spyOn($, "ajax")
# Avoid unnecessary boilerplate
spyOn(@view.showView, "render")
spyOn(@view.showView, "convertMath")
spyOn(@view, "makeWmdEditor")
spyOn(DiscussionThreadView.prototype, "renderResponse")
assertContentVisible = (view, selector, visible) ->
content = view.$el.find(selector)
expect(content.length).toEqual(1)
expect(content.is(":visible")).toEqual(visible)
assertExpandedContentVisible = (view, expanded) ->
expect(view.$el.hasClass("expanded")).toEqual(expanded)
assertContentVisible(view, ".post-extended-content", expanded)
assertContentVisible(view, ".expand-post", not expanded)
assertContentVisible(view, ".collapse-post", expanded)
describe "render", ->
it "uses the cohorted template if cohorted", ->
@view.model.set({group_id: 1})
@view.render()
expect(@view.$el.find(".cohorted-indicator").length).toEqual(1)
it "uses the non-cohorted template if not cohorted", ->
@view.render()
expect(@view.$el.find(".non-cohorted-indicator").length).toEqual(1)
it "shows content that should be visible when collapsed", ->
@view.render()
assertExpandedContentVisible(@view, false)
it "does not render any responses by default", ->
@view.render()
expect($.ajax).not.toHaveBeenCalled()
expect(@view.$el.find(".responses li").length).toEqual(0)
describe "expand/collapse", ->
it "shows/hides appropriate content", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
@view.render()
@view.expandPost()
assertExpandedContentVisible(@view, true)
@view.collapsePost()
assertExpandedContentVisible(@view, false)
it "switches between the abbreviated and full body", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
@thread.set("body", new Array(100).join("test "))
@view.abbreviateBody()
expect(@thread.get("body")).not.toEqual(@thread.get("abbreviatedBody"))
@view.render()
@view.expandPost()
expect(@view.$el.find(".post-body").text()).toEqual(@thread.get("body"))
expect(@view.showView.convertMath).toHaveBeenCalled()
@view.showView.convertMath.reset()
@view.collapsePost()
expect(@view.$el.find(".post-body").text()).toEqual(@thread.get("abbreviatedBody"))
expect(@view.showView.convertMath).toHaveBeenCalled()

View File

@@ -4,77 +4,144 @@ describe "DiscussionThreadView", ->
"""
<script type="text/template" id="thread-template">
<article class="discussion-article">
<div class="response-count"/>
<ol class="responses"/>
<div class="response-pagination"/>
<div class="thread-content-wrapper"></div>
<div class="post-extended-content">
<div class="response-count"></div>
<ol class="responses"></ol>
<div class="response-pagination"></div>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="forum-thread-expand">Expand</a>
<a href="javascript:void(0)" class="forum-thread-collapse">Collapse</a>
</div>
</article>
</script>
<script type="text/template" id="thread-show-template">
<div class="discussion-post">
<div class="post-body"><%- body %></div>
</div>
</script>
<div class="thread-fixture"/>
"""
)
jasmine.Clock.useMock()
@threadData = {
id: "dummy"
}
@threadData = DiscussionViewSpecHelper.makeThreadWithProps({})
@thread = new Thread(@threadData)
@view = new DiscussionThreadView({ model: @thread })
@view.setElement($(".thread-fixture"))
spyOn($, "ajax")
# Avoid unnecessary boilerplate
spyOn(@view.showView, "render")
spyOn(@view, "makeWmdEditor")
spyOn(DiscussionThreadShowView.prototype, "convertMath")
spyOn(DiscussionContentView.prototype, "makeWmdEditor")
spyOn(DiscussionThreadView.prototype, "renderResponse")
describe "response count and pagination", ->
renderWithContent = (view, content) ->
DiscussionViewSpecHelper.setNextResponseContent(content)
view.render()
jasmine.Clock.tick(100)
assertContentVisible = (view, selector, visible) ->
content = view.$el.find(selector)
expect(content.length).toBeGreaterThan(0)
content.each (i, elem) ->
expect($(elem).is(":visible")).toEqual(visible)
assertRenderedCorrectly = (view, countText, displayCountText, buttonText) ->
expect(view.$el.find(".response-count").text()).toEqual(countText)
if displayCountText
expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText)
else
expect(view.$el.find(".response-display-count").length).toEqual(0)
if buttonText
expect(view.$el.find(".load-response-button").text()).toEqual(buttonText)
else
expect(view.$el.find(".load-response-button").length).toEqual(0)
assertExpandedContentVisible = (view, expanded) ->
expect(view.$el.hasClass("expanded")).toEqual(expanded)
assertContentVisible(view, ".post-extended-content", expanded)
assertContentVisible(view, ".forum-thread-expand", not expanded)
assertContentVisible(view, ".forum-thread-collapse", expanded)
it "correctly render for a thread with no responses", ->
renderWithContent(@view, {resp_total: 0, children: []})
assertRenderedCorrectly(@view, "0 responses", null, null)
describe "tab mode", ->
beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $(".thread-fixture"), mode: "tab"})
it "correctly render for a thread with one response", ->
renderWithContent(@view, {resp_total: 1, children: [{}]})
assertRenderedCorrectly(@view, "1 response", "Showing all responses", null)
describe "response count and pagination", ->
renderWithContent = (view, content) ->
DiscussionViewSpecHelper.setNextResponseContent(content)
view.render()
jasmine.Clock.tick(100)
it "correctly render for a thread with one additional page", ->
renderWithContent(@view, {resp_total: 2, children: [{}]})
assertRenderedCorrectly(@view, "2 responses", "Showing first response", "Load all responses")
assertRenderedCorrectly = (view, countText, displayCountText, buttonText) ->
expect(view.$el.find(".response-count").text()).toEqual(countText)
if displayCountText
expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText)
else
expect(view.$el.find(".response-display-count").length).toEqual(0)
if buttonText
expect(view.$el.find(".load-response-button").text()).toEqual(buttonText)
else
expect(view.$el.find(".load-response-button").length).toEqual(0)
it "correctly render for a thread with multiple additional pages", ->
renderWithContent(@view, {resp_total: 111, children: [{}, {}]})
assertRenderedCorrectly(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses")
it "correctly render for a thread with no responses", ->
renderWithContent(@view, {resp_total: 0, children: []})
assertRenderedCorrectly(@view, "0 responses", null, null)
describe "on clicking the load more button", ->
beforeEach ->
renderWithContent(@view, {resp_total: 5, children: [{}]})
assertRenderedCorrectly(@view, "5 responses", "Showing first response", "Load all responses")
it "correctly re-render when all threads have loaded", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 5, children: [{}, {}, {}, {}]})
@view.$el.find(".load-response-button").click()
assertRenderedCorrectly(@view, "5 responses", "Showing all responses", null)
it "correctly render for a thread with one response", ->
renderWithContent(@view, {resp_total: 1, children: [{}]})
assertRenderedCorrectly(@view, "1 response", "Showing all responses", null)
it "correctly re-render when one page remains", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 42, children: [{}, {}]})
@view.$el.find(".load-response-button").click()
assertRenderedCorrectly(@view, "42 responses", "Showing first 3 responses", "Load all responses")
it "correctly render for a thread with one additional page", ->
renderWithContent(@view, {resp_total: 2, children: [{}]})
assertRenderedCorrectly(@view, "2 responses", "Showing first response", "Load all responses")
it "correctly re-render when multiple pages remain", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 111, children: [{}, {}]})
@view.$el.find(".load-response-button").click()
assertRenderedCorrectly(@view, "111 responses", "Showing first 3 responses", "Load next 100 responses")
it "correctly render for a thread with multiple additional pages", ->
renderWithContent(@view, {resp_total: 111, children: [{}, {}]})
assertRenderedCorrectly(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses")
describe "on clicking the load more button", ->
beforeEach ->
renderWithContent(@view, {resp_total: 5, children: [{}]})
assertRenderedCorrectly(@view, "5 responses", "Showing first response", "Load all responses")
it "correctly re-render when all threads have loaded", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 5, children: [{}, {}, {}, {}]})
@view.$el.find(".load-response-button").click()
assertRenderedCorrectly(@view, "5 responses", "Showing all responses", null)
it "correctly re-render when one page remains", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 42, children: [{}, {}]})
@view.$el.find(".load-response-button").click()
assertRenderedCorrectly(@view, "42 responses", "Showing first 3 responses", "Load all responses")
it "correctly re-render when multiple pages remain", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 111, children: [{}, {}]})
@view.$el.find(".load-response-button").click()
assertRenderedCorrectly(@view, "111 responses", "Showing first 3 responses", "Load next 100 responses")
describe "inline mode", ->
beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $(".thread-fixture"), mode: "inline"})
describe "render", ->
it "shows content that should be visible when collapsed", ->
@view.render()
assertExpandedContentVisible(@view, false)
it "does not render any responses by default", ->
@view.render()
expect($.ajax).not.toHaveBeenCalled()
expect(@view.$el.find(".responses li").length).toEqual(0)
describe "expand/collapse", ->
it "shows/hides appropriate content", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
@view.render()
@view.expand()
assertExpandedContentVisible(@view, true)
@view.collapse()
assertExpandedContentVisible(@view, false)
it "switches between the abbreviated and full body", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
longBody = new Array(100).join("test ")
expectedAbbreviation = DiscussionUtil.abbreviateString(longBody, 140)
@thread.set("body", longBody)
@view.render()
expect($(".post-body").text()).toEqual(expectedAbbreviation)
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
DiscussionThreadShowView.prototype.convertMath.reset()
@view.expand()
expect($(".post-body").text()).toEqual(longBody)
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
DiscussionThreadShowView.prototype.convertMath.reset()
@view.collapse()
expect($(".post-body").text()).toEqual(expectedAbbreviation)
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()

View File

@@ -1,4 +1,18 @@
class @DiscussionViewSpecHelper
@makeThreadWithProps = (props) ->
# Minimal set of properties necessary for rendering
thread = {
id: "dummy_id",
pinned: false,
endorsed: false,
votes: {up_count: '0'},
unread_comments_count: 0,
comments_count: 0,
abuse_flaggers: [],
body: ""
}
$.extend(thread, props)
@expectVoteRendered = (view, voted) ->
button = view.$el.find(".vote-btn")
if voted

View File

@@ -101,7 +101,7 @@ if Backbone?
@newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) ->
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread
new DiscussionThreadView el: @$("article#thread_#{thread.id}"), model: thread, mode: "inline"
_.each @threadviews, (dtv) -> dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@newPostView = new NewPostView(
@@ -124,7 +124,7 @@ if Backbone?
# 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 DiscussionThreadInlineView el: article, model: thread
threadView = new DiscussionThreadView el: article, model: thread, mode: "inline"
threadView.render()
@threadviews.unshift threadView

View File

@@ -50,7 +50,7 @@ if Backbone?
if(@newPost.is(":visible"))
@newPost.fadeOut()
@main = new DiscussionThreadView(el: $(".forum-content"), model: @thread)
@main = new DiscussionThreadView(el: $(".forum-content"), model: @thread, mode: "tab")
@main.render()
@main.on "thread:responses:rendered", =>
@nav.updateSidebar()

View File

@@ -23,13 +23,18 @@ if Backbone?
$: (selector) ->
@$el.find(selector)
initialize: ->
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)
@model.on "change", @updateModelDetails
renderTemplate: ->
@template = _.template($("#thread-show-template").html())
@template(@model.toJSON())
context = @model.toJSON()
context.mode = @mode
@template(context)
render: ->
@$el.html(@renderTemplate())
@@ -170,13 +175,3 @@ if Backbone?
highlight: (el) ->
if el.html()
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
class @DiscussionThreadInlineShowView extends DiscussionThreadShowView
renderTemplate: ->
@template = DiscussionUtil.getTemplate('_inline_thread_show')
params = @model.toJSON()
if @model.get('username')?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params)

View File

@@ -7,14 +7,20 @@ if Backbone?
events:
"click .discussion-submit-post": "submitComment"
"click .add-response-btn": "scrollToAddResponse"
"click .forum-thread-expand": "expand"
"click .forum-thread-collapse": "collapse"
$: (selector) ->
@$el.find(selector)
initialize: ->
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)
@createShowView()
@responses = new Comments()
@loadedResponses = false
renderTemplate: ->
@template = _.template($("#thread-template").html())
@@ -31,10 +37,44 @@ if Backbone?
@makeWmdEditor "reply-body"
@renderAddResponseButton()
@responses.on("add", @renderResponse)
# Without a delay, jQuery doesn't add the loading extension defined in
# utils.coffee before safeAjax is invoked, which results in an error
setTimeout((=> @loadInitialResponses()), 100)
@
if @mode == "tab"
# Without a delay, jQuery doesn't add the loading extension defined in
# utils.coffee before safeAjax is invoked, which results in an error
setTimeout((=> @loadInitialResponses()), 100)
@$(".post-tools").hide()
else # mode == "inline"
@collapse()
expand: (event) ->
if event
event.preventDefault()
@$el.addClass("expanded")
@$el.find(".post-body").html(@model.get("body"))
@showView.convertMath()
@$el.find(".forum-thread-expand").hide()
@$el.find(".forum-thread-collapse").show()
@$el.find(".post-extended-content").show()
if not @loadedResponses
@loadInitialResponses()
collapse: (event) ->
if event
event.preventDefault()
@$el.removeClass("expanded")
@$el.find(".post-body").html(@getAbbreviatedBody())
@showView.convertMath()
@$el.find(".forum-thread-expand").show()
@$el.find(".forum-thread-collapse").hide()
@$el.find(".post-extended-content").hide()
getAbbreviatedBody: ->
cached = @model.get("abbreviatedBody")
if cached
cached
else
abbreviated = DiscussionUtil.abbreviateString @model.get("body"), 140
@model.set("abbreviatedBody", abbreviated)
abbreviated
cleanup: ->
if @responsesRequest?
@@ -56,6 +96,7 @@ if Backbone?
@responses.add(data['content']['children'])
@renderResponseCountAndPagination(data['content']['resp_total'])
@trigger "thread:responses:rendered"
@loadedResponses = true
error: (xhr) =>
if xhr.status == 404
DiscussionUtil.discussionAlert(
@@ -208,6 +249,7 @@ if Backbone?
@model.set
title: newTitle
body: newBody
@model.unset("abbreviatedBody")
@createShowView()
@renderShowView()
@@ -231,9 +273,6 @@ if Backbone?
renderEditView: () ->
@renderSubView(@editView)
getShowViewClass: () ->
return DiscussionThreadShowView
createShowView: () ->
if @editView?
@@ -241,8 +280,7 @@ if Backbone?
@editView.$el.empty()
@editView = null
showViewClass = @getShowViewClass()
@showView = new showViewClass(model: @model)
@showView = new DiscussionThreadShowView({model: @model, mode: @mode})
@showView.bind "thread:_delete", @_delete
@showView.bind "thread:edit", @edit

View File

@@ -1,65 +0,0 @@
if Backbone?
class @DiscussionThreadInlineView extends DiscussionThreadView
expanded = false
events:
"click .discussion-submit-post": "submitComment"
"click .expand-post": "expandPost"
"click .collapse-post": "collapsePost"
"click .add-response-btn": "scrollToAddResponse"
initialize: ->
super()
renderTemplate: () ->
if @model.has('group_id')
@template = DiscussionUtil.getTemplate("_inline_thread_cohorted")
else
@template = DiscussionUtil.getTemplate("_inline_thread")
if not @model.has('abbreviatedBody')
@abbreviateBody()
params = @model.toJSON()
Mustache.render(@template, params)
render: () ->
super()
@$el.find('.post-extended-content').hide()
@$el.find('.collapse-post').hide()
getShowViewClass: () ->
return DiscussionThreadInlineShowView
loadInitialResponses: () ->
if @expanded
super()
abbreviateBody: ->
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
@model.set('abbreviatedBody', abbreviated)
expandPost: (event) =>
@$el.addClass('expanded')
@$el.find('.post-body').html(@model.get('body'))
@showView.convertMath()
@$el.find('.expand-post').css('display', 'none')
@$el.find('.collapse-post').css('display', 'block')
@$el.find('.post-extended-content').show()
if not @expanded
@expanded = true
@loadInitialResponses()
collapsePost: (event) ->
curScroll = $(window).scrollTop()
postTop = @$el.offset().top
if postTop < curScroll
$('html, body').animate({scrollTop: postTop})
@$el.removeClass('expanded')
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
@showView.convertMath()
@$el.find('.expand-post').css('display', 'block')
@$el.find('.collapse-post').css('display', 'none')
@$el.find('.post-extended-content').hide()
createEditView: () ->
super()
@editView.bind "thread:update", @abbreviateBody