TNL-171: Change topic of a previously posted post.
This commit is contained in:
2
common/static/coffee/spec/discussion/.gitignore
vendored
Normal file
2
common/static/coffee/spec/discussion/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!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
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="thread-edit-template">
|
||||
<div class="discussion-post edit-post-form">
|
||||
<h1>Editing post</h1>
|
||||
<ul class="edit-post-form-errors"></ul>
|
||||
<div class="forum-edit-post-form-wrapper"></div>
|
||||
<div class="form-row">
|
||||
<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">
|
||||
@@ -83,7 +83,6 @@ browser and pasting the output. When that file changes, this one should be rege
|
||||
</div>
|
||||
<input type="submit" id="edit-post-submit" class="post-update" value="Update post">
|
||||
<a href="#" class="post-cancel">Cancel</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<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
|
||||
<%= author_display %>
|
||||
<p class="posted-details">
|
||||
<span class="timeago" title="<%= created_at %>"><%= created_at %></span>
|
||||
|
||||
|
||||
<% if (obj.endorsement) { %> - <%=
|
||||
interpolate(
|
||||
thread.get("thread_type") == "question" ?
|
||||
@@ -174,7 +173,7 @@ browser and pasting the output. When that file changes, this one should be rege
|
||||
}
|
||||
)
|
||||
%>
|
||||
|
||||
|
||||
<p class="posted-details">
|
||||
<%=
|
||||
interpolate(
|
||||
@@ -222,7 +221,7 @@ browser and pasting the output. When that file changes, this one should be rege
|
||||
<i class="icon <%= icon_class %>"></i>
|
||||
</div><div class="forum-nav-thread-wrapper-1">
|
||||
<span class="forum-nav-thread-title"><%- title %></span>
|
||||
|
||||
|
||||
<%
|
||||
var labels = "";
|
||||
if (pinned) {
|
||||
@@ -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">
|
||||
|
||||
|
||||
<span class="forum-nav-thread-votes-count">+<%=
|
||||
interpolate(
|
||||
'%(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
|
||||
true
|
||||
)
|
||||
%></span>
|
||||
|
||||
|
||||
<span class="forum-nav-thread-comments-count <% if (unread_comments_count > 0) { %>is-unread<% } %>">
|
||||
<%
|
||||
var fmt;
|
||||
@@ -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.
|
||||
</span>
|
||||
</div>
|
||||
<% if (mode=="tab") { %>
|
||||
<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>
|
||||
<% } %>
|
||||
<div class="forum-new-post-form-wrapper"></div>
|
||||
<% if (cohort_options) { %>
|
||||
<div class="post-field">
|
||||
<label class="field-label">
|
||||
@@ -406,6 +382,27 @@ browser and pasting the output. When that file changes, this one should be rege
|
||||
</li>
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
(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", ->
|
||||
jasmine.Clock.useMock()
|
||||
@threadData = DiscussionViewSpecHelper.makeThreadWithProps({})
|
||||
@thread = new Thread(@threadData)
|
||||
@discussion = new Discussion(@thread)
|
||||
spyOn($, "ajax")
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(DiscussionThreadShowView.prototype, "convertMath")
|
||||
@@ -44,6 +45,7 @@ describe "DiscussionThreadView", ->
|
||||
checkCommentForm = (originallyClosed, mode) ->
|
||||
threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed})
|
||||
thread = new Thread(threadData)
|
||||
discussion = new Discussion(thread)
|
||||
view = new DiscussionThreadView({ model: thread, el: $("#fixture-element"), mode: mode})
|
||||
renderWithContent(view, {resp_total: 1, children: [{}]})
|
||||
if mode == "inline"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
(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", ->
|
||||
)
|
||||
@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", ->
|
||||
beforeEach ->
|
||||
@course_settings = new DiscussionCourseSettings({
|
||||
|
||||
2
common/static/coffee/src/discussion/.gitignore
vendored
Normal file
2
common/static/coffee/src/discussion/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!views/discussion_thread_edit_view.js
|
||||
!views/discussion_topic_menu_view.js
|
||||
@@ -99,7 +99,13 @@ if Backbone?
|
||||
|
||||
@newPostForm = $('.new-post-article')
|
||||
@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()
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
|
||||
@newPostView = new NewPostView(
|
||||
@@ -123,7 +129,14 @@ if Backbone?
|
||||
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
|
||||
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></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()
|
||||
@threadviews.unshift threadView
|
||||
|
||||
|
||||
@@ -54,7 +54,12 @@ if Backbone?
|
||||
if(@newPost.is(":visible"))
|
||||
@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.on "thread:responses:rendered", =>
|
||||
@nav.updateSidebar()
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
@@ -0,0 +1,103 @@
|
||||
(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?
|
||||
"keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event)
|
||||
"keyup .forum-nav-browse-filter-input": "filterTopics"
|
||||
"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"
|
||||
"change .forum-nav-sort-control": "sortThreads"
|
||||
"click .forum-nav-thread-link": "threadSelected"
|
||||
@@ -130,12 +130,12 @@ if Backbone?
|
||||
)
|
||||
@$(".forum-nav-sort-control").val(@collection.sort_preference)
|
||||
|
||||
$(window).bind "load", @updateSidebar
|
||||
$(window).bind "scroll", @updateSidebar
|
||||
$(window).bind "resize", @updateSidebar
|
||||
$(window).bind "load scroll resize", @updateSidebar
|
||||
|
||||
@displayedCollection.on "reset", @renderThreads
|
||||
@displayedCollection.on "thread:remove", @renderThreads
|
||||
@displayedCollection.on "change:commentable_id", (model, commentable_id) =>
|
||||
@retrieveDiscussions @discussionIds.split(",") if @mode is "commentables"
|
||||
@renderThreads()
|
||||
@
|
||||
|
||||
@@ -185,7 +185,7 @@ if Backbone?
|
||||
when 'search'
|
||||
options.search_text = @current_search
|
||||
if @group_id
|
||||
options.group_id = @group_id
|
||||
options.group_id = @group_id
|
||||
when 'followed'
|
||||
options.user_id = window.user.id
|
||||
options.group_id = "all"
|
||||
@@ -196,8 +196,7 @@ if Backbone?
|
||||
when 'all'
|
||||
if @group_id
|
||||
options.group_id = @group_id
|
||||
|
||||
|
||||
|
||||
lastThread = @collection.last()?.get('id')
|
||||
if lastThread
|
||||
# Pagination; focus the first thread after what was previously the last thread
|
||||
@@ -262,7 +261,7 @@ if Backbone?
|
||||
else
|
||||
$('input.email-setting').removeAttr('checked')
|
||||
thread_id = null
|
||||
@trigger("thread:removed")
|
||||
@trigger("thread:removed")
|
||||
#select all threads
|
||||
|
||||
isBrowseMenuVisible: =>
|
||||
@@ -359,12 +358,15 @@ if Backbone?
|
||||
name = prefix + rawName + gettext("…")
|
||||
return name
|
||||
|
||||
selectTopic: (event) ->
|
||||
selectTopicHandler: (event) ->
|
||||
event.preventDefault()
|
||||
@selectTopic $(event.target)
|
||||
|
||||
selectTopic: ($target) ->
|
||||
@hideBrowseMenu()
|
||||
@clearSearch()
|
||||
|
||||
item = $(event.target).closest('.forum-nav-browse-menu-item')
|
||||
item = $target.closest('.forum-nav-browse-menu-item')
|
||||
@setCurrentTopicDisplay(@getPathText(item))
|
||||
if item.hasClass("forum-nav-browse-menu-all")
|
||||
@discussionIds = ""
|
||||
@@ -388,7 +390,7 @@ if Backbone?
|
||||
chooseCohort: (event) =>
|
||||
@group_id = @$('.forum-nav-filter-cohort-control :selected').val()
|
||||
@retrieveFirstPage()
|
||||
|
||||
|
||||
retrieveDiscussion: (discussion_id, callback=null) ->
|
||||
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
|
||||
DiscussionUtil.safeAjax
|
||||
@@ -403,7 +405,7 @@ if Backbone?
|
||||
if callback?
|
||||
callback()
|
||||
|
||||
|
||||
|
||||
retrieveDiscussions: (discussion_ids) ->
|
||||
@discussionIds = discussion_ids.join(',')
|
||||
@mode = 'commentables'
|
||||
|
||||
@@ -21,6 +21,13 @@ if Backbone?
|
||||
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
|
||||
if @mode not in ["tab", "inline"]
|
||||
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()
|
||||
@responses = new Comments()
|
||||
@loadedResponses = false
|
||||
@@ -254,49 +261,20 @@ if Backbone?
|
||||
@createEditView()
|
||||
@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: () ->
|
||||
|
||||
if @showView?
|
||||
@showView.undelegateEvents()
|
||||
@showView.$el.empty()
|
||||
@showView = null
|
||||
|
||||
@editView = new DiscussionThreadEditView(model: @model)
|
||||
@editView.bind "thread:update", @update
|
||||
@editView.bind "thread:cancel_edit", @cancelEdit
|
||||
@editView = new DiscussionThreadEditView(
|
||||
container: @$('.thread-content-wrapper')
|
||||
model: @model
|
||||
mode: @mode
|
||||
course_settings: @options.course_settings
|
||||
topicId: @model.get('commentable_id')
|
||||
)
|
||||
@editView.bind "thread:updated thread:cancel_edit", @closeEditView
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$('.thread-content-wrapper'))
|
||||
@@ -304,15 +282,9 @@ if Backbone?
|
||||
view.delegateEvents()
|
||||
|
||||
renderEditView: () ->
|
||||
@renderSubView(@editView)
|
||||
@editView.render()
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
|
||||
@showView = new DiscussionThreadShowView({model: @model, mode: @mode})
|
||||
@showView.bind "thread:_delete", @_delete
|
||||
@showView.bind "thread:edit", @edit
|
||||
@@ -320,8 +292,7 @@ if Backbone?
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
|
||||
cancelEdit: (event) =>
|
||||
event.preventDefault()
|
||||
closeEditView: (event) =>
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
(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?
|
||||
if @mode not in ["tab", "inline"]
|
||||
throw new Error("invalid mode: " + @mode)
|
||||
@course_settings = options.course_settings
|
||||
@maxNameWidth = 100
|
||||
@topicId = options.topicId
|
||||
|
||||
render: () ->
|
||||
@@ -16,29 +15,21 @@ if Backbone?
|
||||
mode: @mode,
|
||||
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))
|
||||
|
||||
if @mode is "tab"
|
||||
# set up the topic dropdown in tab mode
|
||||
@dropdownButton = @$(".post-topic-button")
|
||||
@topicMenu = @$(".topic-menu-wrapper")
|
||||
@hideTopicDropdown()
|
||||
@setTopic(@$("a.topic-title").first())
|
||||
|
||||
if @isTabMode()
|
||||
@topicView = new DiscussionTopicMenuView {
|
||||
topicId: @topicId
|
||||
course_settings: @course_settings
|
||||
}
|
||||
@topicView.on('thread:topic_change', @toggleGroupDropdown)
|
||||
@addField(@topicView.render())
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "js-post-body"
|
||||
|
||||
renderCategoryMap: (map) ->
|
||||
category_template = _.template($("#new-post-menu-category-template").html())
|
||||
entry_template = _.template($("#new-post-menu-entry-template").html())
|
||||
html = ""
|
||||
for name in map.children
|
||||
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
|
||||
addField: (fieldView) ->
|
||||
@$('.forum-new-post-form-wrapper').append fieldView
|
||||
|
||||
isTabMode: () ->
|
||||
@mode is "tab"
|
||||
|
||||
getCohortOptions: () ->
|
||||
if @course_settings.get("is_cohorted") and DiscussionUtil.isPrivilegedUser()
|
||||
@@ -50,19 +41,15 @@ if Backbone?
|
||||
|
||||
events:
|
||||
"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"
|
||||
"click .cancel": "cancel"
|
||||
"reset .forum-new-post-form": "updateStyles"
|
||||
|
||||
# Because we want the behavior that when the body is clicked the menu is
|
||||
# closed, we need to ignore clicks in the search field and stop propagation.
|
||||
# Without this, clicking the search field would also close the menu.
|
||||
ignoreClick: (event) ->
|
||||
event.stopPropagation()
|
||||
toggleGroupDropdown: ($target) ->
|
||||
if $target.data('cohorted')
|
||||
$('.js-group-select').prop('disabled', false);
|
||||
else
|
||||
$('.js-group-select').val('').prop('disabled', true);
|
||||
|
||||
postOptionChange: (event) ->
|
||||
$target = $(event.target)
|
||||
@@ -77,13 +64,14 @@ if Backbone?
|
||||
thread_type = @$(".post-type-input:checked").val()
|
||||
title = @$(".js-post-title").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_to_peers = false || @$(".js-anon-peers").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
|
||||
$elem: $(event.target)
|
||||
@@ -108,97 +96,6 @@ if Backbone?
|
||||
@resetForm()
|
||||
@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) ->
|
||||
event.preventDefault()
|
||||
if not confirm gettext("Your post will be discarded.")
|
||||
@@ -210,8 +107,8 @@ if Backbone?
|
||||
@$(".forum-new-post-form")[0].reset()
|
||||
DiscussionUtil.clearFormErrors(@$(".post-errors"))
|
||||
@$(".wmd-preview p").html("")
|
||||
if @mode is "tab"
|
||||
@setTopic(@$("a.topic-title").first())
|
||||
if @isTabMode()
|
||||
@topicView.setTopic(@$("a.topic-title").first())
|
||||
|
||||
updateStyles: =>
|
||||
# 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
|
||||
from django_comment_client.tests.group_id import CohortedTopicGroupIdTestMixin, NonCohortedTopicGroupIdTestMixin, GroupIdAssertionMixin
|
||||
from django_comment_client.tests.utils import CohortedContentTestCase
|
||||
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 student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
@@ -160,7 +160,6 @@ class ThreadActionGroupIdTestCase(
|
||||
)
|
||||
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
@@ -369,6 +368,15 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
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):
|
||||
self._test_request_error(
|
||||
"create_comment",
|
||||
@@ -460,7 +468,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
"at_position_list": [],
|
||||
"closed": is_closed,
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"user_id": "1","username": "robot",
|
||||
"user_id": "1", "username": "robot",
|
||||
"votes": {
|
||||
"count": 0,
|
||||
"up_count": 0,
|
||||
@@ -853,13 +861,14 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq
|
||||
self.student = UserFactory.create()
|
||||
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')
|
||||
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, {
|
||||
"user_id": str(self.student.id),
|
||||
"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.view_name = "update_thread"
|
||||
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
|
||||
self.assertTrue(mock_request.called)
|
||||
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"]["commentable_id"], "test_commentable")
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
|
||||
@@ -18,8 +18,6 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.access import has_access
|
||||
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
|
||||
from django_comment_client.utils import (
|
||||
add_courseware_context,
|
||||
@@ -28,7 +26,8 @@ from django_comment_client.utils import (
|
||||
JsonError,
|
||||
JsonResponse,
|
||||
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
|
||||
import lms.lib.comment_client as cc
|
||||
@@ -139,12 +138,21 @@ def update_thread(request, course_id, thread_id):
|
||||
return JsonError(_("Title can't be empty"))
|
||||
if 'body' not in request.POST or not request.POST['body'].strip():
|
||||
return JsonError(_("Body can't be empty"))
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.body = request.POST["body"]
|
||||
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():
|
||||
return ajax_content_response(request, course_key, thread.to_dict())
|
||||
else:
|
||||
@@ -614,6 +622,7 @@ def upload(request, course_id): # ajax upload file to a question or answer
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def users(request, course_id):
|
||||
@@ -640,7 +649,7 @@ def users(request, course_id):
|
||||
try:
|
||||
matched_user = User.objects.get(username=username)
|
||||
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)
|
||||
if (cc_user['threads_count'] + cc_user['comments_count']) > 0:
|
||||
user_objs.append({
|
||||
|
||||
@@ -71,7 +71,7 @@ def _get_discussion_modules(course):
|
||||
return filter(has_required_keys, all_modules)
|
||||
|
||||
|
||||
def _get_discussion_id_map(course):
|
||||
def get_discussion_id_map(course):
|
||||
def get_entry(module):
|
||||
discussion_id = module.discussion_id
|
||||
title = module.discussion_target
|
||||
@@ -352,7 +352,7 @@ def extend_content(content):
|
||||
|
||||
|
||||
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:
|
||||
commentable_id = content['commentable_id']
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
@import "discussion/elements/labels";
|
||||
@import "discussion/elements/navigation";
|
||||
@import "discussion/views/thread";
|
||||
@import "discussion/views/new-post";
|
||||
@import "discussion/views/create-edit-post";
|
||||
@import "discussion/views/response";
|
||||
@import 'discussion/utilities/developer';
|
||||
@import 'discussion/utilities/shame';
|
||||
|
||||
@@ -107,7 +107,8 @@ li[class*=forum-nav-thread-label-] {
|
||||
// new post form
|
||||
// -------------
|
||||
|
||||
.forum-new-post-form {
|
||||
.forum-new-post-form,
|
||||
.edit-post-form {
|
||||
// Override global label rules
|
||||
.post-type {
|
||||
text-shadow: none;
|
||||
@@ -127,7 +128,7 @@ li[class*=forum-nav-thread-label-] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Override global span rules
|
||||
// Override global span rules
|
||||
.post-topic-button .drop-arrow {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// ====================
|
||||
|
||||
// UI: form structure
|
||||
.forum-new-post-form {
|
||||
.forum-new-post-form,
|
||||
.edit-post-form {
|
||||
@include clearfix;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -64,7 +65,8 @@
|
||||
// ====================
|
||||
|
||||
// UI: inputs
|
||||
.forum-new-post-form {
|
||||
.forum-new-post-form,
|
||||
.edit-post-form {
|
||||
.post-topic-button {
|
||||
@include white-button;
|
||||
@extend %cont-truncated;
|
||||
@@ -172,7 +174,8 @@
|
||||
// ====================
|
||||
|
||||
// UI: errors - new post creation
|
||||
.forum-new-post-form {
|
||||
.forum-new-post-form,
|
||||
.edit-post-form {
|
||||
.post-errors {
|
||||
margin-bottom: $baseline;
|
||||
border-radius: 3px;
|
||||
@@ -199,7 +202,8 @@
|
||||
// UI: topic menu
|
||||
|
||||
// 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;
|
||||
|
||||
.topic-menu-wrapper {
|
||||
@@ -54,9 +54,9 @@
|
||||
% endfor
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="thread-edit-template">
|
||||
<div class="discussion-post edit-post-form">
|
||||
<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">
|
||||
<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}">
|
||||
@@ -66,7 +66,6 @@
|
||||
</div>
|
||||
<input type="submit" id="edit-post-submit" class="post-update" value="${_("Update post") | h}">
|
||||
<a href="#" class="post-cancel">${_("Cancel")}</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="thread-response-template">
|
||||
@@ -408,31 +407,7 @@
|
||||
${_("Questions raise issues that need answers. Discussions share ideas and start conversations.")}
|
||||
</span>
|
||||
</div>
|
||||
${'<% if (mode=="tab") { %>'}
|
||||
<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>
|
||||
${'<% } %>'}
|
||||
<div class="forum-new-post-form-wrapper"></div>
|
||||
${'<% if (cohort_options) { %>'}
|
||||
<div class="post-field">
|
||||
<label class="field-label">
|
||||
@@ -497,6 +472,28 @@
|
||||
</li>
|
||||
</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)">
|
||||
<script type="text/template" id="forum-action-${action_class}">
|
||||
<li class="actions-item">
|
||||
|
||||
Reference in New Issue
Block a user