From c3dc40d7298b0ae6db4b091632c52e46e96ebc10 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 24 Feb 2014 14:45:28 -0500 Subject: [PATCH] Add UI for editing forum third-level content --- .../response_comment_show_view_spec.coffee | 12 ++ .../view/response_comment_view_spec.coffee | 109 +++++++++++++++++- .../view/thread_response_view_spec.coffee | 39 +++++++ .../static/coffee/src/discussion/utils.coffee | 19 ++- .../views/response_comment_edit_view.coffee | 25 ++++ .../views/response_comment_show_view.coffee | 7 +- .../views/response_comment_view.coffee | 56 +++++++-- .../views/thread_response_view.coffee | 11 ++ lms/static/sass/_discussion.scss | 50 +++----- .../discussion/_underscore_templates.html | 16 ++- 10 files changed, 286 insertions(+), 58 deletions(-) create mode 100644 common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee create mode 100644 common/static/coffee/src/discussion/views/response_comment_edit_view.coffee 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")}

      -
      ${"<%- body %>"}
      +
      ${"<%- body %>"}
      ${_("Cancel")} @@ -168,6 +168,8 @@ ${_("Report Misuse")}
    • +
      + ${_("Edit")}
      <% js_block = u""" interpolate( @@ -190,6 +192,18 @@
      + +