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

View File

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

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, 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();

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">
{{#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>

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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