Commit 00c7e60e by polesye

TNL-171: Change topic of a previously posted post.

parent 3cdfdae8
!view/discussion_thread_edit_view_spec.js
!view/discussion_topic_menu_view_spec.js
...@@ -71,9 +71,9 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -71,9 +71,9 @@ browser and pasting the output. When that file changes, this one should be rege
</script> </script>
<script aria-hidden="true" type="text/template" id="thread-edit-template"> <script aria-hidden="true" type="text/template" id="thread-edit-template">
<div class="discussion-post edit-post-form">
<h1>Editing post</h1> <h1>Editing post</h1>
<ul class="edit-post-form-errors"></ul> <ul class="edit-post-form-errors"></ul>
<div class="forum-edit-post-form-wrapper"></div>
<div class="form-row"> <div class="form-row">
<label class="sr" for="edit-post-title">Edit post title</label> <label class="sr" for="edit-post-title">Edit post title</label>
<input type="text" id="edit-post-title" class="edit-post-title" name="title" value="<%-title %>" placeholder="Title"> <input type="text" id="edit-post-title" class="edit-post-title" name="title" value="<%-title %>" placeholder="Title">
...@@ -83,7 +83,6 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -83,7 +83,6 @@ browser and pasting the output. When that file changes, this one should be rege
</div> </div>
<input type="submit" id="edit-post-submit" class="post-update" value="Update post"> <input type="submit" id="edit-post-submit" class="post-update" value="Update post">
<a href="#" class="post-cancel">Cancel</a> <a href="#" class="post-cancel">Cancel</a>
</div>
</script> </script>
<script aria-hidden="true" type="text/template" id="thread-response-template"> <script aria-hidden="true" type="text/template" id="thread-response-template">
...@@ -113,7 +112,7 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -113,7 +112,7 @@ browser and pasting the output. When that file changes, this one should be rege
<%= author_display %> <%= author_display %>
<p class="posted-details"> <p class="posted-details">
<span class="timeago" title="<%= created_at %>"><%= created_at %></span> <span class="timeago" title="<%= created_at %>"><%= created_at %></span>
<% if (obj.endorsement) { %> - <%= <% if (obj.endorsement) { %> - <%=
interpolate( interpolate(
thread.get("thread_type") == "question" ? thread.get("thread_type") == "question" ?
...@@ -174,7 +173,7 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -174,7 +173,7 @@ browser and pasting the output. When that file changes, this one should be rege
} }
) )
%> %>
<p class="posted-details"> <p class="posted-details">
<%= <%=
interpolate( interpolate(
...@@ -222,7 +221,7 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -222,7 +221,7 @@ browser and pasting the output. When that file changes, this one should be rege
<i class="icon <%= icon_class %>"></i> <i class="icon <%= icon_class %>"></i>
</div><div class="forum-nav-thread-wrapper-1"> </div><div class="forum-nav-thread-wrapper-1">
<span class="forum-nav-thread-title"><%- title %></span> <span class="forum-nav-thread-title"><%- title %></span>
<% <%
var labels = ""; var labels = "";
if (pinned) { if (pinned) {
...@@ -242,7 +241,7 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -242,7 +241,7 @@ browser and pasting the output. When that file changes, this one should be rege
} }
%> %>
</div><div class="forum-nav-thread-wrapper-2"> </div><div class="forum-nav-thread-wrapper-2">
<span class="forum-nav-thread-votes-count">+<%= <span class="forum-nav-thread-votes-count">+<%=
interpolate( interpolate(
'%(votes_up_count)s%(span_sr_open)s votes %(span_close)s', '%(votes_up_count)s%(span_sr_open)s votes %(span_close)s',
...@@ -250,7 +249,7 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -250,7 +249,7 @@ browser and pasting the output. When that file changes, this one should be rege
true true
) )
%></span> %></span>
<span class="forum-nav-thread-comments-count <% if (unread_comments_count > 0) { %>is-unread<% } %>"> <span class="forum-nav-thread-comments-count <% if (unread_comments_count > 0) { %>is-unread<% } %>">
<% <%
var fmt; var fmt;
...@@ -319,30 +318,7 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -319,30 +318,7 @@ browser and pasting the output. When that file changes, this one should be rege
Questions raise issues that need answers. Discussions share ideas and start conversations. Questions raise issues that need answers. Discussions share ideas and start conversations.
</span> </span>
</div> </div>
<% if (mode=="tab") { %> <div class="forum-new-post-form-wrapper"></div>
<div class="post-field">
<div class="field-label">
<span class="field-label-text">
Topic Area:
</span><div class="field-input post-topic">
<a href="#" class="post-topic-button">
<span class="sr">Discussion topics; current selection is: </span>
<span class="js-selected-topic"></span>
<span class="drop-arrow" aria-hidden="true">▾</span>
</a>
<div class="topic-menu-wrapper">
<label class="topic-filter-label">
<span class="sr">Filter topics</span>
<input type="text" class="topic-filter-input" placeholder="Filter topics">
</label>
<ul class="topic-menu" role="menu"><%= topics_html %></ul>
</div>
</div>
</div><span class="field-help">
Add your post to a relevant topic to help others find it.
</span>
</div>
<% } %>
<% if (cohort_options) { %> <% if (cohort_options) { %>
<div class="post-field"> <div class="post-field">
<label class="field-label"> <label class="field-label">
...@@ -406,6 +382,27 @@ browser and pasting the output. When that file changes, this one should be rege ...@@ -406,6 +382,27 @@ browser and pasting the output. When that file changes, this one should be rege
</li> </li>
</script> </script>
<script aria-hidden="true" type="text/template" id="topic-template">
<div class="field-label">
<span class="field-label-text">Topic Area:</span><div class="field-input post-topic">
<a href="#" class="post-topic-button">
<span class="sr">Discussion topics; current selection is: </span>
<span class="js-selected-topic"></span>
<span class="drop-arrow" aria-hidden="true">▾</span>
</a>
<div class="topic-menu-wrapper">
<label class="topic-filter-label">
<span class="sr">Filter topics</span>
<input type="text" class="topic-filter-input" placeholder="Filter topics">
</label>
<ul class="topic-menu" role="menu"><%= topics_html %></ul>
</div>
</div>
</div><span class="field-help">
Add your post to a relevant topic to help others find it.
</span>
</script>
......
(function() {
'use strict';
describe('DiscussionThreadEditView', function() {
beforeEach(function() {
DiscussionSpecHelper.setUpGlobals();
DiscussionSpecHelper.setUnderscoreFixtures();
spyOn(DiscussionUtil, 'makeWmdEditor');
this.threadData = DiscussionViewSpecHelper.makeThreadWithProps();
this.thread = new Thread(this.threadData);
this.course_settings = new DiscussionCourseSettings({
'category_map': {
'children': ['Topic'],
'entries': {
'Topic': {
'is_cohorted': true,
'id': 'topic'
}
}
},
'is_cohorted': true
});
this.createEditView = function (options) {
options = _.extend({
container: $('#fixture-element'),
model: this.thread,
mode: 'tab',
topicId: 'dummy_id',
course_settings: this.course_settings
}, options);
this.view = new DiscussionThreadEditView(options);
this.view.render();
};
});
it('can save new data correctly', function() {
var view;
spyOn($, 'ajax').andCallFake(function(params) {
expect(params.url.path()).toEqual(DiscussionUtil.urlFor('update_thread', 'dummy_id'));
expect(params.data.commentable_id).toBe('topic');
expect(params.data.title).toBe('new_title');
params.success();
return {always: function() {}};
});
this.createEditView();
this.view.$el.find('a.topic-title').first().click(); // set new topic
this.view.$('.edit-post-title').val('new_title'); // set new title
this.view.$('.post-update').click();
expect($.ajax).toHaveBeenCalled();
expect(this.thread.get('title')).toBe('new_title');
expect(this.thread.get('commentable_id')).toBe('topic');
expect(this.thread.get('courseware_title')).toBe('Topic');
expect(this.view.$('.edit-post-title')).toHaveValue('');
expect(this.view.$('.wmd-preview p')).toHaveText('');
});
it('can close the view', function() {
this.createEditView();
this.view.$('.post-cancel').click();
expect($('.edit-post-form')).not.toExist();
});
});
}).call(this);
...@@ -6,6 +6,7 @@ describe "DiscussionThreadView", -> ...@@ -6,6 +6,7 @@ describe "DiscussionThreadView", ->
jasmine.Clock.useMock() jasmine.Clock.useMock()
@threadData = DiscussionViewSpecHelper.makeThreadWithProps({}) @threadData = DiscussionViewSpecHelper.makeThreadWithProps({})
@thread = new Thread(@threadData) @thread = new Thread(@threadData)
@discussion = new Discussion(@thread)
spyOn($, "ajax") spyOn($, "ajax")
# Avoid unnecessary boilerplate # Avoid unnecessary boilerplate
spyOn(DiscussionThreadShowView.prototype, "convertMath") spyOn(DiscussionThreadShowView.prototype, "convertMath")
...@@ -44,6 +45,7 @@ describe "DiscussionThreadView", -> ...@@ -44,6 +45,7 @@ describe "DiscussionThreadView", ->
checkCommentForm = (originallyClosed, mode) -> checkCommentForm = (originallyClosed, mode) ->
threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed}) threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed})
thread = new Thread(threadData) thread = new Thread(threadData)
discussion = new Discussion(thread)
view = new DiscussionThreadView({ model: thread, el: $("#fixture-element"), mode: mode}) view = new DiscussionThreadView({ model: thread, el: $("#fixture-element"), mode: mode})
renderWithContent(view, {resp_total: 1, children: [{}]}) renderWithContent(view, {resp_total: 1, children: [{}]})
if mode == "inline" if mode == "inline"
......
(function() {
'use strict';
describe('DiscussionTopicMenuView', function() {
beforeEach(function() {
this.createTopicView = function (options) {
options = _.extend({
course_settings: this.course_settings,
topicId: void 0
}, options);
this.view = new DiscussionTopicMenuView(options);
this.view.render().appendTo('#fixture-element');
this.defaultTextWidth = this.view.getNameWidth(this.completeText);
};
this.openMenu = function () {
var menuWrapper = this.view.$('.topic-menu-wrapper');
expect(menuWrapper).toBeHidden();
this.view.$el.find('.post-topic-button').first().click();
expect(menuWrapper).toBeVisible();
};
this.closeMenu = function () {
var menuWrapper = this.view.$('.topic-menu-wrapper');
expect(menuWrapper).toBeVisible();
this.view.$el.find('.post-topic-button').first().click();
expect(menuWrapper).toBeHidden();
};
DiscussionSpecHelper.setUpGlobals();
DiscussionSpecHelper.setUnderscoreFixtures();
this.course_settings = new DiscussionCourseSettings({
'category_map': {
'subcategories': {
'Basic Question Types': {
'subcategories': {},
'children': ['Selection From Options', 'Numerical Input'],
'entries': {
'Selection From Options': {
'sort_key': null,
'is_cohorted': true,
'id': 'cba3e4cd91d0466b9ac50926e495b76f'
},
'Numerical Input': {
'sort_key': null,
'is_cohorted': false,
'id': 'c49f0dfb8fc94c9c8d9999cc95190c56'
}
}
}
},
'children': ['Basic Question Types'],
'entries': {}
},
'is_cohorted': true
});
this.parentCategoryText = 'Basic Question Types';
this.selectedOptionText = 'Selection From Options';
this.completeText = this.parentCategoryText + ' / ' + this.selectedOptionText;
});
it('completely show parent category and sub-category', function() {
var dropdownText;
this.createTopicView();
this.view.maxNameWidth = this.defaultTextWidth + 1;
this.view.$el.find('a.topic-title').first().click();
dropdownText = this.view.$el.find('.js-selected-topic').text();
expect(this.completeText).toEqual(dropdownText);
});
it('completely show just sub-category', function() {
var dropdownText;
this.createTopicView();
this.view.maxNameWidth = this.defaultTextWidth - 10;
this.view.$el.find('a.topic-title').first().click();
dropdownText = this.view.$el.find('.js-selected-topic').text();
expect(dropdownText.indexOf('…')).toEqual(0);
expect(dropdownText).toContain(this.selectedOptionText);
});
it('partially show sub-category', function() {
this.createTopicView();
var parentWidth = this.view.getNameWidth(this.parentCategoryText),
dropdownText;
this.view.maxNameWidth = this.defaultTextWidth - parentWidth;
this.view.$el.find('a.topic-title').first().click();
dropdownText = this.view.$el.find('.js-selected-topic').text();
expect(dropdownText.indexOf('…')).toEqual(0);
expect(dropdownText.lastIndexOf('…')).toBeGreaterThan(0);
});
it('broken span doesn\'t occur', function() {
var dropdownText;
this.createTopicView();
this.view.maxNameWidth = this.view.getNameWidth(this.selectedOptionText) + 100;
this.view.$el.find('a.topic-title').first().click();
dropdownText = this.view.$el.find('.js-selected-topic').text();
expect(dropdownText.indexOf('/ span>')).toEqual(-1);
});
it('appropriate topic is selected if `topicId` is passed', function () {
var completeText = this.parentCategoryText + ' / Numerical Input',
dropdownText;
this.createTopicView({
topicId: 'c49f0dfb8fc94c9c8d9999cc95190c56'
});
this.view.maxNameWidth = this.defaultTextWidth + 1;
this.view.render();
dropdownText = this.view.$el.find('.js-selected-topic').text();
expect(completeText).toEqual(dropdownText);
});
it('click outside of the dropdown close it', function () {
this.createTopicView();
this.openMenu();
$(document.body).click();
expect(this.view.$('.topic-menu-wrapper')).toBeHidden();
});
it('can toggle the menu', function () {
this.createTopicView();
this.openMenu();
this.closeMenu();
});
});
}).call(this);
...@@ -10,115 +10,6 @@ describe "NewPostView", -> ...@@ -10,115 +10,6 @@ describe "NewPostView", ->
) )
@discussion = new Discussion([], {pages: 1}) @discussion = new Discussion([], {pages: 1})
describe "Drop down works correct", ->
beforeEach ->
@course_settings = new DiscussionCourseSettings({
"category_map": {
"subcategories": {
"Basic Question Types": {
"subcategories": {},
"children": ["Selection From Options"],
"entries": {
"Selection From Options": {
"sort_key": null,
"is_cohorted": true,
"id": "cba3e4cd91d0466b9ac50926e495b76f"
}
},
},
},
"children": ["Basic Question Types"],
"entries": {}
},
"allow_anonymous": true,
"allow_anonymous_to_peers": true,
"is_cohorted": true
})
@view = new NewPostView(
el: $("#fixture-element"),
collection: @discussion,
course_settings: @course_settings,
mode: "tab"
)
@view.render()
@parent_category_text = "Basic Question Types"
@selected_option_text = "Selection From Options"
it "completely show parent category and sub-category", ->
complete_text = @parent_category_text + " / " + @selected_option_text
selected_text_width = @view.getNameWidth(complete_text)
@view.maxNameWidth = selected_text_width + 1
@view.$el.find( "a.topic-title" ).first().click()
dropdown_text = @view.$el.find(".js-selected-topic").text()
expect(complete_text).toEqual(dropdown_text)
it "completely show just sub-category", ->
complete_text = @parent_category_text + " / " + @selected_option_text
selected_text_width = @view.getNameWidth(complete_text)
@view.maxNameWidth = selected_text_width - 10
@view.$el.find( "a.topic-title" ).first().click()
dropdown_text = @view.$el.find(".js-selected-topic").text()
expect(dropdown_text.indexOf("…")).toEqual(0)
expect(dropdown_text).toContain(@selected_option_text)
it "partially show sub-category", ->
parent_width = @view.getNameWidth(@parent_category_text)
complete_text = @parent_category_text + " / " + @selected_option_text
selected_text_width = @view.getNameWidth(complete_text)
@view.maxNameWidth = selected_text_width - parent_width
@view.$el.find( "a.topic-title" ).first().click()
dropdown_text = @view.$el.find(".js-selected-topic").text()
expect(dropdown_text.indexOf("…")).toEqual(0)
expect(dropdown_text.lastIndexOf("…")).toBeGreaterThan(0)
it "broken span doesn't occur", ->
complete_text = @parent_category_text + " / " + @selected_option_text
selected_text_width = @view.getNameWidth(complete_text)
@view.maxNameWidth = @view.getNameWidth(@selected_option_text) + 100
@view.$el.find( "a.topic-title" ).first().click()
dropdown_text = @view.$el.find(".js-selected-topic").text()
expect(dropdown_text.indexOf("/ span>")).toEqual(-1)
describe "cohort selector", ->
renderWithCohortedTopics = (course_settings, view, isCohortedFirst) ->
course_settings.set(
"category_map",
{
"children": if isCohortedFirst then ["Cohorted", "Non-Cohorted"] else ["Non-Cohorted", "Cohorted"],
"entries": {
"Non-Cohorted": {
"sort_key": null,
"is_cohorted": false,
"id": "non-cohorted"
},
"Cohorted": {
"sort_key": null,
"is_cohorted": true,
"id": "cohorted"
}
}
}
)
DiscussionSpecHelper.makeModerator()
view.render()
expectCohortSelectorEnabled = (view, enabled) ->
expect(view.$(".js-group-select").prop("disabled")).toEqual(not enabled)
if not enabled
expect(view.$(".js-group-select option:selected").attr("value")).toEqual("")
it "is disabled with non-cohorted default topic and enabled by selecting cohorted topic", ->
renderWithCohortedTopics(@course_settings, @view, false)
expectCohortSelectorEnabled(@view, false)
@view.$("a.topic-title[data-discussion-id=cohorted]").click()
expectCohortSelectorEnabled(@view, true)
it "is enabled with cohorted default topic and disabled by selecting non-cohorted topic", ->
renderWithCohortedTopics(@course_settings, @view, true)
expectCohortSelectorEnabled(@view, true)
@view.$("a.topic-title[data-discussion-id=non-cohorted]").click()
expectCohortSelectorEnabled(@view, false)
describe "cohort selector", -> describe "cohort selector", ->
beforeEach -> beforeEach ->
@course_settings = new DiscussionCourseSettings({ @course_settings = new DiscussionCourseSettings({
......
!views/discussion_thread_edit_view.js
!views/discussion_topic_menu_view.js
...@@ -99,7 +99,13 @@ if Backbone? ...@@ -99,7 +99,13 @@ if Backbone?
@newPostForm = $('.new-post-article') @newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) -> @threadviews = @discussion.map (thread) ->
new DiscussionThreadView el: @$("article#thread_#{thread.id}"), model: thread, mode: "inline" new DiscussionThreadView(
el: @$("article#thread_#{thread.id}"),
model: thread,
mode: "inline",
course_settings: @course_settings,
topicId: discussionId
)
_.each @threadviews, (dtv) -> dtv.render() _.each @threadviews, (dtv) -> dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@newPostView = new NewPostView( @newPostView = new NewPostView(
...@@ -123,7 +129,14 @@ if Backbone? ...@@ -123,7 +129,14 @@ if Backbone?
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1? # TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>") article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
@$('section.discussion > .threads').prepend(article) @$('section.discussion > .threads').prepend(article)
threadView = new DiscussionThreadView el: article, model: thread, mode: "inline"
threadView = new DiscussionThreadView(
el: article,
model: thread,
mode: "inline",
course_settings: @course_settings,
topicId: @$el.data("discussion-id")
)
threadView.render() threadView.render()
@threadviews.unshift threadView @threadviews.unshift threadView
......
...@@ -54,7 +54,12 @@ if Backbone? ...@@ -54,7 +54,12 @@ if Backbone?
if(@newPost.is(":visible")) if(@newPost.is(":visible"))
@newPost.fadeOut() @newPost.fadeOut()
@main = new DiscussionThreadView(el: $(".forum-content"), model: @thread, mode: "tab") @main = new DiscussionThreadView(
el: $(".forum-content"),
model: @thread,
mode: "tab",
course_settings: @course_settings,
)
@main.render() @main.render()
@main.on "thread:responses:rendered", => @main.on "thread:responses:rendered", =>
@nav.updateSidebar() @nav.updateSidebar()
......
if Backbone?
class @DiscussionThreadEditView extends Backbone.View
events:
"click .post-update": "update"
"click .post-cancel": "cancel_edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
render: ->
@template = _.template($("#thread-edit-template").html())
@$el.html(@template(@model.toJSON()))
@delegateEvents()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
@
update: (event) ->
@trigger "thread:update", event
cancel_edit: (event) ->
@trigger "thread:cancel_edit", event
(function(Backbone) {
'use strict';
if (Backbone) {
this.DiscussionThreadEditView = Backbone.View.extend({
tagName: 'form',
events: {
'submit': 'updateHandler',
'click .post-cancel': 'cancelHandler'
},
attributes: {
'class': 'discussion-post edit-post-form'
},
initialize: function(options) {
this.container = options.container || $('.thread-content-wrapper');
this.mode = options.mode || 'inline';
this.course_settings = options.course_settings;
this.topicId = options.topicId;
_.bindAll(this);
return this;
},
render: function() {
this.template = _.template($('#thread-edit-template').html());
this.$el.html(this.template(this.model.toJSON())).appendTo(this.container);
this.submitBtn = this.$('.post-update');
if (this.isTabMode()) {
this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this;
},
addField: function(fieldView) {
this.$('.forum-edit-post-form-wrapper').append(fieldView);
return this;
},
isTabMode: function () {
return this.mode === 'tab';
},
save: function() {
var title = this.$('.edit-post-title').val(),
body = this.$('.edit-post-body textarea').val(),
commentableId = this.isTabMode() ? this.topicView.getCurrentTopicId() : null;
return DiscussionUtil.safeAjax({
$elem: this.submitBtn,
$loading: this.submitBtn,
url: DiscussionUtil.urlFor('update_thread', this.model.id),
type: 'POST',
dataType: 'json',
async: false, // @TODO when the rest of the stuff below is made to work properly..
data: {
title: title,
body: body,
commentable_id: commentableId
},
error: DiscussionUtil.formErrorHandler(this.$('.post-errors')),
success: function() {
var newAttrs = {
title: title,
body: body
};
// @TODO: Move this out of the callback, this makes it feel sluggish
this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html('');
if (this.isTabMode()) {
_.extend(newAttrs, {
commentable_id: commentableId,
courseware_title: this.topicView.getFullTopicName()
});
}
this.model.set(newAttrs).unset('abbreviatedBody');
this.trigger('thread:updated');
}.bind(this)
});
},
updateHandler: function(event) {
event.preventDefault();
// this event is for the moment triggered and used nowhere.
this.trigger('thread:update', event);
this.save();
return this;
},
cancelHandler: function(event) {
event.preventDefault();
this.trigger("thread:cancel_edit", event);
this.remove();
return this;
}
});
}
}).call(this, Backbone);
...@@ -5,7 +5,7 @@ if Backbone? ...@@ -5,7 +5,7 @@ if Backbone?
"keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event) "keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event)
"keyup .forum-nav-browse-filter-input": "filterTopics" "keyup .forum-nav-browse-filter-input": "filterTopics"
"click .forum-nav-browse-menu-wrapper": "ignoreClick" "click .forum-nav-browse-menu-wrapper": "ignoreClick"
"click .forum-nav-browse-title": "selectTopic" "click .forum-nav-browse-title": "selectTopicHandler"
"keydown .forum-nav-search-input": "performSearch" "keydown .forum-nav-search-input": "performSearch"
"change .forum-nav-sort-control": "sortThreads" "change .forum-nav-sort-control": "sortThreads"
"click .forum-nav-thread-link": "threadSelected" "click .forum-nav-thread-link": "threadSelected"
...@@ -130,12 +130,12 @@ if Backbone? ...@@ -130,12 +130,12 @@ if Backbone?
) )
@$(".forum-nav-sort-control").val(@collection.sort_preference) @$(".forum-nav-sort-control").val(@collection.sort_preference)
$(window).bind "load", @updateSidebar $(window).bind "load scroll resize", @updateSidebar
$(window).bind "scroll", @updateSidebar
$(window).bind "resize", @updateSidebar
@displayedCollection.on "reset", @renderThreads @displayedCollection.on "reset", @renderThreads
@displayedCollection.on "thread:remove", @renderThreads @displayedCollection.on "thread:remove", @renderThreads
@displayedCollection.on "change:commentable_id", (model, commentable_id) =>
@retrieveDiscussions @discussionIds.split(",") if @mode is "commentables"
@renderThreads() @renderThreads()
@ @
...@@ -185,7 +185,7 @@ if Backbone? ...@@ -185,7 +185,7 @@ if Backbone?
when 'search' when 'search'
options.search_text = @current_search options.search_text = @current_search
if @group_id if @group_id
options.group_id = @group_id options.group_id = @group_id
when 'followed' when 'followed'
options.user_id = window.user.id options.user_id = window.user.id
options.group_id = "all" options.group_id = "all"
...@@ -196,8 +196,7 @@ if Backbone? ...@@ -196,8 +196,7 @@ if Backbone?
when 'all' when 'all'
if @group_id if @group_id
options.group_id = @group_id options.group_id = @group_id
lastThread = @collection.last()?.get('id') lastThread = @collection.last()?.get('id')
if lastThread if lastThread
# Pagination; focus the first thread after what was previously the last thread # Pagination; focus the first thread after what was previously the last thread
...@@ -262,7 +261,7 @@ if Backbone? ...@@ -262,7 +261,7 @@ if Backbone?
else else
$('input.email-setting').removeAttr('checked') $('input.email-setting').removeAttr('checked')
thread_id = null thread_id = null
@trigger("thread:removed") @trigger("thread:removed")
#select all threads #select all threads
isBrowseMenuVisible: => isBrowseMenuVisible: =>
...@@ -359,12 +358,15 @@ if Backbone? ...@@ -359,12 +358,15 @@ if Backbone?
name = prefix + rawName + gettext("…") name = prefix + rawName + gettext("…")
return name return name
selectTopic: (event) -> selectTopicHandler: (event) ->
event.preventDefault() event.preventDefault()
@selectTopic $(event.target)
selectTopic: ($target) ->
@hideBrowseMenu() @hideBrowseMenu()
@clearSearch() @clearSearch()
item = $(event.target).closest('.forum-nav-browse-menu-item') item = $target.closest('.forum-nav-browse-menu-item')
@setCurrentTopicDisplay(@getPathText(item)) @setCurrentTopicDisplay(@getPathText(item))
if item.hasClass("forum-nav-browse-menu-all") if item.hasClass("forum-nav-browse-menu-all")
@discussionIds = "" @discussionIds = ""
...@@ -388,7 +390,7 @@ if Backbone? ...@@ -388,7 +390,7 @@ if Backbone?
chooseCohort: (event) => chooseCohort: (event) =>
@group_id = @$('.forum-nav-filter-cohort-control :selected').val() @group_id = @$('.forum-nav-filter-cohort-control :selected').val()
@retrieveFirstPage() @retrieveFirstPage()
retrieveDiscussion: (discussion_id, callback=null) -> retrieveDiscussion: (discussion_id, callback=null) ->
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id) url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
...@@ -403,7 +405,7 @@ if Backbone? ...@@ -403,7 +405,7 @@ if Backbone?
if callback? if callback?
callback() callback()
retrieveDiscussions: (discussion_ids) -> retrieveDiscussions: (discussion_ids) ->
@discussionIds = discussion_ids.join(',') @discussionIds = discussion_ids.join(',')
@mode = 'commentables' @mode = 'commentables'
......
...@@ -21,6 +21,13 @@ if Backbone? ...@@ -21,6 +21,13 @@ if Backbone?
@mode = options.mode or "inline" # allowed values are "tab" or "inline" @mode = options.mode or "inline" # allowed values are "tab" or "inline"
if @mode not in ["tab", "inline"] if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode) throw new Error("invalid mode: " + @mode)
# Quick fix to have an actual model when we're receiving new models from
# the server.
@model.collection.on "reset", (collection) =>
id = @model.get("id")
@model = collection.get(id) if collection.get(id)
@createShowView() @createShowView()
@responses = new Comments() @responses = new Comments()
@loadedResponses = false @loadedResponses = false
...@@ -254,49 +261,20 @@ if Backbone? ...@@ -254,49 +261,20 @@ if Backbone?
@createEditView() @createEditView()
@renderEditView() @renderEditView()
update: (event) =>
newTitle = @editView.$(".edit-post-title").val()
newBody = @editView.$(".edit-post-body textarea").val()
url = DiscussionUtil.urlFor('update_thread', @model.id)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: newTitle
body: newBody
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
@editView.$(".edit-post-title").val("").attr("prev-text", "")
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
@editView.$(".wmd-preview p").html("")
@model.set
title: newTitle
body: newBody
@model.unset("abbreviatedBody")
@createShowView()
@renderShowView()
createEditView: () -> createEditView: () ->
if @showView? if @showView?
@showView.undelegateEvents() @showView.undelegateEvents()
@showView.$el.empty() @showView.$el.empty()
@showView = null @showView = null
@editView = new DiscussionThreadEditView(model: @model) @editView = new DiscussionThreadEditView(
@editView.bind "thread:update", @update container: @$('.thread-content-wrapper')
@editView.bind "thread:cancel_edit", @cancelEdit model: @model
mode: @mode
course_settings: @options.course_settings
topicId: @model.get('commentable_id')
)
@editView.bind "thread:updated thread:cancel_edit", @closeEditView
renderSubView: (view) -> renderSubView: (view) ->
view.setElement(@$('.thread-content-wrapper')) view.setElement(@$('.thread-content-wrapper'))
...@@ -304,15 +282,9 @@ if Backbone? ...@@ -304,15 +282,9 @@ if Backbone?
view.delegateEvents() view.delegateEvents()
renderEditView: () -> renderEditView: () ->
@renderSubView(@editView) @editView.render()
createShowView: () -> createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadShowView({model: @model, mode: @mode}) @showView = new DiscussionThreadShowView({model: @model, mode: @mode})
@showView.bind "thread:_delete", @_delete @showView.bind "thread:_delete", @_delete
@showView.bind "thread:edit", @edit @showView.bind "thread:edit", @edit
...@@ -320,8 +292,7 @@ if Backbone? ...@@ -320,8 +292,7 @@ if Backbone?
renderShowView: () -> renderShowView: () ->
@renderSubView(@showView) @renderSubView(@showView)
cancelEdit: (event) => closeEditView: (event) =>
event.preventDefault()
@createShowView() @createShowView()
@renderShowView() @renderShowView()
......
(function(Backbone) {
'use strict';
if (Backbone) {
this.DiscussionTopicMenuView = Backbone.View.extend({
events: {
'click .post-topic-button': 'toggleTopicDropdown',
'click .topic-menu-wrapper': 'handleTopicEvent',
'click .topic-filter-label': 'ignoreClick',
'keyup .topic-filter-input': this.DiscussionFilter.filterDrop
},
attributes: {
'class': 'post-field'
},
initialize: function(options) {
this.course_settings = options.course_settings;
this.currentTopicId = options.topicId;
this.maxNameWidth = 100;
_.bindAll(this);
return this;
},
/**
* When the menu is expanded, a click on the body element (outside of the menu) or on a menu element
* should close the menu except when the target is the search field. To accomplish this, we have to ignore
* clicks on the search field by stopping the propagation of the event.
*/
ignoreClick: function(event) {
event.stopPropagation();
return this;
},
render: function() {
var context = _.clone(this.course_settings.attributes);
context.topics_html = this.renderCategoryMap(this.course_settings.get('category_map'));
this.$el.html(_.template($('#topic-template').html(), context));
this.dropdownButton = this.$('.post-topic-button');
this.topicMenu = this.$('.topic-menu-wrapper');
this.selectedTopic = this.$('.js-selected-topic');
this.hideTopicDropdown();
if (this.getCurrentTopicId()) {
this.setTopic(this.$('a.topic-title').filter('[data-discussion-id=' + this.getCurrentTopicId() + ']'));
} else {
this.setTopic(this.$('a.topic-title').first());
}
return this.$el;
},
renderCategoryMap: function(map) {
var category_template = _.template($('#new-post-menu-category-template').html()),
entry_template = _.template($('#new-post-menu-entry-template').html());
return _.map(map.children, function(name) {
var html = '', entry;
if (_.has(map.entries, name)) {
entry = map.entries[name];
html = entry_template({
text: name,
id: entry.id,
is_cohorted: entry.is_cohorted
});
} else { // subcategory
html = category_template({
text: name,
entries: this.renderCategoryMap(map.subcategories[name])
});
}
return html;
}, this).join('');
},
toggleTopicDropdown: function(event) {
event.preventDefault();
event.stopPropagation();
if (this.menuOpen) {
this.hideTopicDropdown();
} else {
this.showTopicDropdown();
}
return this;
},
showTopicDropdown: function() {
this.menuOpen = true;
this.dropdownButton.addClass('dropped');
this.topicMenu.show();
$(document.body).on('click.topicMenu', this.hideTopicDropdown);
// Set here because 1) the window might get resized and things could
// change and 2) can't set in initialize because the button is hidden
this.maxNameWidth = this.dropdownButton.width() - 40;
return this;
},
hideTopicDropdown: function() {
this.menuOpen = false;
this.dropdownButton.removeClass('dropped');
this.topicMenu.hide();
$(document.body).off('click.topicMenu');
return this;
},
handleTopicEvent: function(event) {
event.preventDefault();
event.stopPropagation();
this.setTopic($(event.target));
return this;
},
setTopic: function($target) {
if ($target.data('discussion-id')) {
this.topicText = this.getFullTopicName($target);
this.currentTopicId = $target.data('discussion-id');
this.setSelectedTopicName(this.topicText);
this.trigger('thread:topic_change', $target);
this.hideTopicDropdown();
}
return this;
},
getCurrentTopicId: function() {
return this.currentTopicId;
},
setSelectedTopicName: function(text) {
return this.selectedTopic.html(this.fitName(text));
},
/**
* Return full name for the `topicElement` if it is passed.
* Otherwise, full name for the current topic will be returned.
* @param {jQuery Element} [topicElement]
* @return {String}
*/
getFullTopicName: function(topicElement) {
var name;
if (topicElement) {
name = topicElement.html();
_.each(topicElement.parents('.topic-submenu'), function(item) {
name = $(item).siblings('.topic-title').text() + ' / ' + name;
});
return name;
} else {
return this.topicText;
}
},
// @TODO move into utils.coffee
getNameWidth: function(name) {
var test = $('<div>'),
width;
test.css({
'font-size': this.dropdownButton.css('font-size'),
'opacity': 0,
'position': 'absolute',
'left': -1000,
'top': -1000
}).html(name).appendTo(document.body);
width = test.width();
test.remove();
return width;
},
// @TODO move into utils.coffee
fitName: function(name) {
var ellipsisText = gettext('…'),
partialName, path, rawName;
if (this.getNameWidth(name) < this.maxNameWidth) {
return name;
} else {
path = _.map(name.split('/'), function(item){
return item.replace(/^\s+|\s+$/g, '');
});
while (path.length > 1) {
path.shift();
partialName = ellipsisText + ' / ' + path.join(' / ');
if (this.getNameWidth(partialName) < this.maxNameWidth) {
return partialName;
}
}
rawName = path[0];
name = ellipsisText + ' / ' + rawName;
while (this.getNameWidth(name) > this.maxNameWidth) {
rawName = rawName.slice(0, -1);
name = ellipsisText + ' / ' + rawName + ' ' + ellipsisText;
}
}
return name;
}
});
}
}).call(this, Backbone);
...@@ -6,7 +6,6 @@ if Backbone? ...@@ -6,7 +6,6 @@ if Backbone?
if @mode not in ["tab", "inline"] if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode) throw new Error("invalid mode: " + @mode)
@course_settings = options.course_settings @course_settings = options.course_settings
@maxNameWidth = 100
@topicId = options.topicId @topicId = options.topicId
render: () -> render: () ->
...@@ -16,29 +15,21 @@ if Backbone? ...@@ -16,29 +15,21 @@ if Backbone?
mode: @mode, mode: @mode,
form_id: @mode + (if @topicId then "-" + @topicId else "") form_id: @mode + (if @topicId then "-" + @topicId else "")
}) })
context.topics_html = @renderCategoryMap(@course_settings.get("category_map")) if @mode is "tab"
@$el.html(_.template($("#new-post-template").html(), context)) @$el.html(_.template($("#new-post-template").html(), context))
if @isTabMode()
if @mode is "tab" @topicView = new DiscussionTopicMenuView {
# set up the topic dropdown in tab mode topicId: @topicId
@dropdownButton = @$(".post-topic-button") course_settings: @course_settings
@topicMenu = @$(".topic-menu-wrapper") }
@hideTopicDropdown() @topicView.on('thread:topic_change', @toggleGroupDropdown)
@setTopic(@$("a.topic-title").first()) @addField(@topicView.render())
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "js-post-body" DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "js-post-body"
renderCategoryMap: (map) -> addField: (fieldView) ->
category_template = _.template($("#new-post-menu-category-template").html()) @$('.forum-new-post-form-wrapper').append fieldView
entry_template = _.template($("#new-post-menu-entry-template").html())
html = "" isTabMode: () ->
for name in map.children @mode is "tab"
if name of map.entries
entry = map.entries[name]
html += entry_template({text: name, id: entry.id, is_cohorted: entry.is_cohorted})
else # subcategory
html += category_template({text: name, entries: @renderCategoryMap(map.subcategories[name])})
html
getCohortOptions: () -> getCohortOptions: () ->
if @course_settings.get("is_cohorted") and DiscussionUtil.isPrivilegedUser() if @course_settings.get("is_cohorted") and DiscussionUtil.isPrivilegedUser()
...@@ -50,19 +41,15 @@ if Backbone? ...@@ -50,19 +41,15 @@ if Backbone?
events: events:
"submit .forum-new-post-form": "createPost" "submit .forum-new-post-form": "createPost"
"click .post-topic-button": "toggleTopicDropdown"
"click .topic-menu-wrapper": "handleTopicEvent"
"click .topic-filter-label": "ignoreClick"
"keyup .topic-filter-input": DiscussionFilter.filterDrop
"change .post-option-input": "postOptionChange" "change .post-option-input": "postOptionChange"
"click .cancel": "cancel" "click .cancel": "cancel"
"reset .forum-new-post-form": "updateStyles" "reset .forum-new-post-form": "updateStyles"
# Because we want the behavior that when the body is clicked the menu is toggleGroupDropdown: ($target) ->
# closed, we need to ignore clicks in the search field and stop propagation. if $target.data('cohorted')
# Without this, clicking the search field would also close the menu. $('.js-group-select').prop('disabled', false);
ignoreClick: (event) -> else
event.stopPropagation() $('.js-group-select').val('').prop('disabled', true);
postOptionChange: (event) -> postOptionChange: (event) ->
$target = $(event.target) $target = $(event.target)
...@@ -77,13 +64,14 @@ if Backbone? ...@@ -77,13 +64,14 @@ if Backbone?
thread_type = @$(".post-type-input:checked").val() thread_type = @$(".post-type-input:checked").val()
title = @$(".js-post-title").val() title = @$(".js-post-title").val()
body = @$(".js-post-body").find(".wmd-input").val() body = @$(".js-post-body").find(".wmd-input").val()
group = @$(".js-group-select option:selected").attr("value") group = @$(".js-group-select option:selected").attr("value")
anonymous = false || @$(".js-anon").is(":checked") anonymous = false || @$(".js-anon").is(":checked")
anonymous_to_peers = false || @$(".js-anon-peers").is(":checked") anonymous_to_peers = false || @$(".js-anon-peers").is(":checked")
follow = false || @$(".js-follow").is(":checked") follow = false || @$(".js-follow").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @topicId) topicId = if @isTabMode() then @topicView.getCurrentTopicId() else @topicId
url = DiscussionUtil.urlFor('create_thread', topicId)
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
$elem: $(event.target) $elem: $(event.target)
...@@ -108,97 +96,6 @@ if Backbone? ...@@ -108,97 +96,6 @@ if Backbone?
@resetForm() @resetForm()
@collection.add thread @collection.add thread
toggleTopicDropdown: (event) ->
event.preventDefault()
event.stopPropagation()
if @menuOpen
@hideTopicDropdown()
else
@showTopicDropdown()
showTopicDropdown: () ->
@menuOpen = true
@dropdownButton.addClass('dropped')
@topicMenu.show()
$(".form-topic-drop-search-input").focus()
$("body").bind "click", @hideTopicDropdown
# Set here because 1) the window might get resized and things could
# change and 2) can't set in initialize because the button is hidden
@maxNameWidth = @dropdownButton.width() - 40
# Need a fat arrow because hideTopicDropdown is passed as a callback to bind
hideTopicDropdown: () =>
@menuOpen = false
@dropdownButton.removeClass('dropped')
@topicMenu.hide()
$("body").unbind "click", @hideTopicDropdown
handleTopicEvent: (event) ->
event.preventDefault()
event.stopPropagation()
@setTopic($(event.target))
setTopic: ($target) ->
if $target.data('discussion-id')
@topicText = $target.html()
@topicText = @getFullTopicName($target)
@topicId = $target.data('discussion-id')
@setSelectedTopic()
if $target.data("cohorted")
$(".js-group-select").prop("disabled", false)
else
$(".js-group-select").val("")
$(".js-group-select").prop("disabled", true)
@hideTopicDropdown()
setSelectedTopic: ->
@$(".js-selected-topic").html(@fitName(@topicText))
getFullTopicName: (topicElement) ->
name = topicElement.html()
topicElement.parents('.topic-submenu').each ->
name = $(this).siblings('.topic-title').text() + ' / ' + name
return name
getNameWidth: (name) ->
test = $("<div>")
test.css
"font-size": @dropdownButton.css('font-size')
opacity: 0
position: 'absolute'
left: -1000
top: -1000
$("body").append(test)
test.html(name)
width = test.width()
test.remove()
return width
fitName: (name) ->
width = @getNameWidth(name)
if width < @maxNameWidth
return name
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
while path.length > 1
path.shift()
partialName = gettext("…") + " / " + path.join(" / ")
if @getNameWidth(partialName) < @maxNameWidth
return partialName
rawName = path[0]
name = gettext("…") + " / " + rawName
while @getNameWidth(name) > @maxNameWidth
rawName = rawName[0...rawName.length-1]
name = gettext("…") + " / " + rawName + " " + gettext("…")
return name
cancel: (event) -> cancel: (event) ->
event.preventDefault() event.preventDefault()
if not confirm gettext("Your post will be discarded.") if not confirm gettext("Your post will be discarded.")
...@@ -210,8 +107,8 @@ if Backbone? ...@@ -210,8 +107,8 @@ if Backbone?
@$(".forum-new-post-form")[0].reset() @$(".forum-new-post-form")[0].reset()
DiscussionUtil.clearFormErrors(@$(".post-errors")) DiscussionUtil.clearFormErrors(@$(".post-errors"))
@$(".wmd-preview p").html("") @$(".wmd-preview p").html("")
if @mode is "tab" if @isTabMode()
@setTopic(@$("a.topic-title").first()) @topicView.setTopic(@$("a.topic-title").first())
updateStyles: => updateStyles: =>
# form reset doesn't change the style of checkboxes so this event is to do that job # form reset doesn't change the style of checkboxes so this event is to do that job
......
...@@ -15,7 +15,7 @@ from django_comment_client.base import views ...@@ -15,7 +15,7 @@ from django_comment_client.base import views
from django_comment_client.tests.group_id import CohortedTopicGroupIdTestMixin, NonCohortedTopicGroupIdTestMixin, GroupIdAssertionMixin from django_comment_client.tests.group_id import CohortedTopicGroupIdTestMixin, NonCohortedTopicGroupIdTestMixin, GroupIdAssertionMixin
from django_comment_client.tests.utils import CohortedContentTestCase from django_comment_client.tests.utils import CohortedContentTestCase
from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_common.models import Role, FORUM_ROLE_STUDENT from django_comment_common.models import Role
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
...@@ -160,7 +160,6 @@ class ThreadActionGroupIdTestCase( ...@@ -160,7 +160,6 @@ class ThreadActionGroupIdTestCase(
) )
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@patch('lms.lib.comment_client.utils.requests.request') @patch('lms.lib.comment_client.utils.requests.request')
class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
...@@ -369,6 +368,15 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): ...@@ -369,6 +368,15 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
mock_request mock_request
) )
@patch('django_comment_client.base.views.get_discussion_id_map', return_value={"test_commentable": {}})
def test_update_thread_wrong_commentable_id(self, mock_get_discussion_id_map, mock_request):
self._test_request_error(
"update_thread",
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
{"body": "foo", "title": "foo", "commentable_id": "wrong_commentable"},
mock_request
)
def test_create_comment_no_body(self, mock_request): def test_create_comment_no_body(self, mock_request):
self._test_request_error( self._test_request_error(
"create_comment", "create_comment",
...@@ -460,7 +468,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): ...@@ -460,7 +468,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
"at_position_list": [], "at_position_list": [],
"closed": is_closed, "closed": is_closed,
"id": "518d4237b023791dca00000d", "id": "518d4237b023791dca00000d",
"user_id": "1","username": "robot", "user_id": "1", "username": "robot",
"votes": { "votes": {
"count": 0, "count": 0,
"up_count": 0, "up_count": 0,
...@@ -853,13 +861,14 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq ...@@ -853,13 +861,14 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq
self.student = UserFactory.create() self.student = UserFactory.create()
CourseEnrollmentFactory(user=self.student, course_id=self.course.id) CourseEnrollmentFactory(user=self.student, course_id=self.course.id)
@patch('django_comment_client.base.views.get_discussion_id_map', return_value={"test_commentable": {}})
@patch('lms.lib.comment_client.utils.requests.request') @patch('lms.lib.comment_client.utils.requests.request')
def _test_unicode_data(self, text, mock_request): def _test_unicode_data(self, text, mock_request, mock_get_discussion_id_map):
self._set_mock_request_data(mock_request, { self._set_mock_request_data(mock_request, {
"user_id": str(self.student.id), "user_id": str(self.student.id),
"closed": False, "closed": False,
}) })
request = RequestFactory().post("dummy_url", {"body": text, "title": text}) request = RequestFactory().post("dummy_url", {"body": text, "title": text, "commentable_id": "test_commentable"})
request.user = self.student request.user = self.student
request.view_name = "update_thread" request.view_name = "update_thread"
response = views.update_thread(request, course_id=self.course.id.to_deprecated_string(), thread_id="dummy_thread_id") response = views.update_thread(request, course_id=self.course.id.to_deprecated_string(), thread_id="dummy_thread_id")
...@@ -868,6 +877,7 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq ...@@ -868,6 +877,7 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq
self.assertTrue(mock_request.called) self.assertTrue(mock_request.called)
self.assertEqual(mock_request.call_args[1]["data"]["body"], text) self.assertEqual(mock_request.call_args[1]["data"]["body"], text)
self.assertEqual(mock_request.call_args[1]["data"]["title"], text) self.assertEqual(mock_request.call_args[1]["data"]["title"], text)
self.assertEqual(mock_request.call_args[1]["data"]["commentable_id"], "test_commentable")
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......
...@@ -18,8 +18,6 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey ...@@ -18,8 +18,6 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import get_course_with_access, get_course_by_id from courseware.courses import get_course_with_access, get_course_by_id
from course_groups.models import CourseUserGroup
from course_groups.cohorts import get_cohort_by_id, get_cohort_id, is_commentable_cohorted
import django_comment_client.settings as cc_settings import django_comment_client.settings as cc_settings
from django_comment_client.utils import ( from django_comment_client.utils import (
add_courseware_context, add_courseware_context,
...@@ -28,7 +26,8 @@ from django_comment_client.utils import ( ...@@ -28,7 +26,8 @@ from django_comment_client.utils import (
JsonError, JsonError,
JsonResponse, JsonResponse,
prepare_content, prepare_content,
get_group_id_for_comments_service get_group_id_for_comments_service,
get_discussion_id_map,
) )
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
import lms.lib.comment_client as cc import lms.lib.comment_client as cc
...@@ -139,12 +138,21 @@ def update_thread(request, course_id, thread_id): ...@@ -139,12 +138,21 @@ def update_thread(request, course_id, thread_id):
return JsonError(_("Title can't be empty")) return JsonError(_("Title can't be empty"))
if 'body' not in request.POST or not request.POST['body'].strip(): if 'body' not in request.POST or not request.POST['body'].strip():
return JsonError(_("Body can't be empty")) return JsonError(_("Body can't be empty"))
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.body = request.POST["body"] thread.body = request.POST["body"]
thread.title = request.POST["title"] thread.title = request.POST["title"]
thread.save()
if "commentable_id" in request.POST:
course = get_course_with_access(request.user, 'load', course_key)
id_map = get_discussion_id_map(course)
if request.POST.get("commentable_id") in id_map:
thread.commentable_id = request.POST["commentable_id"]
else:
return JsonError(_("Topic doesn't exist"))
thread.save()
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_key, thread.to_dict()) return ajax_content_response(request, course_key, thread.to_dict())
else: else:
...@@ -614,6 +622,7 @@ def upload(request, course_id): # ajax upload file to a question or answer ...@@ -614,6 +622,7 @@ def upload(request, course_id): # ajax upload file to a question or answer
} }
}) })
@require_GET @require_GET
@login_required @login_required
def users(request, course_id): def users(request, course_id):
...@@ -640,7 +649,7 @@ def users(request, course_id): ...@@ -640,7 +649,7 @@ def users(request, course_id):
try: try:
matched_user = User.objects.get(username=username) matched_user = User.objects.get(username=username)
cc_user = cc.User.from_django_user(matched_user) cc_user = cc.User.from_django_user(matched_user)
cc_user.course_id=course_key cc_user.course_id = course_key
cc_user.retrieve(complete=False) cc_user.retrieve(complete=False)
if (cc_user['threads_count'] + cc_user['comments_count']) > 0: if (cc_user['threads_count'] + cc_user['comments_count']) > 0:
user_objs.append({ user_objs.append({
......
...@@ -71,7 +71,7 @@ def _get_discussion_modules(course): ...@@ -71,7 +71,7 @@ def _get_discussion_modules(course):
return filter(has_required_keys, all_modules) return filter(has_required_keys, all_modules)
def _get_discussion_id_map(course): def get_discussion_id_map(course):
def get_entry(module): def get_entry(module):
discussion_id = module.discussion_id discussion_id = module.discussion_id
title = module.discussion_target title = module.discussion_target
...@@ -352,7 +352,7 @@ def extend_content(content): ...@@ -352,7 +352,7 @@ def extend_content(content):
def add_courseware_context(content_list, course): def add_courseware_context(content_list, course):
id_map = _get_discussion_id_map(course) id_map = get_discussion_id_map(course)
for content in content_list: for content in content_list:
commentable_id = content['commentable_id'] commentable_id = content['commentable_id']
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
@import "discussion/elements/labels"; @import "discussion/elements/labels";
@import "discussion/elements/navigation"; @import "discussion/elements/navigation";
@import "discussion/views/thread"; @import "discussion/views/thread";
@import "discussion/views/new-post"; @import "discussion/views/create-edit-post";
@import "discussion/views/response"; @import "discussion/views/response";
@import 'discussion/utilities/developer'; @import 'discussion/utilities/developer';
@import 'discussion/utilities/shame'; @import 'discussion/utilities/shame';
......
...@@ -107,7 +107,8 @@ li[class*=forum-nav-thread-label-] { ...@@ -107,7 +107,8 @@ li[class*=forum-nav-thread-label-] {
// new post form // new post form
// ------------- // -------------
.forum-new-post-form { .forum-new-post-form,
.edit-post-form {
// Override global label rules // Override global label rules
.post-type { .post-type {
text-shadow: none; text-shadow: none;
...@@ -127,7 +128,7 @@ li[class*=forum-nav-thread-label-] { ...@@ -127,7 +128,7 @@ li[class*=forum-nav-thread-label-] {
margin-bottom: 0; margin-bottom: 0;
} }
// Override global span rules // Override global span rules
.post-topic-button .drop-arrow { .post-topic-button .drop-arrow {
line-height: 36px; line-height: 36px;
} }
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
// ==================== // ====================
// UI: form structure // UI: form structure
.forum-new-post-form { .forum-new-post-form,
.edit-post-form {
@include clearfix; @include clearfix;
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
...@@ -64,7 +65,8 @@ ...@@ -64,7 +65,8 @@
// ==================== // ====================
// UI: inputs // UI: inputs
.forum-new-post-form { .forum-new-post-form,
.edit-post-form {
.post-topic-button { .post-topic-button {
@include white-button; @include white-button;
@extend %cont-truncated; @extend %cont-truncated;
...@@ -172,7 +174,8 @@ ...@@ -172,7 +174,8 @@
// ==================== // ====================
// UI: errors - new post creation // UI: errors - new post creation
.forum-new-post-form { .forum-new-post-form,
.edit-post-form {
.post-errors { .post-errors {
margin-bottom: $baseline; margin-bottom: $baseline;
border-radius: 3px; border-radius: 3px;
...@@ -199,7 +202,8 @@ ...@@ -199,7 +202,8 @@
// UI: topic menu // UI: topic menu
// TO-DO: refactor to use _navigation.scss as general topic selector // TO-DO: refactor to use _navigation.scss as general topic selector
.forum-new-post-form .post-topic { .forum-new-post-form .post-topic ,
.edit-post-form .post-topic {
position: relative; position: relative;
.topic-menu-wrapper { .topic-menu-wrapper {
......
...@@ -54,9 +54,9 @@ ...@@ -54,9 +54,9 @@
% endfor % endfor
<script aria-hidden="true" type="text/template" id="thread-edit-template"> <script aria-hidden="true" type="text/template" id="thread-edit-template">
<div class="discussion-post edit-post-form">
<h1>${_("Editing post")}</h1> <h1>${_("Editing post")}</h1>
<ul class="edit-post-form-errors"></ul> <ul class="post-errors"></ul>
<div class="forum-edit-post-form-wrapper"></div>
<div class="form-row"> <div class="form-row">
<label class="sr" for="edit-post-title">${_("Edit post title")}</label> <label class="sr" for="edit-post-title">${_("Edit post title")}</label>
<input type="text" id="edit-post-title" class="edit-post-title" name="title" value="${"<%-title %>"}" placeholder="${_('Title') | h}"> <input type="text" id="edit-post-title" class="edit-post-title" name="title" value="${"<%-title %>"}" placeholder="${_('Title') | h}">
...@@ -66,7 +66,6 @@ ...@@ -66,7 +66,6 @@
</div> </div>
<input type="submit" id="edit-post-submit" class="post-update" value="${_("Update post") | h}"> <input type="submit" id="edit-post-submit" class="post-update" value="${_("Update post") | h}">
<a href="#" class="post-cancel">${_("Cancel")}</a> <a href="#" class="post-cancel">${_("Cancel")}</a>
</div>
</script> </script>
<script aria-hidden="true" type="text/template" id="thread-response-template"> <script aria-hidden="true" type="text/template" id="thread-response-template">
...@@ -408,31 +407,7 @@ ...@@ -408,31 +407,7 @@
${_("Questions raise issues that need answers. Discussions share ideas and start conversations.")} ${_("Questions raise issues that need answers. Discussions share ideas and start conversations.")}
</span> </span>
</div> </div>
${'<% if (mode=="tab") { %>'} <div class="forum-new-post-form-wrapper"></div>
<div class="post-field">
## Using div here instead of label because we are using a non-native control
<div class="field-label">
<span class="field-label-text">
${_("Topic Area:")}
</span><div class="field-input post-topic">
<a href="#" class="post-topic-button">
<span class="sr">${_("Discussion topics; current selection is: ")}</span>
<span class="js-selected-topic"></span>
<span class="drop-arrow" aria-hidden="true"></span>
</a>
<div class="topic-menu-wrapper">
<label class="topic-filter-label">
<span class="sr">${_("Filter topics")}</span>
<input type="text" class="topic-filter-input" placeholder="${_('Filter topics')}">
</label>
<ul class="topic-menu" role="menu">${'<%= topics_html %>'}</ul>
</div>
</div>
</div><span class="field-help">
${_("Add your post to a relevant topic to help others find it.")}
</span>
</div>
${'<% } %>'}
${'<% if (cohort_options) { %>'} ${'<% if (cohort_options) { %>'}
<div class="post-field"> <div class="post-field">
<label class="field-label"> <label class="field-label">
...@@ -497,6 +472,28 @@ ...@@ -497,6 +472,28 @@
</li> </li>
</script> </script>
<script aria-hidden="true" type="text/template" id="topic-template">
## Using div here instead of label because we are using a non-native control
<div class="field-label">
<span class="field-label-text">${_("Topic Area:")}</span><div class="field-input post-topic">
<a href="#" class="post-topic-button">
<span class="sr">${_("Discussion topics; current selection is: ")}</span>
<span class="js-selected-topic"></span>
<span class="drop-arrow" aria-hidden="true"></span>
</a>
<div class="topic-menu-wrapper">
<label class="topic-filter-label">
<span class="sr">${_("Filter topics")}</span>
<input type="text" class="topic-filter-input" placeholder="${_('Filter topics')}">
</label>
<ul class="topic-menu" role="menu">${'<%= topics_html %>'}</ul>
</div>
</div>
</div><span class="field-help">
${_("Add your post to a relevant topic to help others find it.")}
</span>
</script>
<%def name="primaryAction(action_class, icon, sr_label, unchecked_label, checked_label)"> <%def name="primaryAction(action_class, icon, sr_label, unchecked_label, checked_label)">
<script type="text/template" id="forum-action-${action_class}"> <script type="text/template" id="forum-action-${action_class}">
<li class="actions-item"> <li class="actions-item">
......
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