Experimental: Interface to Move Posts to an Existing Topic

This commit is contained in:
Robin Ward 2013-05-08 13:33:58 -04:00
parent f8e8538e19
commit cf01c98d81
24 changed files with 511 additions and 151 deletions

View File

@ -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 }
});
}
}

View File

@ -6,7 +6,7 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.TopicController = Discourse.ObjectController.extend({ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
userFilters: new Em.Set(), userFilters: new Em.Set(),
multiSelect: false, multiSelect: false,
bestOf: false, bestOf: false,
@ -22,11 +22,6 @@ Discourse.TopicController = Discourse.ObjectController.extend({
return posts.filterProperty('selected'); return posts.filterProperty('selected');
}.property('content.posts.@each.selected'), }.property('content.posts.@each.selected'),
selectedCount: function() {
if (!this.get('selectedPosts')) return 0;
return this.get('selectedPosts').length;
}.property('selectedPosts'),
canMoveSelected: function() { canMoveSelected: function() {
if (!this.get('content.can_move_posts')) return false; 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. // 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() { deleteSelected: function() {
var topicController = this; 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) { if (result) {
var selectedPosts = topicController.get('selectedPosts'); var selectedPosts = topicController.get('selectedPosts');
Discourse.Post.deleteMany(selectedPosts); Discourse.Post.deleteMany(selectedPosts);

View File

@ -56,6 +56,26 @@ Handlebars.registerHelper('categoryLink', function(property, options) {
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category)); 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 Produces a bound link to a category

View File

@ -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')
});

View File

@ -449,12 +449,10 @@ Discourse.Topic.reopenClass({
}); });
}, },
// Create a topic from posts movePosts: function(topicId, opts) {
movePosts: function(topicId, title, postIds) {
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", { var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
type: 'POST', type: 'POST',
data: { title: title, post_ids: postIds } data: opts
}).then(function (result) { }).then(function (result) {
if (result.success) return result; if (result.success) return result;
promise.reject(); promise.reject();

View File

@ -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}}

View File

@ -1,20 +1,8 @@
<div id='move-selected' class="modal-body"> <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> <button {{action showMoveNewTopic target="view"}} class="btn">{{i18n topic.move_selected.new_topic.title}}</button>
<label>{{i18n topic.move_selected.topic_name}}</label> <button {{action showMoveExistingTopic target="view"}} class="btn">{{i18n topic.move_selected.existing_topic.title}}</button>
{{view Discourse.TextField valueBinding="view.topicName" placeholderKey="composer.title_placeholder"}}
</form>
</div> </div>
<div class="modal-footer">
<button class='btn btn-primary' {{bindAttr disabled="view.buttonDisabled"}} {{action movePosts target="view"}}>{{view.buttonTitle}}</button>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -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}} {{#if canDeleteSelected}}
<button class='btn' {{action deleteSelected}}><i class='icon icon-trash'></i> {{i18n topic.multi_select.delete}}</button> <button class='btn' {{action deleteSelected}}><i class='icon icon-trash'></i> {{i18n topic.multi_select.delete}}</button>
@ -6,6 +6,6 @@
{{#if canMoveSelected}} {{#if canMoveSelected}}
<button class='btn' {{action moveSelected}}><i class='icon icon-move'></i> {{i18n topic.multi_select.move}}</button> <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> <p class='cancel'><a href='#' {{action toggleMultiSelect}}>{{i18n topic.multi_select.cancel}}</a></p>

View File

@ -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;
}
});

View File

@ -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;
}
});

View File

@ -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;
}
});

View File

@ -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 @class MoveSelectedView
@extends Discourse.ModalBodyView @extends Discourse.ModalBodyView
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.MoveSelectedView = Discourse.ModalBodyView.extend({ Discourse.MoveSelectedView = Discourse.ModalBodyView.extend(Discourse.SelectedPostsCount, {
templateName: 'modal/move_selected', templateName: 'modal/move_selected',
title: Em.String.i18n('topic.move_selected.title'), title: Em.String.i18n('topic.move_selected.title'),
saving: false,
selectedCount: function() { showMoveNewTopic: function() {
if (!this.get('selectedPosts')) return 0; var modalController = this.get('controller');
return this.get('selectedPosts').length; if (!modalController) return;
}.property('selectedPosts'),
buttonDisabled: function() { modalController.show(Discourse.MoveSelectedNewTopicView.create({
if (this.get('saving')) return true; topicController: this.get('topicController'),
return this.blank('topicName'); topic: this.get('topic'),
}.property('saving', 'topicName'), selectedPosts: this.get('selectedPosts')
}));
},
buttonTitle: function() { showMoveExistingTopic: function() {
if (this.get('saving')) return Em.String.i18n('saving'); var modalController = this.get('controller');
return Em.String.i18n('topic.move_selected.title'); if (!modalController) return;
}.property('saving'),
movePosts: function() { modalController.show(Discourse.MoveSelectedExistingTopicView.create({
this.set('saving', true); topicController: this.get('topicController'),
topic: this.get('topic'),
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }); selectedPosts: this.get('selectedPosts')
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;
} }
}); });

View File

@ -50,8 +50,8 @@ Discourse.PostView = Discourse.View.extend({
}, },
selectText: function() { 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'); 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.selectedCount'), }.property('post.selected', 'controller.selectedPostsCount'),
repliesHidden: function() { repliesHidden: function() {
return !this.get('repliesShown'); return !this.get('repliesShown');

View File

@ -46,6 +46,13 @@ Discourse.SearchView = Discourse.View.extend({
return this.set('selectedIndex', 0); return this.set('selectedIndex', 0);
}.observes('term', 'typeFilter'), }.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() { showCancelFilter: function() {
if (this.get('loading')) return false; if (this.get('loading')) return false;
return this.present('typeFilter'); return this.present('typeFilter');
@ -56,7 +63,7 @@ Discourse.SearchView = Discourse.View.extend({
}.observes('term'), }.observes('term'),
// We can re-order them based on the context // We can re-order them based on the context
content: (function() { content: function() {
var index, order, path, results, results_hashed; var index, order, path, results, results_hashed;
if (results = this.get('results')) { if (results = this.get('results')) {
// Make it easy to find the results by type // Make it easy to find the results by type
@ -78,29 +85,17 @@ Discourse.SearchView = Discourse.View.extend({
}); });
} }
return results; return results;
}).property('results'), }.property('results'),
updateProgress: (function() { updateProgress: function() {
var results; var results;
if (results = this.get('results')) { if (results = this.get('results')) {
this.set('noResults', results.length === 0); this.set('noResults', results.length === 0);
} }
return this.set('loading', false); return this.set('loading', false);
}).observes('results'), }.observes('results'),
searchTerm: Discourse.debouncePromise(function(term, typeFilter) { resultCount: function() {
var searchView = this;
return Discourse.ajax('/search', {
data: {
term: term,
type_filter: typeFilter
}
}).then(function(results) {
searchView.set('results', results);
});
}, 300),
resultCount: (function() {
var count; var count;
if (this.blank('content')) return 0; if (this.blank('content')) return 0;
count = 0; count = 0;
@ -108,7 +103,7 @@ Discourse.SearchView = Discourse.View.extend({
count += result.results.length; count += result.results.length;
}); });
return count; return count;
}).property('content'), }.property('content'),
moreOfType: function(type) { moreOfType: function(type) {
this.set('typeFilter', type); this.set('typeFilter', type);

View File

@ -157,6 +157,20 @@
} }
#move-selected { #move-selected {
p {
margin-top: 0;
}
input[type=radio] {
margin-right: 10px;
}
button {
margin-top: 10px;
display: block;
width: 300px;
}
form { form {
margin-top: 20px; margin-top: 20px;
input[type=text] { input[type=text] {

View File

@ -39,6 +39,14 @@
border: 1px solid lighten($blue, 40%); border: 1px solid lighten($blue, 40%);
padding: 5px; padding: 5px;
margin-bottom: 5px; margin-bottom: 5px;
button {
width: 160px;
margin: 4px auto;
display: inline-block;
text-align: left;
}
&.hidden { &.hidden {
display: none; display: none;
} }

View File

@ -127,18 +127,22 @@ class TopicsController < ApplicationController
end end
def move_posts def move_posts
requires_parameters(:title, :post_ids) requires_parameters(:post_ids)
topic = Topic.where(id: params[:topic_id]).first topic = Topic.where(id: params[:topic_id]).first
guardian.ensure_can_move_posts!(topic) guardian.ensure_can_move_posts!(topic)
# Move the posts args = {}
new_topic = topic.move_posts(current_user, params[:title], params[:post_ids].map {|p| p.to_i}) 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? dest_topic = topic.move_posts(current_user, params[:post_ids].map {|p| p.to_i}, args)
render json: {success: true, url: new_topic.relative_url} if dest_topic.present?
render json: {success: true, url: dest_topic.relative_url}
else else
render json: {success: false} render json: {success: false}
end end
end end
def clear_pin def clear_pin

View File

@ -447,30 +447,53 @@ class Topic < ActiveRecord::Base
invite invite
end end
def move_posts(moved_by, new_title, post_ids) def move_posts_to_topic(post_ids, destination_topic)
topic = nil to_move = posts.where(id: post_ids).order(:created_at)
raise Discourse::InvalidParameters.new(:post_ids) if to_move.blank?
first_post_number = nil first_post_number = nil
Topic.transaction do Topic.transaction do
topic = Topic.create(user: moved_by, title: new_title, category: category) # Find the max post number in the topic
max_post_number = destination_topic.posts.maximum(:post_number) || 0
to_move = posts.where(id: post_ids).order(:created_at)
raise Discourse::InvalidParameters.new(:post_ids) if to_move.blank?
to_move.each_with_index do |post, i| to_move.each_with_index do |post, i|
first_post_number ||= post.post_number 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 # We raise an error if any of the posts can't be moved
raise Discourse::InvalidParameters.new(:post_ids) if row_count == 0 raise Discourse::InvalidParameters.new(:post_ids) if row_count == 0
end 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 end
# Add a moderator post explaining that the post was moved # Add a moderator post explaining that the post was moved
if topic.present? if topic.present?
topic_url = "#{Discourse.base_url}#{topic.relative_url}" 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) 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) Jobs.enqueue(:notify_moved_posts, post_ids: post_ids, moved_by_id: moved_by.id)

View File

@ -50,6 +50,12 @@ en:
saving: "Saving..." saving: "Saving..."
saved: "Saved!" saved: "Saved!"
choose_topic:
none_found: "No topics found."
title:
search: "Search for a Topic:"
placeholder: "type the topic title here"
user_action: user_action:
user_posted_topic: "<a href='{{userUrl}}'>{{user}}</a> posted <a href='{{topicUrl}}'>the topic</a>" 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>" you_posted_topic: "<a href='{{userUrl}}'>You</a> posted <a href='{{topicUrl}}'>the topic</a>"
@ -573,11 +579,23 @@ en:
move_selected: move_selected:
title: "Move Selected Posts" title: "Move Selected Posts"
topic_name: "New Topic Name:"
error: "Sorry, there was an error moving those posts." error: "Sorry, there was an error moving those posts."
instructions: instructions:
one: "You are about to create a new topic and populate it with the post you've selected." one: "How would you like to move this post?"
other: "You are about to create a new topic and populate it with the <b>{{count}}</b> posts you've selected." 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: multi_select:
select: 'select' select: 'select'

View File

@ -171,7 +171,6 @@ module Search
end end
# Remove attributes when we know they don't matter # Remove attributes when we know they don't matter
row.delete('id')
if type == 'user' if type == 'user'
row['avatar_template'] = User.avatar_template(row['email']) row['avatar_template'] = User.avatar_template(row['email'])
end end

View File

@ -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) lambda { xhr :post, :move_posts, topic_id: 111, title: 'blah', post_ids: [1,2,3] }.should raise_error(Discourse::NotLoggedIn)
end end
describe 'when logged in' do describe 'moving to a new topic' do
let!(:user) { log_in(:moderator) } let!(:user) { log_in(:moderator) }
let(:p1) { Fabricate(:post, user: user) } let(:p1) { Fabricate(:post, user: user) }
let(:topic) { p1.topic } 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 it "raises an error without postIds" do
lambda { xhr :post, :move_posts, topic_id: topic.id, title: 'blah' }.should raise_error(Discourse::InvalidParameters) lambda { xhr :post, :move_posts, topic_id: topic.id, title: 'blah' }.should raise_error(Discourse::InvalidParameters)
end end
@ -30,20 +26,15 @@ describe TopicsController do
let(:p2) { Fabricate(:post, user: user) } let(:p2) { Fabricate(:post, user: user) }
before do 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] xhr :post, :move_posts, topic_id: topic.id, title: 'blah', post_ids: [p2.id]
end end
it "returns success" do it "returns success" do
response.should be_success response.should be_success
end result = ::JSON.parse(response.body)
result['success'].should be_true
it "has a JSON response" do result['url'].should be_present
::JSON.parse(response.body)['success'].should be_true
end
it "has a url" do
::JSON.parse(response.body)['url'].should be_present
end end
end end
@ -51,24 +42,56 @@ describe TopicsController do
let(:p2) { Fabricate(:post, user: user) } let(:p2) { Fabricate(:post, user: user) }
before do 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] xhr :post, :move_posts, topic_id: topic.id, title: 'blah', post_ids: [p2.id]
end 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 it "returns success" do
response.should be_success response.should be_success
result = ::JSON.parse(response.body)
result['success'].should be_true
result['url'].should be_present
end 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 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
end end

View File

@ -222,12 +222,12 @@ describe Topic do
it "enqueues a job to notify users" do it "enqueues a job to notify users" do
topic.stubs(:add_moderator_post) topic.stubs(:add_moderator_post)
Jobs.expects(:enqueue).with(:notify_moved_posts, post_ids: [p1.id, p4.id], moved_by_id: user.id) 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 end
it "adds a moderator post at the location of the first moved post" do 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.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
end end
@ -235,52 +235,97 @@ describe Topic do
context "errors" do context "errors" do
it "raises an error when one of the posts doesn't exist" 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 end
it "raises an error if no posts were moved" do 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
end end
context "afterwards" do context "successfully moved" do
before do before do
topic.expects(:add_moderator_post) topic.expects(:add_moderator_post)
TopicUser.update_last_read(user, topic.id, p4.post_number, 0) TopicUser.update_last_read(user, topic.id, p4.post_number, 0)
end 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 it "moved correctly" do
TopicUser.where(user_id: user.id, topic_id: topic.id).first.last_read_post_number.should == p3.post_number 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.should be_present
new_topic.featured_user1_id.should == another_user.id new_topic.featured_user1_id.should == another_user.id
new_topic.like_count.should == 1 new_topic.like_count.should == 1
new_topic.category.should == category new_topic.category.should == category
topic.featured_user1_id.should be_blank topic.featured_user1_id.should be_blank
new_topic.posts.should =~ [p2, p4] new_topic.posts.should =~ [p2, p4]
new_topic.reload new_topic.reload
new_topic.posts_count.should == 2 new_topic.posts_count.should == 2
new_topic.highest_post_number.should == 2 new_topic.highest_post_number.should == 2
p2.reload p2.reload
p2.sort_order.should == 1 p2.sort_order.should == 1
p2.post_number.should == 1 p2.post_number.should == 1
p4.reload p4.reload
p4.post_number.should == 2 p4.post_number.should == 2
p4.sort_order.should == 2 p4.sort_order.should == 2
topic.reload topic.reload
topic.featured_user1_id.should be_blank topic.featured_user1_id.should be_blank
topic.like_count.should == 0 topic.like_count.should == 0
topic.posts_count.should == 2 topic.posts_count.should == 2
topic.posts.should =~ [p1, p3] topic.posts.should =~ [p1, p3]
topic.highest_post_number.should == p3.post_number topic.highest_post_number.should == p3.post_number
end
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
end end