diff --git a/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee
index 2b918fb94f..c9e1467975 100644
--- a/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee
+++ b/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee
@@ -10,6 +10,8 @@ describe 'ResponseCommentShowView', ->
+
–posted <%- created_at %> by
<% if (obj.username) { %>
<%- username %>
@@ -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()
diff --git a/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee b/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee
index 9a0bc03d64..05bfe7ee40 100644
--- a/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee
+++ b/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee
@@ -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 """
+
+
+
+ """
+ @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)
diff --git a/common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee b/common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee
new file mode 100644
index 0000000000..9ae1b00dd5
--- /dev/null
+++ b/common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee
@@ -0,0 +1,39 @@
+describe 'ThreadResponseView', ->
+ beforeEach ->
+ setFixtures """
+
+
+ """
+ @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())
diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee
index ad10ccae38..b40c6d71f4 100644
--- a/common/static/coffee/src/discussion/utils.coffee
+++ b/common/static/coffee/src/discussion/utils.coffee
@@ -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($("").addClass("new-post-form-error").html(error)).show()
+ makeErrorElem = (message) ->
+ $("").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()
diff --git a/common/static/coffee/src/discussion/views/response_comment_edit_view.coffee b/common/static/coffee/src/discussion/views/response_comment_edit_view.coffee
new file mode 100644
index 0000000000..b2370fd021
--- /dev/null
+++ b/common/static/coffee/src/discussion/views/response_comment_edit_view.coffee
@@ -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
diff --git a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee
index 35f03a8b02..e08011fef9 100644
--- a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee
+++ b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee
@@ -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
diff --git a/common/static/coffee/src/discussion/views/response_comment_view.coffee b/common/static/coffee/src/discussion/views/response_comment_view.coffee
index dbf6a3d701..c38e4f985d 100644
--- a/common/static/coffee/src/discussion/views/response_comment_view.coffee
+++ b/common/static/coffee/src/discussion/views/response_comment_view.coffee
@@ -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()
diff --git a/common/static/coffee/src/discussion/views/thread_response_view.coffee b/common/static/coffee/src/discussion/views/thread_response_view.coffee
index 1403e8d02d..b299ae983e 100644
--- a/common/static/coffee/src/discussion/views/thread_response_view.coffee
+++ b/common/static/coffee/src/discussion/views/thread_response_view.coffee
@@ -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) =>
diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss
index 76234c7f66..43f2cf946c 100644
--- a/lms/static/sass/_discussion.scss
+++ b/lms/static/sass/_discussion.scss
@@ -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;
diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html
index aef968288f..e592efc9bf 100644
--- a/lms/templates/discussion/_underscore_templates.html
+++ b/lms/templates/discussion/_underscore_templates.html
@@ -154,7 +154,7 @@
${_("Editing response")}
${_("Cancel")}
@@ -168,6 +168,8 @@
${_("Report Misuse")}
+
<%
js_block = u"""
interpolate(
@@ -190,6 +192,18 @@
+
+