Add UI for editing forum third-level content
This commit is contained in:
@@ -10,6 +10,8 @@ describe 'ResponseCommentShowView', ->
|
||||
<i class="icon"></i><span class="flag-label"></span></div>
|
||||
<div style="display:none" class="discussion-delete-comment action-delete" data-role="comment-delete" data-tooltip="Delete Comment" role="button" aria-pressed="false" tabindex="0">
|
||||
<i class="icon icon-remove"></i><span class="sr delete-label">Delete Comment</span></div>
|
||||
<div style="display:none" class="discussion-edit-comment action-edit" data-tooltip="Edit Comment" role="button" tabindex="0">
|
||||
<i class="icon icon-pencil"></i><span class="sr">Edit Comment</span></div>
|
||||
<p class="posted-details">–posted <span class="timeago" title="<%- created_at %>"><%- created_at %></span> by
|
||||
<% if (obj.username) { %>
|
||||
<a href="<%- user_url %>" class="profile-link"><%- username %></a>
|
||||
@@ -70,4 +72,14 @@ describe 'ResponseCommentShowView', ->
|
||||
@view.bind "comment:_delete", triggerTarget
|
||||
@view.render()
|
||||
@view.$el.find('.action-delete').click()
|
||||
|
||||
describe 'comment edit', ->
|
||||
|
||||
it 'triggers comment:edit when the edit button is clicked', ->
|
||||
DiscussionUtil.loadRoles []
|
||||
@comment.updateInfo {ability: {'can_edit': true}}
|
||||
triggerTarget = jasmine.createSpy()
|
||||
@view.bind "comment:edit", triggerTarget
|
||||
@view.render()
|
||||
@view.$el.find(".action-edit").click()
|
||||
expect(triggerTarget).toHaveBeenCalled()
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
describe 'ResponseCommentView', ->
|
||||
beforeEach ->
|
||||
|
||||
window.$$course_id = 'edX/999/test'
|
||||
window.user = new DiscussionUser {id: '567'}
|
||||
DiscussionUtil.loadRoles []
|
||||
@comment = new Comment {
|
||||
id: '01234567',
|
||||
user_id: '567',
|
||||
course_id: 'edX/999/test',
|
||||
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']
|
||||
}
|
||||
@view = new ResponseCommentView({ model: @comment })
|
||||
spyOn(@view, "render")
|
||||
setFixtures """
|
||||
<script id="response-comment-show-template" type="text/template">
|
||||
<div id="response-comment-show-div"/>
|
||||
</script>
|
||||
<script id="response-comment-edit-template" type="text/template">
|
||||
<div id="response-comment-edit-div">
|
||||
<div class="edit-comment-body"><textarea/></div>
|
||||
<ul class="edit-comment-form-errors"/>
|
||||
</div>
|
||||
</script>
|
||||
<div id="response-comment-fixture"/>
|
||||
"""
|
||||
@view = new ResponseCommentView({ model: @comment, el: $("#response-comment-fixture") })
|
||||
spyOn(ResponseCommentShowView.prototype, "convertMath")
|
||||
spyOn(DiscussionUtil, "makeWmdEditor")
|
||||
@view.render()
|
||||
|
||||
makeEventSpy = () -> jasmine.createSpyObj('event', ['preventDefault', 'target'])
|
||||
|
||||
describe '_delete', ->
|
||||
beforeEach ->
|
||||
@comment.updateInfo {ability: {can_delete: true}}
|
||||
@event = jasmine.createSpyObj('event', ['preventDefault', 'target'])
|
||||
@event = makeEventSpy()
|
||||
spyOn(@comment, "remove")
|
||||
spyOn(@view.$el, "remove")
|
||||
|
||||
@@ -68,3 +86,82 @@ describe 'ResponseCommentView', ->
|
||||
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", makeEventSpy()
|
||||
expect(@view._delete).toHaveBeenCalled()
|
||||
@view.showView.trigger "comment:edit", makeEventSpy()
|
||||
expect(@view.edit).toHaveBeenCalled()
|
||||
expect(@view.$("#response-comment-show-div").length).toEqual(1)
|
||||
expect(@view.$("#response-comment-edit-div").length).toEqual(0)
|
||||
|
||||
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", makeEventSpy()
|
||||
expect(@view.update).toHaveBeenCalled()
|
||||
@view.editView.trigger "comment:cancel_edit", makeEventSpy()
|
||||
expect(@view.cancelEdit).toHaveBeenCalled()
|
||||
expect(@view.$("#response-comment-show-div").length).toEqual(0)
|
||||
expect(@view.$("#response-comment-edit-div").length).toEqual(1)
|
||||
|
||||
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"
|
||||
@view.$el.find(".edit-comment-body textarea").val(@updatedBody)
|
||||
spyOn(@view, 'cancelEdit')
|
||||
spyOn($, "ajax").andCallFake(
|
||||
(params) =>
|
||||
expect(params.url._parts.path).toEqual("/courses/edX/999/test/discussion/comments/01234567/update")
|
||||
expect(params.data.body).toEqual(@updatedBody)
|
||||
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(makeEventSpy())
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect(@view.model.get("body")).toEqual(@updatedBody)
|
||||
expect(@view.cancelEdit).toHaveBeenCalled()
|
||||
|
||||
it 'handles AJAX errors', ->
|
||||
originalBody = @comment.get("body")
|
||||
@ajaxSucceed = false
|
||||
@view.update(makeEventSpy())
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect(@view.model.get("body")).toEqual(originalBody)
|
||||
expect(@view.cancelEdit).not.toHaveBeenCalled()
|
||||
expect(@view.$(".edit-comment-form-errors *").length).toEqual(1)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
describe 'ThreadResponseView', ->
|
||||
beforeEach ->
|
||||
setFixtures """
|
||||
<script id="thread-response-template" type="text/template">
|
||||
<div/>
|
||||
</script>
|
||||
<div id="thread-response-fixture"/>
|
||||
"""
|
||||
@response = new Comment {
|
||||
children: [{}, {}]
|
||||
}
|
||||
@view = new ThreadResponseView({model: @response, el: $("#thread-response-fixture")})
|
||||
spyOn(ThreadResponseShowView.prototype, "render")
|
||||
spyOn(ResponseCommentView.prototype, "render")
|
||||
|
||||
describe 'renderComments', ->
|
||||
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())
|
||||
@@ -157,11 +157,20 @@ class @DiscussionUtil
|
||||
|
||||
@formErrorHandler: (errorsField) ->
|
||||
(xhr, textStatus, error) ->
|
||||
response = JSON.parse(xhr.responseText)
|
||||
if response.errors? and response.errors.length > 0
|
||||
errorsField.empty()
|
||||
for error in response.errors
|
||||
errorsField.append($("<li>").addClass("new-post-form-error").html(error)).show()
|
||||
makeErrorElem = (message) ->
|
||||
$("<li>").addClass("new-post-form-error").html(message)
|
||||
errorsField.empty().show()
|
||||
if xhr.status == 400
|
||||
response = JSON.parse(xhr.responseText)
|
||||
if response.errors? and response.errors.length > 0
|
||||
for error in response.errors
|
||||
errorsField.append(makeErrorElem(error))
|
||||
else
|
||||
errorsField.append(
|
||||
makeErrorElem(
|
||||
gettext("We had some trouble processing your request. Please try again.")
|
||||
)
|
||||
)
|
||||
|
||||
@clearFormErrors: (errorsField) ->
|
||||
errorsField.empty()
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
@@ -3,6 +3,7 @@ if Backbone?
|
||||
|
||||
events:
|
||||
"click .action-delete": "_delete"
|
||||
"click .action-edit": "edit"
|
||||
|
||||
tagName: "li"
|
||||
|
||||
@@ -14,6 +15,9 @@ if Backbone?
|
||||
can_delete:
|
||||
enable: -> @$(".action-delete").show()
|
||||
disable: -> @$(".action-delete").hide()
|
||||
editable:
|
||||
enable: -> @$(".action-edit").show()
|
||||
disable: -> @$(".action-edit").hide()
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#response-comment-show-template").html())
|
||||
@@ -68,4 +72,5 @@ if Backbone?
|
||||
updateModelDetails: =>
|
||||
@renderFlagged()
|
||||
|
||||
|
||||
edit: (event) =>
|
||||
@trigger "comment:edit", event
|
||||
|
||||
@@ -7,29 +7,37 @@ if Backbone?
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@createShowView()
|
||||
|
||||
render: ->
|
||||
@renderShowView()
|
||||
@
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
|
||||
@showView = new ResponseCommentShowView(model: @model)
|
||||
@showView.bind "comment:_delete", @_delete
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$el)
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
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()
|
||||
@@ -51,3 +59,27 @@ if Backbone?
|
||||
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()
|
||||
|
||||
@@ -53,6 +53,7 @@ if Backbone?
|
||||
|
||||
renderComments: ->
|
||||
comments = new Comments()
|
||||
@commentViews = []
|
||||
comments.comparator = (comment) ->
|
||||
comment.get('created_at')
|
||||
collectComments = (comment) ->
|
||||
@@ -69,6 +70,12 @@ if Backbone?
|
||||
view = new ResponseCommentView(model: comment)
|
||||
view.render()
|
||||
@$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) ->
|
||||
@@ -128,6 +135,9 @@ if Backbone?
|
||||
renderEditView: () ->
|
||||
@renderSubView(@editView)
|
||||
|
||||
cancelCommentEdits: () ->
|
||||
_.each(@commentViews, (view) -> view.cancelEdit())
|
||||
|
||||
hideCommentForm: () ->
|
||||
@$('.comment-form').closest('li').hide()
|
||||
|
||||
@@ -157,6 +167,7 @@ if Backbone?
|
||||
edit: (event) =>
|
||||
@createEditView()
|
||||
@renderEditView()
|
||||
@cancelCommentEdits()
|
||||
@hideCommentForm()
|
||||
|
||||
update: (event) =>
|
||||
|
||||
@@ -342,6 +342,10 @@ body.discussion {
|
||||
|
||||
}
|
||||
|
||||
.comments .edit-post-form h1 {
|
||||
@extend %t-title6;
|
||||
}
|
||||
|
||||
.new-post-form {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
@@ -539,13 +543,14 @@ body.discussion {
|
||||
height: 20px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.wmd-button > span {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url('/static/images/wmd-buttons.png');
|
||||
background-image: url('/static/images/wmd-buttons-transparent.png');
|
||||
background-position: 0px 0px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -1631,6 +1636,7 @@ body.discussion {
|
||||
> li {
|
||||
background: #f6f6f6;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: ($baseline/2) $baseline;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@@ -1667,7 +1673,7 @@ body.discussion {
|
||||
|
||||
.response-body {
|
||||
font-size: 13px;
|
||||
padding: $baseline/2 $baseline;
|
||||
margin-bottom: $baseline/2;
|
||||
|
||||
p + p {
|
||||
margin-top: 12px;
|
||||
@@ -1675,7 +1681,6 @@ body.discussion {
|
||||
}
|
||||
|
||||
.posted-details {
|
||||
margin: 0 $baseline $baseline/2;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -2529,51 +2534,30 @@ body.discussion {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.discussion-flag-abuse, .discussion-delete-comment {
|
||||
.discussion-flag-abuse, .discussion-delete-comment, .discussion-edit-comment {
|
||||
font-size: 12px;
|
||||
float:right;
|
||||
padding-right: 5px;
|
||||
margin-left: $baseline/2;
|
||||
font-style: italic;
|
||||
cursor:pointer;
|
||||
color: $dark-gray;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover, &:focus {
|
||||
@include transition(opacity .2s linear 0s);
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.flag-label {
|
||||
font-style: italic;
|
||||
margin-left: $baseline/4;
|
||||
}
|
||||
}
|
||||
|
||||
.notflagged .icon {
|
||||
display: block;
|
||||
color: #333;
|
||||
float: left;
|
||||
margin: 3px;
|
||||
width: 10px;
|
||||
height: 14px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.flagged .icon
|
||||
{
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 3px;
|
||||
width: 10px;
|
||||
height: 14px;
|
||||
padding-right: 3px;
|
||||
.flagged * {
|
||||
color: $pink;
|
||||
}
|
||||
|
||||
.flagged span {
|
||||
color: $pink;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.notflagged span {
|
||||
color: #333;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.response-count {
|
||||
margin-top: $baseline;
|
||||
padding: 0px 3*$baseline;
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<h1>${_("Editing response")}</h1>
|
||||
<ul class="edit-post-form-errors"></ul>
|
||||
<div class="form-row">
|
||||
<div class="edit-post-body" name="body">${"<%- body %>"}</div>
|
||||
<div class="edit-post-body" name="body" data-id="${'<%- id %>'}">${"<%- body %>"}</div>
|
||||
</div>
|
||||
<input type="submit" id="edit-response-submit"class="post-update" value="${_("Update response") | h}">
|
||||
<a href="#" class="post-cancel">${_("Cancel")}</a>
|
||||
@@ -168,6 +168,8 @@
|
||||
<i class="icon icon-flag"></i><span class="sr flag-label">${_("Report Misuse")}</span></div>
|
||||
<div style="display: none" class="discussion-delete-comment action-delete" data-tooltip="${_('Delete Comment') | h}" role="button" tabindex="0">
|
||||
<i class="icon icon-remove"></i><span class="sr">${_("Delete Comment")}</span></div>
|
||||
<div class="discussion-edit-comment action-edit" data-tooltip="${_('Edit') | h}" role="button" tabindex="0">
|
||||
<i class="icon icon-pencil"></i><span class="sr">${_("Edit")}</span></div>
|
||||
<%
|
||||
js_block = u"""
|
||||
interpolate(
|
||||
@@ -190,6 +192,18 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="response-comment-edit-template">
|
||||
<div class="edit-post-form">
|
||||
<h1>${_("Editing comment")}</h1>
|
||||
<ul class="edit-comment-form-errors"></ul>
|
||||
<div class="form-row">
|
||||
<div class="edit-comment-body" name="body" data-id="${'<%- id %>'}">${"<%- body %>"}</div>
|
||||
</div>
|
||||
<input type="submit" id="edit-comment-submit" class="post-update" value="${_("Update comment") | h}">
|
||||
<a href="#" class="post-cancel">${_("Cancel")}</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="thread-list-item-template">
|
||||
<a href="${'<%- id %>'}" data-id="${'<%- id %>'}">
|
||||
<span class="title">${"<%- title %>"}</span>
|
||||
|
||||
Reference in New Issue
Block a user