Experimental: Interface to Move Posts to an Existing Topic
This commit is contained in:
parent
f8e8538e19
commit
cf01c98d81
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
This component helps with Searching
|
||||
|
||||
@class Search
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.Search = {
|
||||
|
||||
/**
|
||||
Search for a term, with an optional filter.
|
||||
|
||||
@method forTerm
|
||||
@param {String} term The term to search for
|
||||
@param {String} typeFilter An optional filter to restrict the search by type
|
||||
@return {Promise} a promise that resolves the search results
|
||||
**/
|
||||
forTerm: function(term, typeFilter) {
|
||||
return Discourse.ajax('/search', {
|
||||
data: { term: term, type_filter: typeFilter }
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.TopicController = Discourse.ObjectController.extend({
|
||||
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
|
||||
userFilters: new Em.Set(),
|
||||
multiSelect: false,
|
||||
bestOf: false,
|
||||
|
@ -22,11 +22,6 @@ Discourse.TopicController = Discourse.ObjectController.extend({
|
|||
return posts.filterProperty('selected');
|
||||
}.property('content.posts.@each.selected'),
|
||||
|
||||
selectedCount: function() {
|
||||
if (!this.get('selectedPosts')) return 0;
|
||||
return this.get('selectedPosts').length;
|
||||
}.property('selectedPosts'),
|
||||
|
||||
canMoveSelected: function() {
|
||||
if (!this.get('content.can_move_posts')) return false;
|
||||
// For now, we can move it if we can delete it since the posts need to be deleted.
|
||||
|
@ -91,7 +86,7 @@ Discourse.TopicController = Discourse.ObjectController.extend({
|
|||
|
||||
deleteSelected: function() {
|
||||
var topicController = this;
|
||||
return bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedCount')}), function(result) {
|
||||
return bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||
if (result) {
|
||||
var selectedPosts = topicController.get('selectedPosts');
|
||||
Discourse.Post.deleteMany(selectedPosts);
|
||||
|
|
|
@ -56,6 +56,26 @@ Handlebars.registerHelper('categoryLink', function(property, options) {
|
|||
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
|
||||
});
|
||||
|
||||
/**
|
||||
Inserts a Discourse.TextField to allow the user to enter information.
|
||||
|
||||
@method textField
|
||||
@for Handlebars
|
||||
**/
|
||||
Ember.Handlebars.registerHelper('textField', function(options) {
|
||||
var hash = options.hash,
|
||||
types = options.hashTypes;
|
||||
|
||||
for (var prop in hash) {
|
||||
if (types[prop] === 'ID') {
|
||||
hash[prop + 'Binding'] = hash[prop];
|
||||
delete hash[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return Ember.Handlebars.helpers.view.call(this, Discourse.TextField, options);
|
||||
});
|
||||
|
||||
/**
|
||||
Produces a bound link to a category
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
This mixin allows a modal to list a selected posts count nicely.
|
||||
|
||||
@class Discourse.SelectedPostsCount
|
||||
@extends Ember.Mixin
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.SelectedPostsCount = Em.Mixin.create({
|
||||
|
||||
selectedPostsCount: function() {
|
||||
if (!this.get('selectedPosts')) return 0;
|
||||
return this.get('selectedPosts').length;
|
||||
}.property('selectedPosts')
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -449,12 +449,10 @@ Discourse.Topic.reopenClass({
|
|||
});
|
||||
},
|
||||
|
||||
// Create a topic from posts
|
||||
movePosts: function(topicId, title, postIds) {
|
||||
|
||||
movePosts: function(topicId, opts) {
|
||||
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
|
||||
type: 'POST',
|
||||
data: { title: title, post_ids: postIds }
|
||||
data: opts
|
||||
}).then(function (result) {
|
||||
if (result.success) return result;
|
||||
promise.reject();
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<label for='choose-topic-title'>{{i18n choose_topic.title.search}}</label>
|
||||
|
||||
{{textField value=view.topicTitle placeholderKey="choose_topic.title.placeholder" elementId="choose-topic-title"}}
|
||||
|
||||
{{#if view.loading}}
|
||||
<p>{{i18n loading}}</p>
|
||||
{{else}}
|
||||
{{#if view.noResults}}
|
||||
<p>{{i18n choose_topic.none_found}}</p>
|
||||
{{else}}
|
||||
{{#each view.topics}}
|
||||
<div class='controls'>
|
||||
<label class='radio'>
|
||||
<input type='radio' id="choose-topic-{{unbound id}}" name='choose_topic_id' {{action chooseTopic this target="view"}}>{{title}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -1,20 +1,8 @@
|
|||
<div id='move-selected' class="modal-body">
|
||||
{{#if view.error}}
|
||||
<div class="alert alert-error">
|
||||
<button class="close" data-dismiss="alert">×</button>
|
||||
{{i18n topic.invite_reply.error}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{{i18n topic.move_selected.instructions count="view.selectedCount"}}}
|
||||
<p>{{{i18n topic.move_selected.instructions count="view.selectedPostsCount"}}}</p>
|
||||
|
||||
<form>
|
||||
<label>{{i18n topic.move_selected.topic_name}}</label>
|
||||
{{view Discourse.TextField valueBinding="view.topicName" placeholderKey="composer.title_placeholder"}}
|
||||
</form>
|
||||
<button {{action showMoveNewTopic target="view"}} class="btn">{{i18n topic.move_selected.new_topic.title}}</button>
|
||||
<button {{action showMoveExistingTopic target="view"}} class="btn">{{i18n topic.move_selected.existing_topic.title}}</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{bindAttr disabled="view.buttonDisabled"}} {{action movePosts target="view"}}>{{view.buttonTitle}}</button>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
<div id='move-selected' class="modal-body">
|
||||
{{#if view.error}}
|
||||
<div class="alert alert-error">
|
||||
<button class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<p>{{{i18n topic.move_selected.existing_topic.instructions count="view.selectedPostsCount"}}}</p>
|
||||
|
||||
{{view Discourse.ChooseTopicView selectedTopicIdBinding="view.selectedTopicId"}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{bindAttr disabled="view.buttonDisabled"}} {{action movePostsToExistingTopic target="view"}}>{{view.buttonTitle}}</button>
|
||||
</div>
|
|
@ -0,0 +1,19 @@
|
|||
<div id='move-selected' class="modal-body">
|
||||
{{#if view.error}}
|
||||
<div class="alert alert-error">
|
||||
<button class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{{i18n topic.move_selected.new_topic.instructions count="view.selectedPostsCount"}}}
|
||||
|
||||
<form>
|
||||
<label>{{i18n topic.move_selected.new_topic.topic_name}}</label>
|
||||
{{view Discourse.TextField valueBinding="view.topicName" placeholderKey="composer.title_placeholder"}}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{bindAttr disabled="view.buttonDisabled"}} {{action movePostsToNewTopic target="view"}}>{{view.buttonTitle}}</button>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
<p>{{countI18n topic.multi_select.description countBinding="controller.selectedCount"}}</p>
|
||||
<p>{{countI18n topic.multi_select.description countBinding="controller.selectedPostsCount"}}</p>
|
||||
|
||||
{{#if canDeleteSelected}}
|
||||
<button class='btn' {{action deleteSelected}}><i class='icon icon-trash'></i> {{i18n topic.multi_select.delete}}</button>
|
||||
|
@ -6,6 +6,6 @@
|
|||
|
||||
{{#if canMoveSelected}}
|
||||
<button class='btn' {{action moveSelected}}><i class='icon icon-move'></i> {{i18n topic.multi_select.move}}</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<p class='cancel'><a href='#' {{action toggleMultiSelect}}>{{i18n topic.multi_select.cancel}}</a></p>
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
This view presents the user with a widget to choose a topic.
|
||||
|
||||
@class ChooseTopicView
|
||||
@extends Discourse.View
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.ChooseTopicView = Discourse.View.extend({
|
||||
templateName: 'choose_topic',
|
||||
|
||||
topicTitleChanged: function() {
|
||||
this.set('loading', true);
|
||||
this.set('noResults', true);
|
||||
this.set('selectedTopicId', null);
|
||||
this.search(this.get('topicTitle'));
|
||||
}.observes('topicTitle'),
|
||||
|
||||
topicsChanged: function() {
|
||||
var topics = this.get('topics');
|
||||
if (topics) {
|
||||
this.set('noResults', topics.length === 0);
|
||||
}
|
||||
this.set('loading', false);
|
||||
}.observes('topics'),
|
||||
|
||||
search: Discourse.debounce(function(title) {
|
||||
var chooseTopicView = this;
|
||||
Discourse.Search.forTerm(title, 'topic').then(function (facets) {
|
||||
if (facets && facets[0] && facets[0].results) {
|
||||
chooseTopicView.set('topics', facets[0].results);
|
||||
} else {
|
||||
chooseTopicView.set('topics', null);
|
||||
chooseTopicView.set('loading', false);
|
||||
}
|
||||
});
|
||||
}, 300),
|
||||
|
||||
chooseTopic: function (topic) {
|
||||
var topicId = Em.get(topic, 'id');
|
||||
this.set('selectedTopicId', topicId);
|
||||
|
||||
Em.run.next(function() {
|
||||
$('#choose-topic-' + topicId).prop('checked', 'true');
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
A modal view for handling moving of posts to an existing topic
|
||||
|
||||
@class MoveSelectedExistingTopicView
|
||||
@extends Discourse.ModalBodyView
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.MoveSelectedExistingTopicView = Discourse.ModalBodyView.extend(Discourse.SelectedPostsCount, {
|
||||
templateName: 'modal/move_selected_existing_topic',
|
||||
title: Em.String.i18n('topic.move_selected.existing_topic.title'),
|
||||
|
||||
buttonDisabled: function() {
|
||||
if (this.get('saving')) return true;
|
||||
return this.blank('selectedTopicId');
|
||||
}.property('selectedTopicId', 'saving'),
|
||||
|
||||
buttonTitle: function() {
|
||||
if (this.get('saving')) return Em.String.i18n('saving');
|
||||
return Em.String.i18n('topic.move_selected.title');
|
||||
}.property('saving'),
|
||||
|
||||
movePostsToExistingTopic: function() {
|
||||
this.set('saving', true);
|
||||
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||
var moveSelectedView = this;
|
||||
|
||||
Discourse.Topic.movePosts(this.get('topic.id'), {
|
||||
destination_topic_id: this.get('selectedTopicId'),
|
||||
post_ids: postIds
|
||||
}).then(function(result) {
|
||||
// Posts moved
|
||||
$('#discourse-modal').modal('hide');
|
||||
moveSelectedView.get('topicController').toggleMultiSelect();
|
||||
Em.run.next(function() { Discourse.URL.routeTo(result.url); });
|
||||
}, function() {
|
||||
// Error moving posts
|
||||
moveSelectedView.flash(Em.String.i18n('topic.move_selected.error'));
|
||||
moveSelectedView.set('saving', false);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
A modal view for handling moving of posts to a new topic
|
||||
|
||||
@class MoveSelectedNewTopicView
|
||||
@extends Discourse.ModalBodyView
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.MoveSelectedNewTopicView = Discourse.ModalBodyView.extend(Discourse.SelectedPostsCount, {
|
||||
templateName: 'modal/move_selected_new_topic',
|
||||
title: Em.String.i18n('topic.move_selected.new_topic.title'),
|
||||
saving: false,
|
||||
|
||||
buttonDisabled: function() {
|
||||
if (this.get('saving')) return true;
|
||||
return this.blank('topicName');
|
||||
}.property('saving', 'topicName'),
|
||||
|
||||
buttonTitle: function() {
|
||||
if (this.get('saving')) return Em.String.i18n('saving');
|
||||
return Em.String.i18n('topic.move_selected.title');
|
||||
}.property('saving'),
|
||||
|
||||
movePostsToNewTopic: function() {
|
||||
this.set('saving', true);
|
||||
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||
var moveSelectedView = this;
|
||||
|
||||
Discourse.Topic.movePosts(this.get('topic.id'), {
|
||||
title: this.get('topicName'),
|
||||
post_ids: postIds
|
||||
}).then(function(result) {
|
||||
// Posts moved
|
||||
$('#discourse-modal').modal('hide');
|
||||
moveSelectedView.get('topicController').toggleMultiSelect();
|
||||
Em.run.next(function() { Discourse.URL.routeTo(result.url); });
|
||||
}, function() {
|
||||
// Error moving posts
|
||||
moveSelectedView.flash(Em.String.i18n('topic.move_selected.error'));
|
||||
moveSelectedView.set('saving', false);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1,49 +1,37 @@
|
|||
/**
|
||||
A modal view for handling moving of posts to a new topic
|
||||
A modal view for handling moving of posts.
|
||||
|
||||
@class MoveSelectedView
|
||||
@extends Discourse.ModalBodyView
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.MoveSelectedView = Discourse.ModalBodyView.extend({
|
||||
Discourse.MoveSelectedView = Discourse.ModalBodyView.extend(Discourse.SelectedPostsCount, {
|
||||
templateName: 'modal/move_selected',
|
||||
title: Em.String.i18n('topic.move_selected.title'),
|
||||
saving: false,
|
||||
|
||||
selectedCount: function() {
|
||||
if (!this.get('selectedPosts')) return 0;
|
||||
return this.get('selectedPosts').length;
|
||||
}.property('selectedPosts'),
|
||||
showMoveNewTopic: function() {
|
||||
var modalController = this.get('controller');
|
||||
if (!modalController) return;
|
||||
|
||||
buttonDisabled: function() {
|
||||
if (this.get('saving')) return true;
|
||||
return this.blank('topicName');
|
||||
}.property('saving', 'topicName'),
|
||||
modalController.show(Discourse.MoveSelectedNewTopicView.create({
|
||||
topicController: this.get('topicController'),
|
||||
topic: this.get('topic'),
|
||||
selectedPosts: this.get('selectedPosts')
|
||||
}));
|
||||
},
|
||||
|
||||
buttonTitle: function() {
|
||||
if (this.get('saving')) return Em.String.i18n('saving');
|
||||
return Em.String.i18n('topic.move_selected.title');
|
||||
}.property('saving'),
|
||||
showMoveExistingTopic: function() {
|
||||
var modalController = this.get('controller');
|
||||
if (!modalController) return;
|
||||
|
||||
movePosts: function() {
|
||||
this.set('saving', true);
|
||||
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||
var moveSelectedView = this;
|
||||
|
||||
Discourse.Topic.movePosts(this.get('topic.id'), this.get('topicName'), postIds).then(function(result) {
|
||||
// Posts moved
|
||||
$('#discourse-modal').modal('hide');
|
||||
moveSelectedView.get('topicController').toggleMultiSelect();
|
||||
Em.run.next(function() { Discourse.URL.routeTo(result.url); });
|
||||
}, function() {
|
||||
// Error moving posts
|
||||
moveSelectedView.flash(Em.String.i18n('topic.move_selected.error'));
|
||||
moveSelectedView.set('saving', false);
|
||||
});
|
||||
return false;
|
||||
modalController.show(Discourse.MoveSelectedExistingTopicView.create({
|
||||
topicController: this.get('topicController'),
|
||||
topic: this.get('topic'),
|
||||
selectedPosts: this.get('selectedPosts')
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ Discourse.PostView = Discourse.View.extend({
|
|||
},
|
||||
|
||||
selectText: function() {
|
||||
return this.get('post.selected') ? Em.String.i18n('topic.multi_select.selected', { count: this.get('controller.selectedCount') }) : Em.String.i18n('topic.multi_select.select');
|
||||
}.property('post.selected', 'controller.selectedCount'),
|
||||
return this.get('post.selected') ? Em.String.i18n('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : Em.String.i18n('topic.multi_select.select');
|
||||
}.property('post.selected', 'controller.selectedPostsCount'),
|
||||
|
||||
repliesHidden: function() {
|
||||
return !this.get('repliesShown');
|
||||
|
|
|
@ -46,6 +46,13 @@ Discourse.SearchView = Discourse.View.extend({
|
|||
return this.set('selectedIndex', 0);
|
||||
}.observes('term', 'typeFilter'),
|
||||
|
||||
searchTerm: Discourse.debouncePromise(function(term, typeFilter) {
|
||||
var searchView = this;
|
||||
return Discourse.Search.forTerm(term, typeFilter).then(function(results) {
|
||||
searchView.set('results', results);
|
||||
});
|
||||
}, 300),
|
||||
|
||||
showCancelFilter: function() {
|
||||
if (this.get('loading')) return false;
|
||||
return this.present('typeFilter');
|
||||
|
@ -56,7 +63,7 @@ Discourse.SearchView = Discourse.View.extend({
|
|||
}.observes('term'),
|
||||
|
||||
// We can re-order them based on the context
|
||||
content: (function() {
|
||||
content: function() {
|
||||
var index, order, path, results, results_hashed;
|
||||
if (results = this.get('results')) {
|
||||
// Make it easy to find the results by type
|
||||
|
@ -78,29 +85,17 @@ Discourse.SearchView = Discourse.View.extend({
|
|||
});
|
||||
}
|
||||
return results;
|
||||
}).property('results'),
|
||||
}.property('results'),
|
||||
|
||||
updateProgress: (function() {
|
||||
updateProgress: function() {
|
||||
var results;
|
||||
if (results = this.get('results')) {
|
||||
this.set('noResults', results.length === 0);
|
||||
}
|
||||
return this.set('loading', false);
|
||||
}).observes('results'),
|
||||
}.observes('results'),
|
||||
|
||||
searchTerm: Discourse.debouncePromise(function(term, typeFilter) {
|
||||
var searchView = this;
|
||||
return Discourse.ajax('/search', {
|
||||
data: {
|
||||
term: term,
|
||||
type_filter: typeFilter
|
||||
}
|
||||
}).then(function(results) {
|
||||
searchView.set('results', results);
|
||||
});
|
||||
}, 300),
|
||||
|
||||
resultCount: (function() {
|
||||
resultCount: function() {
|
||||
var count;
|
||||
if (this.blank('content')) return 0;
|
||||
count = 0;
|
||||
|
@ -108,7 +103,7 @@ Discourse.SearchView = Discourse.View.extend({
|
|||
count += result.results.length;
|
||||
});
|
||||
return count;
|
||||
}).property('content'),
|
||||
}.property('content'),
|
||||
|
||||
moreOfType: function(type) {
|
||||
this.set('typeFilter', type);
|
||||
|
|
|
@ -157,6 +157,20 @@
|
|||
}
|
||||
|
||||
#move-selected {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input[type=radio] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-top: 20px;
|
||||
input[type=text] {
|
||||
|
|
|
@ -39,6 +39,14 @@
|
|||
border: 1px solid lighten($blue, 40%);
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
button {
|
||||
width: 160px;
|
||||
margin: 4px auto;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -127,18 +127,22 @@ class TopicsController < ApplicationController
|
|||
end
|
||||
|
||||
def move_posts
|
||||
requires_parameters(:title, :post_ids)
|
||||
requires_parameters(:post_ids)
|
||||
|
||||
topic = Topic.where(id: params[:topic_id]).first
|
||||
guardian.ensure_can_move_posts!(topic)
|
||||
|
||||
# Move the posts
|
||||
new_topic = topic.move_posts(current_user, params[:title], params[:post_ids].map {|p| p.to_i})
|
||||
args = {}
|
||||
args[:title] = params[:title] if params[:title].present?
|
||||
args[:destination_topic_id] = params[:destination_topic_id].to_i if params[:destination_topic_id].present?
|
||||
|
||||
if new_topic.present?
|
||||
render json: {success: true, url: new_topic.relative_url}
|
||||
dest_topic = topic.move_posts(current_user, params[:post_ids].map {|p| p.to_i}, args)
|
||||
if dest_topic.present?
|
||||
render json: {success: true, url: dest_topic.relative_url}
|
||||
else
|
||||
render json: {success: false}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def clear_pin
|
||||
|
|
|
@ -447,30 +447,53 @@ class Topic < ActiveRecord::Base
|
|||
invite
|
||||
end
|
||||
|
||||
def move_posts(moved_by, new_title, post_ids)
|
||||
topic = nil
|
||||
def move_posts_to_topic(post_ids, destination_topic)
|
||||
to_move = posts.where(id: post_ids).order(:created_at)
|
||||
raise Discourse::InvalidParameters.new(:post_ids) if to_move.blank?
|
||||
|
||||
first_post_number = nil
|
||||
Topic.transaction do
|
||||
topic = Topic.create(user: moved_by, title: new_title, category: category)
|
||||
|
||||
to_move = posts.where(id: post_ids).order(:created_at)
|
||||
raise Discourse::InvalidParameters.new(:post_ids) if to_move.blank?
|
||||
# Find the max post number in the topic
|
||||
max_post_number = destination_topic.posts.maximum(:post_number) || 0
|
||||
|
||||
to_move.each_with_index do |post, i|
|
||||
first_post_number ||= post.post_number
|
||||
row_count = Post.update_all ["post_number = :post_number, topic_id = :topic_id, sort_order = :post_number", post_number: i+1, topic_id: topic.id], id: post.id, topic_id: id
|
||||
row_count = Post.update_all ["post_number = :post_number, topic_id = :topic_id, sort_order = :post_number", post_number: max_post_number+i+1, topic_id: destination_topic.id], id: post.id, topic_id: id
|
||||
|
||||
# We raise an error if any of the posts can't be moved
|
||||
raise Discourse::InvalidParameters.new(:post_ids) if row_count == 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
first_post_number
|
||||
end
|
||||
|
||||
def move_posts(moved_by, post_ids, opts)
|
||||
|
||||
topic = nil
|
||||
first_post_number = nil
|
||||
|
||||
if opts[:title].present?
|
||||
# If we're moving to a new topic...
|
||||
Topic.transaction do
|
||||
topic = Topic.create(user: moved_by, title: opts[:title], category: category)
|
||||
first_post_number = move_posts_to_topic(post_ids, topic)
|
||||
end
|
||||
|
||||
elsif opts[:destination_topic_id].present?
|
||||
# If we're moving to an existing topic...
|
||||
|
||||
topic = Topic.where(id: opts[:destination_topic_id]).first
|
||||
Guardian.new(moved_by).ensure_can_see!(topic)
|
||||
first_post_number = move_posts_to_topic(post_ids, topic)
|
||||
|
||||
# Update denormalized values since we've manually moved stuff
|
||||
end
|
||||
|
||||
# Add a moderator post explaining that the post was moved
|
||||
if topic.present?
|
||||
topic_url = "#{Discourse.base_url}#{topic.relative_url}"
|
||||
topic_link = "[#{new_title}](#{topic_url})"
|
||||
topic_link = "[#{topic.title}](#{topic_url})"
|
||||
|
||||
add_moderator_post(moved_by, I18n.t("move_posts.moderator_post", count: post_ids.size, topic_link: topic_link), post_number: first_post_number)
|
||||
Jobs.enqueue(:notify_moved_posts, post_ids: post_ids, moved_by_id: moved_by.id)
|
||||
|
|
|
@ -50,6 +50,12 @@ en:
|
|||
saving: "Saving..."
|
||||
saved: "Saved!"
|
||||
|
||||
choose_topic:
|
||||
none_found: "No topics found."
|
||||
title:
|
||||
search: "Search for a Topic:"
|
||||
placeholder: "type the topic title here"
|
||||
|
||||
user_action:
|
||||
user_posted_topic: "<a href='{{userUrl}}'>{{user}}</a> posted <a href='{{topicUrl}}'>the topic</a>"
|
||||
you_posted_topic: "<a href='{{userUrl}}'>You</a> posted <a href='{{topicUrl}}'>the topic</a>"
|
||||
|
@ -573,11 +579,23 @@ en:
|
|||
|
||||
move_selected:
|
||||
title: "Move Selected Posts"
|
||||
topic_name: "New Topic Name:"
|
||||
error: "Sorry, there was an error moving those posts."
|
||||
instructions:
|
||||
one: "You are about to create a new topic and populate it with the post you've selected."
|
||||
other: "You are about to create a new topic and populate it with the <b>{{count}}</b> posts you've selected."
|
||||
one: "How would you like to move this post?"
|
||||
other: "How would you like to move the <b>{{count}}</b> posts you've created?"
|
||||
|
||||
new_topic:
|
||||
title: "Move Selected Posts to a New Topic"
|
||||
topic_name: "New Topic Name:"
|
||||
instructions:
|
||||
one: "You are about to create a new topic and populate it with the post you've selected."
|
||||
other: "You are about to create a new topic and populate it with the <b>{{count}}</b> posts you've selected."
|
||||
|
||||
existing_topic:
|
||||
title: "Move Selected Posts to an Existing Topic"
|
||||
instructions:
|
||||
one: "Please choose the topic you'd like to move that post to."
|
||||
other: "Please choose the topic you'd like to move those <b>{{count}}</b> posts to."
|
||||
|
||||
multi_select:
|
||||
select: 'select'
|
||||
|
|
|
@ -171,7 +171,6 @@ module Search
|
|||
end
|
||||
|
||||
# Remove attributes when we know they don't matter
|
||||
row.delete('id')
|
||||
if type == 'user'
|
||||
row['avatar_template'] = User.avatar_template(row['email'])
|
||||
end
|
||||
|
|
|
@ -7,15 +7,11 @@ describe TopicsController do
|
|||
lambda { xhr :post, :move_posts, topic_id: 111, title: 'blah', post_ids: [1,2,3] }.should raise_error(Discourse::NotLoggedIn)
|
||||
end
|
||||
|
||||
describe 'when logged in' do
|
||||
describe 'moving to a new topic' do
|
||||
let!(:user) { log_in(:moderator) }
|
||||
let(:p1) { Fabricate(:post, user: user) }
|
||||
let(:topic) { p1.topic }
|
||||
|
||||
it "raises an error without a title" do
|
||||
lambda { xhr :post, :move_posts, topic_id: topic.id, post_ids: [1,2,3] }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "raises an error without postIds" do
|
||||
lambda { xhr :post, :move_posts, topic_id: topic.id, title: 'blah' }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
@ -30,20 +26,15 @@ describe TopicsController do
|
|||
let(:p2) { Fabricate(:post, user: user) }
|
||||
|
||||
before do
|
||||
Topic.any_instance.expects(:move_posts).with(user, 'blah', [p2.id]).returns(topic)
|
||||
Topic.any_instance.expects(:move_posts).with(user, [p2.id], title: 'blah').returns(topic)
|
||||
xhr :post, :move_posts, topic_id: topic.id, title: 'blah', post_ids: [p2.id]
|
||||
end
|
||||
|
||||
it "returns success" do
|
||||
response.should be_success
|
||||
end
|
||||
|
||||
it "has a JSON response" do
|
||||
::JSON.parse(response.body)['success'].should be_true
|
||||
end
|
||||
|
||||
it "has a url" do
|
||||
::JSON.parse(response.body)['url'].should be_present
|
||||
result = ::JSON.parse(response.body)
|
||||
result['success'].should be_true
|
||||
result['url'].should be_present
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -51,24 +42,56 @@ describe TopicsController do
|
|||
let(:p2) { Fabricate(:post, user: user) }
|
||||
|
||||
before do
|
||||
Topic.any_instance.expects(:move_posts).with(user, 'blah', [p2.id]).returns(nil)
|
||||
Topic.any_instance.expects(:move_posts).with(user, [p2.id], title: 'blah').returns(nil)
|
||||
xhr :post, :move_posts, topic_id: topic.id, title: 'blah', post_ids: [p2.id]
|
||||
end
|
||||
|
||||
it "returns JSON with a false success" do
|
||||
response.should be_success
|
||||
result = ::JSON.parse(response.body)
|
||||
result['success'].should be_false
|
||||
result['url'].should be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'moving to an existing topic' do
|
||||
let!(:user) { log_in(:moderator) }
|
||||
let(:p1) { Fabricate(:post, user: user) }
|
||||
let(:topic) { p1.topic }
|
||||
let(:dest_topic) { Fabricate(:topic) }
|
||||
|
||||
context 'success' do
|
||||
let(:p2) { Fabricate(:post, user: user) }
|
||||
|
||||
before do
|
||||
Topic.any_instance.expects(:move_posts).with(user, [p2.id], destination_topic_id: dest_topic.id).returns(topic)
|
||||
xhr :post, :move_posts, topic_id: topic.id, post_ids: [p2.id], destination_topic_id: dest_topic.id
|
||||
end
|
||||
|
||||
it "returns success" do
|
||||
response.should be_success
|
||||
result = ::JSON.parse(response.body)
|
||||
result['success'].should be_true
|
||||
result['url'].should be_present
|
||||
end
|
||||
|
||||
it "has success in the JSON" do
|
||||
::JSON.parse(response.body)['success'].should be_false
|
||||
end
|
||||
|
||||
it "has a url" do
|
||||
::JSON.parse(response.body)['url'].should be_blank
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'failure' do
|
||||
let(:p2) { Fabricate(:post, user: user) }
|
||||
|
||||
before do
|
||||
Topic.any_instance.expects(:move_posts).with(user, [p2.id], destination_topic_id: dest_topic.id).returns(nil)
|
||||
xhr :post, :move_posts, topic_id: topic.id, destination_topic_id: dest_topic.id, post_ids: [p2.id]
|
||||
end
|
||||
|
||||
it "returns JSON with a false success" do
|
||||
response.should be_success
|
||||
result = ::JSON.parse(response.body)
|
||||
result['success'].should be_false
|
||||
result['url'].should be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -222,12 +222,12 @@ describe Topic do
|
|||
it "enqueues a job to notify users" do
|
||||
topic.stubs(:add_moderator_post)
|
||||
Jobs.expects(:enqueue).with(:notify_moved_posts, post_ids: [p1.id, p4.id], moved_by_id: user.id)
|
||||
topic.move_posts(user, "new testing topic name", [p1.id, p4.id])
|
||||
topic.move_posts(user, [p1.id, p4.id], title: "new testing topic name")
|
||||
end
|
||||
|
||||
it "adds a moderator post at the location of the first moved post" do
|
||||
topic.expects(:add_moderator_post).with(user, instance_of(String), has_entries(post_number: 2))
|
||||
topic.move_posts(user, "new testing topic name", [p2.id, p4.id])
|
||||
topic.move_posts(user, [p2.id, p4.id], title: "new testing topic name")
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -235,52 +235,97 @@ describe Topic do
|
|||
context "errors" do
|
||||
|
||||
it "raises an error when one of the posts doesn't exist" do
|
||||
lambda { topic.move_posts(user, "new testing topic name", [1003]) }.should raise_error(Discourse::InvalidParameters)
|
||||
lambda { topic.move_posts(user, [1003], title: "new testing topic name") }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "raises an error if no posts were moved" do
|
||||
lambda { topic.move_posts(user, "new testing topic name", []) }.should raise_error(Discourse::InvalidParameters)
|
||||
lambda { topic.move_posts(user, [], title: "new testing topic name") }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "afterwards" do
|
||||
context "successfully moved" do
|
||||
before do
|
||||
topic.expects(:add_moderator_post)
|
||||
TopicUser.update_last_read(user, topic.id, p4.post_number, 0)
|
||||
end
|
||||
|
||||
let!(:new_topic) { topic.move_posts(user, "new testing topic name", [p2.id, p4.id]) }
|
||||
context "to a new topic" do
|
||||
let!(:new_topic) { topic.move_posts(user, [p2.id, p4.id], title: "new testing topic name") }
|
||||
|
||||
it "moved correctly" do
|
||||
TopicUser.where(user_id: user.id, topic_id: topic.id).first.last_read_post_number.should == p3.post_number
|
||||
it "moved correctly" do
|
||||
TopicUser.where(user_id: user.id, topic_id: topic.id).first.last_read_post_number.should == p3.post_number
|
||||
|
||||
new_topic.should be_present
|
||||
new_topic.featured_user1_id.should == another_user.id
|
||||
new_topic.like_count.should == 1
|
||||
new_topic.category.should == category
|
||||
topic.featured_user1_id.should be_blank
|
||||
new_topic.posts.should =~ [p2, p4]
|
||||
new_topic.should be_present
|
||||
new_topic.featured_user1_id.should == another_user.id
|
||||
new_topic.like_count.should == 1
|
||||
new_topic.category.should == category
|
||||
topic.featured_user1_id.should be_blank
|
||||
new_topic.posts.should =~ [p2, p4]
|
||||
|
||||
new_topic.reload
|
||||
new_topic.posts_count.should == 2
|
||||
new_topic.highest_post_number.should == 2
|
||||
new_topic.reload
|
||||
new_topic.posts_count.should == 2
|
||||
new_topic.highest_post_number.should == 2
|
||||
|
||||
p2.reload
|
||||
p2.sort_order.should == 1
|
||||
p2.post_number.should == 1
|
||||
p2.reload
|
||||
p2.sort_order.should == 1
|
||||
p2.post_number.should == 1
|
||||
|
||||
p4.reload
|
||||
p4.post_number.should == 2
|
||||
p4.sort_order.should == 2
|
||||
p4.reload
|
||||
p4.post_number.should == 2
|
||||
p4.sort_order.should == 2
|
||||
|
||||
topic.reload
|
||||
topic.featured_user1_id.should be_blank
|
||||
topic.like_count.should == 0
|
||||
topic.posts_count.should == 2
|
||||
topic.posts.should =~ [p1, p3]
|
||||
topic.highest_post_number.should == p3.post_number
|
||||
topic.reload
|
||||
topic.featured_user1_id.should be_blank
|
||||
topic.like_count.should == 0
|
||||
topic.posts_count.should == 2
|
||||
topic.posts.should =~ [p1, p3]
|
||||
topic.highest_post_number.should == p3.post_number
|
||||
end
|
||||
end
|
||||
|
||||
context "to an existing topic" do
|
||||
|
||||
let!(:destination_topic) { Fabricate(:topic, user: user ) }
|
||||
let!(:destination_op) { Fabricate(:post, topic: destination_topic, user: user) }
|
||||
let!(:moved_to) { topic.move_posts(user, [p2.id, p4.id], destination_topic_id: destination_topic.id )}
|
||||
|
||||
it "moved correctly" do
|
||||
moved_to.should == destination_topic
|
||||
|
||||
# Check out new topic
|
||||
moved_to.reload
|
||||
moved_to.posts_count.should == 3
|
||||
moved_to.highest_post_number.should == 3
|
||||
moved_to.featured_user1_id.should == another_user.id
|
||||
moved_to.like_count.should == 1
|
||||
moved_to.category.should be_blank
|
||||
|
||||
# Posts should be re-ordered
|
||||
p2.reload
|
||||
p2.sort_order.should == 2
|
||||
p2.post_number.should == 2
|
||||
|
||||
p4.reload
|
||||
p4.post_number.should == 3
|
||||
p4.sort_order.should == 3
|
||||
|
||||
# Check out the original topic
|
||||
topic.reload
|
||||
topic.posts_count.should == 2
|
||||
topic.highest_post_number.should == 3
|
||||
topic.featured_user1_id.should be_blank
|
||||
topic.like_count.should == 0
|
||||
topic.posts_count.should == 2
|
||||
topic.posts.should =~ [p1, p3]
|
||||
topic.highest_post_number.should == p3.post_number
|
||||
|
||||
# Should update last reads
|
||||
TopicUser.where(user_id: user.id, topic_id: topic.id).first.last_read_post_number.should == p3.post_number
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue