Commit 786fc806 by jsa

Add topic selection to inline thread editing, fixing errors

Co-Authored-By: Brian Talbot <btalbot@edx.org>

TNL-549
parent 75908924
...@@ -19,6 +19,23 @@ class @DiscussionSpecHelper ...@@ -19,6 +19,23 @@ class @DiscussionSpecHelper
{always: ->} {always: ->}
) )
@makeEventSpy = () ->
jasmine.createSpyObj('event', ['preventDefault', 'target'])
@makeCourseSettings = (is_cohorted=true) ->
new DiscussionCourseSettings(
category_map:
children: ['Test Topic', 'Other Topic']
entries:
'Test Topic':
is_cohorted: is_cohorted
id: 'test_topic'
'Other Topic':
is_cohorted: is_cohorted
id: 'other_topic'
is_cohorted: is_cohorted
)
@setUnderscoreFixtures = -> @setUnderscoreFixtures = ->
for templateName in ['thread-show'] for templateName in ['thread-show']
templateFixture = readFixtures('templates/discussion/' + templateName + '.underscore') templateFixture = readFixtures('templates/discussion/' + templateName + '.underscore')
......
(function() { (function() {
'use strict'; 'use strict';
describe('DiscussionThreadEditView', function() { describe('DiscussionThreadEditView', function() {
var testUpdate, testCancel;
beforeEach(function() { beforeEach(function() {
DiscussionSpecHelper.setUpGlobals(); DiscussionSpecHelper.setUpGlobals();
DiscussionSpecHelper.setUnderscoreFixtures(); DiscussionSpecHelper.setUnderscoreFixtures();
spyOn(DiscussionUtil, 'makeWmdEditor'); spyOn(DiscussionUtil, 'makeWmdEditor');
this.threadData = DiscussionViewSpecHelper.makeThreadWithProps(); this.threadData = DiscussionViewSpecHelper.makeThreadWithProps({
this.thread = new Thread(this.threadData); 'commentable_id': 'test_topic',
this.course_settings = new DiscussionCourseSettings({ 'title': 'test thread title'
'category_map': {
'children': ['Topic'],
'entries': {
'Topic': {
'is_cohorted': true,
'id': 'topic'
}
}
},
'is_cohorted': true
}); });
this.thread = new Thread(this.threadData);
this.course_settings = DiscussionSpecHelper.makeCourseSettings();
this.createEditView = function (options) { this.createEditView = function (options) {
options = _.extend({ options = _.extend({
container: $('#fixture-element'), container: $('#fixture-element'),
model: this.thread, model: this.thread,
mode: 'tab', mode: 'tab',
topicId: 'dummy_id',
threadType: 'question',
course_settings: this.course_settings course_settings: this.course_settings
}, options); }, options);
this.view = new DiscussionThreadEditView(options); this.view = new DiscussionThreadEditView(options);
...@@ -34,36 +26,58 @@ ...@@ -34,36 +26,58 @@
}; };
}); });
it('can save new data correctly', function() { testUpdate = function(view, thread) {
var view;
spyOn($, 'ajax').andCallFake(function(params) { spyOn($, 'ajax').andCallFake(function(params) {
expect(params.url.path()).toEqual(DiscussionUtil.urlFor('update_thread', 'dummy_id')); expect(params.url.path()).toEqual(DiscussionUtil.urlFor('update_thread', 'dummy_id'));
if (view.isTabMode()) {
// TODO remove the tabMode condition, depends on #5554 / TNL-606
expect(params.data.thread_type).toBe('discussion'); expect(params.data.thread_type).toBe('discussion');
expect(params.data.commentable_id).toBe('topic'); }
expect(params.data.title).toBe('new_title'); expect(params.data.commentable_id).toBe('other_topic');
expect(params.data.title).toBe('changed thread title');
params.success(); params.success();
return {always: function() {}}; return {always: function() {}};
}); });
this.createEditView(); view.$el.find('a.topic-title')[1].click(); // set new topic
this.view.$el.find('a.topic-title').first().click(); // set new topic view.$('.edit-post-title').val('changed thread title'); // set new title
this.view.$('.edit-post-title').val('new_title'); // set new title view.$("label[for$='post-type-discussion']").click(); // set new thread type
this.view.$("label[for$='post-type-discussion']").click(); // set new thread type view.$('.post-update').click();
this.view.$('.post-update').click();
expect($.ajax).toHaveBeenCalled(); expect($.ajax).toHaveBeenCalled();
expect(this.thread.get('title')).toBe('new_title'); expect(thread.get('title')).toBe('changed thread title');
expect(this.thread.get('commentable_id')).toBe('topic'); if (view.isTabMode()) {
expect(this.thread.get('thread_type')).toBe('discussion'); // TODO remove the tabMode condition, depends on #5554 / TNL-606
expect(this.thread.get('courseware_title')).toBe('Topic'); expect(thread.get('thread_type')).toBe('discussion');
}
expect(thread.get('commentable_id')).toBe('other_topic');
expect(thread.get('courseware_title')).toBe('Other Topic');
expect(view.$('.edit-post-title')).toHaveValue('');
expect(view.$('.wmd-preview p')).toHaveText('');
}
it('can save new data correctly in tab mode', function() {
this.createEditView();
testUpdate(this.view, this.thread);
});
expect(this.view.$('.edit-post-title')).toHaveValue(''); it('can save new data correctly in inline mode', function() {
expect(this.view.$('.wmd-preview p')).toHaveText(''); this.createEditView({"mode": "inline"});
testUpdate(this.view, this.thread);
}); });
it('can close the view', function() { testCancel = function(view) {
this.createEditView(); view.$('.post-cancel').click();
this.view.$('.post-cancel').click();
expect($('.edit-post-form')).not.toExist(); expect($('.edit-post-form')).not.toExist();
}
it('can close the view in tab mode', function() {
this.createEditView();
testCancel(this.view);
});
it('can close the view in inline mode', function() {
this.createEditView({"mode": "inline"});
testCancel(this.view);
}); });
}); });
}).call(this); }).call(this);
...@@ -11,6 +11,7 @@ describe "DiscussionThreadView", -> ...@@ -11,6 +11,7 @@ describe "DiscussionThreadView", ->
# Avoid unnecessary boilerplate # Avoid unnecessary boilerplate
spyOn(DiscussionThreadShowView.prototype, "convertMath") spyOn(DiscussionThreadShowView.prototype, "convertMath")
spyOn(DiscussionContentView.prototype, "makeWmdEditor") spyOn(DiscussionContentView.prototype, "makeWmdEditor")
spyOn(DiscussionUtil, "makeWmdEditor")
spyOn(ThreadResponseView.prototype, "renderShowView") spyOn(ThreadResponseView.prototype, "renderShowView")
renderWithContent = (view, content) -> renderWithContent = (view, content) ->
...@@ -46,7 +47,12 @@ describe "DiscussionThreadView", -> ...@@ -46,7 +47,12 @@ describe "DiscussionThreadView", ->
threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed}) threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed})
thread = new Thread(threadData) thread = new Thread(threadData)
discussion = new Discussion(thread) discussion = new Discussion(thread)
view = new DiscussionThreadView({ model: thread, el: $("#fixture-element"), mode: mode}) view = new DiscussionThreadView(
model: thread
el: $("#fixture-element")
mode: mode
course_settings: DiscussionSpecHelper.makeCourseSettings()
)
renderWithContent(view, {resp_total: 1, children: [{}]}) renderWithContent(view, {resp_total: 1, children: [{}]})
if mode == "inline" if mode == "inline"
view.expand() view.expand()
...@@ -70,7 +76,12 @@ describe "DiscussionThreadView", -> ...@@ -70,7 +76,12 @@ describe "DiscussionThreadView", ->
describe "tab mode", -> describe "tab mode", ->
beforeEach -> beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "tab"}) @view = new DiscussionThreadView(
model: @thread
el: $("#fixture-element")
mode: "tab"
course_settings: DiscussionSpecHelper.makeCourseSettings()
)
describe "response count and pagination", -> describe "response count and pagination", ->
it "correctly render for a thread with no responses", -> it "correctly render for a thread with no responses", ->
...@@ -111,7 +122,12 @@ describe "DiscussionThreadView", -> ...@@ -111,7 +122,12 @@ describe "DiscussionThreadView", ->
describe "inline mode", -> describe "inline mode", ->
beforeEach -> beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "inline"}) @view = new DiscussionThreadView(
model: @thread
el: $("#fixture-element")
mode: "inline"
course_settings: DiscussionSpecHelper.makeCourseSettings()
)
describe "render", -> describe "render", ->
it "shows content that should be visible when collapsed", -> it "shows content that should be visible when collapsed", ->
...@@ -178,11 +194,26 @@ describe "DiscussionThreadView", -> ...@@ -178,11 +194,26 @@ describe "DiscussionThreadView", ->
expect($(".post-body").text()).toEqual(maliciousAbbreviation) expect($(".post-body").text()).toEqual(maliciousAbbreviation)
expect($(".post-body").html()).not.toContain("<script") expect($(".post-body").html()).not.toContain("<script")
it "re-renders the show view correctly when leaving the edit view", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
@view.render()
@view.expand()
assertExpandedContentVisible(@view, true)
@view.edit()
assertContentVisible(@view, ".edit-post-body", true)
expect(@view.$el.find(".post-actions-list").length).toBe(0)
@view.closeEditView(DiscussionSpecHelper.makeEventSpy())
expect(@view.$el.find(".edit-post-body").length).toBe(0)
assertContentVisible(@view, ".post-actions-list", true)
describe "for question threads", -> describe "for question threads", ->
beforeEach -> beforeEach ->
@thread.set("thread_type", "question") @thread.set("thread_type", "question")
@view = new DiscussionThreadView( @view = new DiscussionThreadView(
{model: @thread, el: $("#fixture-element"), mode: "tab"} model: @thread
el: $("#fixture-element")
mode: "tab"
course_settings: DiscussionSpecHelper.makeCourseSettings()
) )
renderTestCase = (view, numEndorsed, numNonEndorsed) -> renderTestCase = (view, numEndorsed, numNonEndorsed) ->
......
...@@ -17,12 +17,10 @@ describe 'ResponseCommentView', -> ...@@ -17,12 +17,10 @@ describe 'ResponseCommentView', ->
spyOn(DiscussionUtil, "makeWmdEditor") spyOn(DiscussionUtil, "makeWmdEditor")
@view.render() @view.render()
makeEventSpy = () -> jasmine.createSpyObj('event', ['preventDefault', 'target'])
describe '_delete', -> describe '_delete', ->
beforeEach -> beforeEach ->
@comment.updateInfo {ability: {can_delete: true}} @comment.updateInfo {ability: {can_delete: true}}
@event = makeEventSpy() @event = DiscussionSpecHelper.makeEventSpy()
spyOn(@comment, "remove") spyOn(@comment, "remove")
spyOn(@view.$el, "remove") spyOn(@view.$el, "remove")
...@@ -81,9 +79,9 @@ describe 'ResponseCommentView', -> ...@@ -81,9 +79,9 @@ describe 'ResponseCommentView', ->
# Without calling renderEditView first, renderShowView is a no-op # Without calling renderEditView first, renderShowView is a no-op
@view.renderEditView() @view.renderEditView()
@view.renderShowView() @view.renderShowView()
@view.showView.trigger "comment:_delete", makeEventSpy() @view.showView.trigger "comment:_delete", DiscussionSpecHelper.makeEventSpy()
expect(@view._delete).toHaveBeenCalled() expect(@view._delete).toHaveBeenCalled()
@view.showView.trigger "comment:edit", makeEventSpy() @view.showView.trigger "comment:edit", DiscussionSpecHelper.makeEventSpy()
expect(@view.edit).toHaveBeenCalled() expect(@view.edit).toHaveBeenCalled()
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).not.toHaveClass("edit-post-form") expect(@view.$(".edit-post-form#comment_#{@comment.id}")).not.toHaveClass("edit-post-form")
...@@ -92,9 +90,9 @@ describe 'ResponseCommentView', -> ...@@ -92,9 +90,9 @@ describe 'ResponseCommentView', ->
spyOn(@view, "update") spyOn(@view, "update")
spyOn(@view, "cancelEdit") spyOn(@view, "cancelEdit")
@view.renderEditView() @view.renderEditView()
@view.editView.trigger "comment:update", makeEventSpy() @view.editView.trigger "comment:update", DiscussionSpecHelper.makeEventSpy()
expect(@view.update).toHaveBeenCalled() expect(@view.update).toHaveBeenCalled()
@view.editView.trigger "comment:cancel_edit", makeEventSpy() @view.editView.trigger "comment:cancel_edit", DiscussionSpecHelper.makeEventSpy()
expect(@view.cancelEdit).toHaveBeenCalled() expect(@view.cancelEdit).toHaveBeenCalled()
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).toHaveClass("edit-post-form") expect(@view.$(".edit-post-form#comment_#{@comment.id}")).toHaveClass("edit-post-form")
...@@ -138,7 +136,7 @@ describe 'ResponseCommentView', -> ...@@ -138,7 +136,7 @@ describe 'ResponseCommentView', ->
it 'calls the update endpoint correctly and displays the show view on success', -> it 'calls the update endpoint correctly and displays the show view on success', ->
@ajaxSucceed = true @ajaxSucceed = true
@view.update(makeEventSpy()) @view.update(DiscussionSpecHelper.makeEventSpy())
expect($.ajax).toHaveBeenCalled() expect($.ajax).toHaveBeenCalled()
expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update') expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update')
expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody) expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody)
...@@ -148,7 +146,7 @@ describe 'ResponseCommentView', -> ...@@ -148,7 +146,7 @@ describe 'ResponseCommentView', ->
it 'handles AJAX errors', -> it 'handles AJAX errors', ->
originalBody = @comment.get("body") originalBody = @comment.get("body")
@ajaxSucceed = false @ajaxSucceed = false
@view.update(makeEventSpy()) @view.update(DiscussionSpecHelper.makeEventSpy())
expect($.ajax).toHaveBeenCalled() expect($.ajax).toHaveBeenCalled()
expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update') expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update')
expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody) expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody)
......
...@@ -98,7 +98,7 @@ if Backbone? ...@@ -98,7 +98,7 @@ if Backbone?
@$el.append($discussion) @$el.append($discussion)
@newPostForm = $('.new-post-article') @newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) -> @threadviews = @discussion.map (thread) =>
new DiscussionThreadView( new DiscussionThreadView(
el: @$("article#thread_#{thread.id}"), el: @$("article#thread_#{thread.id}"),
model: thread, model: thread,
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
this.mode = options.mode || 'inline'; this.mode = options.mode || 'inline';
this.course_settings = options.course_settings; this.course_settings = options.course_settings;
this.threadType = this.model.get('thread_type'); this.threadType = this.model.get('thread_type');
this.topicId = options.topicId; this.topicId = this.model.get('commentable_id');
_.bindAll(this); _.bindAll(this);
return this; return this;
}, },
...@@ -32,12 +32,12 @@ ...@@ -32,12 +32,12 @@
threadTypeTemplate = _.template($("#thread-type-template").html()); threadTypeTemplate = _.template($("#thread-type-template").html());
this.addField(threadTypeTemplate({form_id: formId})); this.addField(threadTypeTemplate({form_id: formId}));
this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true); this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true);
}
this.topicView = new DiscussionTopicMenuView({ this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId, topicId: this.topicId,
course_settings: this.course_settings course_settings: this.course_settings
}); });
this.addField(this.topicView.render()); this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body'); DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this; return this;
}, },
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
var title = this.$('.edit-post-title').val(), var title = this.$('.edit-post-title').val(),
threadType = this.$(".post-type-input:checked").val(), threadType = this.$(".post-type-input:checked").val(),
body = this.$('.edit-post-body textarea').val(), body = this.$('.edit-post-body textarea').val(),
commentableId = this.isTabMode() ? this.topicView.getCurrentTopicId() : null; commentableId = this.topicView.getCurrentTopicId();
return DiscussionUtil.safeAjax({ return DiscussionUtil.safeAjax({
$elem: this.submitBtn, $elem: this.submitBtn,
...@@ -74,19 +74,15 @@ ...@@ -74,19 +74,15 @@
success: function() { success: function() {
var newAttrs = { var newAttrs = {
title: title, title: title,
body: body body: body,
thread_type: threadType,
commentable_id: commentableId,
courseware_title: this.topicView.getFullTopicName()
}; };
// @TODO: Move this out of the callback, this makes it feel sluggish // @TODO: Move this out of the callback, this makes it feel sluggish
this.$('.edit-post-title').val('').attr('prev-text', ''); this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', ''); this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html(''); this.$('.wmd-preview p').html('');
if (this.isTabMode()) {
_.extend(newAttrs, {
thread_type: threadType,
commentable_id: commentableId,
courseware_title: this.topicView.getFullTopicName()
});
}
this.model.set(newAttrs).unset('abbreviatedBody'); this.model.set(newAttrs).unset('abbreviatedBody');
this.trigger('thread:updated'); this.trigger('thread:updated');
if (this.threadType !== threadType) { if (this.threadType !== threadType) {
......
...@@ -272,7 +272,6 @@ if Backbone? ...@@ -272,7 +272,6 @@ if Backbone?
model: @model model: @model
mode: @mode mode: @mode
course_settings: @options.course_settings course_settings: @options.course_settings
topicId: @model.get('commentable_id')
) )
@editView.bind "thread:updated thread:cancel_edit", @closeEditView @editView.bind "thread:updated thread:cancel_edit", @closeEditView
@editView.bind "comment:endorse", @endorseThread @editView.bind "comment:endorse", @endorseThread
...@@ -296,6 +295,9 @@ if Backbone? ...@@ -296,6 +295,9 @@ if Backbone?
closeEditView: (event) => closeEditView: (event) =>
@createShowView() @createShowView()
@renderShowView() @renderShowView()
# next call is necessary to re-render the post action controls after
# submitting or cancelling a thread edit in inline mode.
@$el.find(".post-extended-content").show()
# If you use "delete" here, it will compile down into JS that includes the # If you use "delete" here, it will compile down into JS that includes the
# use of DiscussionThreadView.prototype.delete, and that will break IE8 # use of DiscussionThreadView.prototype.delete, and that will break IE8
......
...@@ -40,7 +40,7 @@ def _attr_safe_json(obj): ...@@ -40,7 +40,7 @@ def _attr_safe_json(obj):
return saxutils.escape(json.dumps(obj), {'"': '&quot;'}) return saxutils.escape(json.dumps(obj), {'"': '&quot;'})
@newrelic.agent.function_trace() @newrelic.agent.function_trace()
def make_course_settings(course, include_category_map=False): def make_course_settings(course):
""" """
Generate a JSON-serializable model for course settings, which will be used to initialize a Generate a JSON-serializable model for course settings, which will be used to initialize a
DiscussionCourseSettings object on the client. DiscussionCourseSettings object on the client.
...@@ -51,11 +51,9 @@ def make_course_settings(course, include_category_map=False): ...@@ -51,11 +51,9 @@ def make_course_settings(course, include_category_map=False):
'allow_anonymous': course.allow_anonymous, 'allow_anonymous': course.allow_anonymous,
'allow_anonymous_to_peers': course.allow_anonymous_to_peers, 'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)], 'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)],
'category_map': utils.get_discussion_category_map(course)
} }
if include_category_map:
obj['category_map'] = utils.get_discussion_category_map(course)
return obj return obj
@newrelic.agent.function_trace() @newrelic.agent.function_trace()
...@@ -167,7 +165,7 @@ def forum_form_discussion(request, course_id): ...@@ -167,7 +165,7 @@ def forum_form_discussion(request, course_id):
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True) course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course, include_category_map=True) course_settings = make_course_settings(course)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user_info = user.to_dict() user_info = user.to_dict()
...@@ -231,7 +229,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -231,7 +229,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key) course = get_course_with_access(request.user, 'load_forum', course_key)
course_settings = make_course_settings(course, include_category_map=True) course_settings = make_course_settings(course)
cc_user = cc.User.from_django_user(request.user) cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict() user_info = cc_user.to_dict()
is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_key) is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_key)
......
...@@ -262,3 +262,19 @@ footer .references { ...@@ -262,3 +262,19 @@ footer .references {
.dashboard { .dashboard {
padding-top: 60px; padding-top: 60px;
} }
// ====================
// poor definition/scope on ul elements inside .vert-mod element in courseware - override needed for inline discussion editing
.course-content .discussion-post.edit-post-form .topic-menu {
padding-left: 0;
list-style: none;
.topic-menu-item {
margin-bottom: 0;
}
}
.course-content .discussion-post.edit-post-form .topic-submenu {
list-style: none;
}
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