diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index 71be8528f..1071f6d7f 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -545,6 +545,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
changePostOwner(post) {
this.get('selectedPosts').addObject(post);
this.send('changeOwner');
+ },
+
+ convertToPublicTopic() {
+ this.get('content').convertTopic("public");
+ },
+
+ convertToPrivateMessage() {
+ this.get('content').convertTopic("private");
}
},
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index 949386c9f..700e07aeb 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -4,6 +4,7 @@ import { propertyEqual } from 'discourse/lib/computed';
import { longDate } from 'discourse/lib/formatter';
import computed from 'ember-addons/ember-computed-decorators';
import ActionSummary from 'discourse/models/action-summary';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
export function loadTopicView(topic, args) {
const topicId = topic.get('id');
@@ -446,8 +447,13 @@ const Topic = RestModel.extend({
}).finally(()=>this.set('archiving', false));
return promise;
- }
+ },
+ convertTopic(type) {
+ return Discourse.ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => {
+ window.location.reload();
+ }).catch(popupAjaxError);
+ }
});
Topic.reopenClass({
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index 6772028a3..ace64e6e7 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -256,6 +256,16 @@
{{/if}}
+ {{#if currentUser.admin}}
+
+ {{#if model.isPrivateMessage}}
+ {{d-button action="convertToPublicTopic" icon="comment" label="topic.actions.make_public"}}
+ {{else}}
+ {{d-button action="convertToPrivateMessage" icon="envelope" label="topic.actions.make_private"}}
+ {{/if}}
+
+ {{/if}}
+
{{plugin-outlet "topic-admin-menu-buttons"}}
{{/popup-menu}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/widgets/post-small-action.js.es6 b/app/assets/javascripts/discourse/widgets/post-small-action.js.es6
index 4940b6446..4f56c01dd 100644
--- a/app/assets/javascripts/discourse/widgets/post-small-action.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-small-action.js.es6
@@ -20,7 +20,9 @@ const icons = {
'visible.disabled': 'eye-slash',
'split_topic': 'sign-out',
'invited_user': 'plus-circle',
- 'removed_user': 'minus-circle'
+ 'removed_user': 'minus-circle',
+ 'public_topic': 'comment',
+ 'private_topic': 'envelope'
};
export default createWidget('post-small-action', {
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 8e02fad1e..9a97662a6 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -27,6 +27,7 @@ class TopicsController < ApplicationController
:change_timestamps,
:archive_message,
:move_to_inbox,
+ :convert_topic,
:bookmark]
before_filter :consider_user_for_promotion, only: :show
@@ -510,6 +511,22 @@ class TopicsController < ApplicationController
render nothing: true
end
+ def convert_topic
+ params.require(:id)
+ params.require(:type)
+ topic = Topic.find_by(id: params[:id])
+ guardian.ensure_can_convert_topic!(topic)
+
+ if params[:type] == "public"
+ converted_topic = topic.convert_to_public_topic(current_user)
+ else
+ converted_topic = topic.convert_to_private_message(current_user)
+ end
+ render_topic_changes(converted_topic)
+ rescue ActiveRecord::RecordInvalid => ex
+ render_json_error(ex)
+ end
+
private
def toggle_mute
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 6d516e360..2fc448405 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -1047,6 +1047,18 @@ SQL
[result].flatten unless result.blank?
end
+ def convert_to_public_topic(user)
+ public_topic = TopicConverter.new(self, user).convert_to_public_topic
+ add_small_action(user, "public_topic") if public_topic
+ public_topic
+ end
+
+ def convert_to_private_message(user)
+ private_topic = TopicConverter.new(self, user).convert_to_private_message
+ add_small_action(user, "private_topic") if private_topic
+ private_topic
+ end
+
private
def update_category_topic_count_by(num)
diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb
new file mode 100644
index 000000000..4e40c2834
--- /dev/null
+++ b/app/models/topic_converter.rb
@@ -0,0 +1,69 @@
+class TopicConverter
+
+ attr_reader :topic
+
+ def initialize(topic, user)
+ @topic = topic
+ @user = user
+ end
+
+ def convert_to_public_topic
+ Topic.transaction do
+ @topic.category_id = SiteSetting.allow_uncategorized_topics ? SiteSetting.uncategorized_category_id : Category.where(read_restricted: false).first.id
+ @topic.archetype = Archetype.default
+ @topic.save
+ update_user_stats
+ watch_topic(topic)
+ end
+ @topic
+ end
+
+ def convert_to_private_message
+ Topic.transaction do
+ @topic.category_id = nil
+ @topic.archetype = Archetype.private_message
+ add_allowed_users
+ @topic.save
+ watch_topic(topic)
+ end
+ @topic
+ end
+
+ private
+
+ def update_user_stats
+ @topic.posts.where(deleted_at: nil).each do |p|
+ user = User.find(p.user_id)
+ # update posts count
+ user.user_stat.post_count += 1
+ user.user_stat.save!
+ end
+ # update topics count
+ @topic.user.user_stat.topic_count += 1
+ @topic.user.user_stat.save!
+ end
+
+ def add_allowed_users
+ @topic.posts.where(deleted_at: nil).each do |p|
+ user = User.find(p.user_id)
+ @topic.topic_allowed_users.build(user_id: user.id) unless @topic.topic_allowed_users.where(user_id: user.id).exists?
+ # update posts count
+ user.user_stat.post_count -= 1
+ user.user_stat.save!
+ end
+ @topic.topic_allowed_users.build(user_id: @user.id)
+ # update topics count
+ @topic.user.user_stat.topic_count -= 1
+ @topic.user.user_stat.save!
+ end
+
+ def watch_topic(topic)
+ @topic.notifier.watch_topic!(topic.user_id)
+
+ @topic.topic_allowed_users(true).each do |tau|
+ next if tau.user_id == -1 || tau.user_id == topic.user_id
+ topic.notifier.watch!(tau.user_id)
+ end
+ end
+
+end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index f5ce598f9..c9d7675d9 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -121,6 +121,8 @@ en:
email: 'send this link in an email'
action_codes:
+ public_topic: "made this topic public %{when}"
+ private_topic: "made this topic private %{when}"
split_topic: "split this topic %{when}"
invited_user: "invited %{who} %{when}"
removed_user: "removed %{who} %{when}"
@@ -1344,6 +1346,8 @@ en:
invisible: "Make Unlisted"
visible: "Make Listed"
reset_read: "Reset Read Data"
+ make_public: "Make Public Topic"
+ make_private: "Make Private Message"
feature:
pin: "Pin Topic"
@@ -2983,4 +2987,3 @@ en:
top: "There are no more top topics."
bookmarks: "There are no more bookmarked topics."
search: "There are no more search results."
-
diff --git a/config/routes.rb b/config/routes.rb
index a0c233965..31682f385 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -486,6 +486,7 @@ Discourse::Application.routes.draw do
delete "t/:id" => "topics#destroy"
put "t/:id/archive-message" => "topics#archive_message"
put "t/:id/move-to-inbox" => "topics#move_to_inbox"
+ put "t/:id/convert-topic/:type" => "topics#convert_topic"
put "topics/bulk"
put "topics/reset-new" => 'topics#reset_new'
post "topics/timings"
diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb
index c6963214d..04c81a894 100644
--- a/lib/guardian/topic_guardian.rb
+++ b/lib/guardian/topic_guardian.rb
@@ -58,6 +58,10 @@ module TopicGuardian
!Discourse.static_doc_topic_ids.include?(topic.id)
end
+ def can_convert_topic?(topic)
+ topic && !topic.trashed? && is_admin?
+ end
+
def can_reply_as_new_topic?(topic)
authenticated? && topic && not(topic.private_message?) && @user.has_trust_level?(TrustLevel[1])
end
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 5a6d9776f..969067404 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -872,6 +872,24 @@ describe Guardian do
end
+ context 'can_convert_topic?' do
+ it 'returns false with a nil object' do
+ expect(Guardian.new(user).can_convert_topic?(nil)).to be_falsey
+ end
+
+ it 'returns false when not logged in' do
+ expect(Guardian.new.can_convert_topic?(topic)).to be_falsey
+ end
+
+ it 'returns false when not admin' do
+ expect(Guardian.new(moderator).can_convert_topic?(topic)).to be_falsey
+ end
+
+ it 'returns true when an admin' do
+ expect(Guardian.new(admin).can_convert_topic?(topic)).to be_truthy
+ end
+ end
+
describe 'can_edit?' do
it 'returns false with a nil object' do
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 4a82c8074..780e6f4a4 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -1235,4 +1235,63 @@ describe TopicsController do
expect(response.headers['X-Robots-Tag']).to eq(nil)
end
end
+
+ context "convert_topic" do
+ it 'needs you to be logged in' do
+ expect { xhr :put, :convert_topic, id: 111, type: "private" }.to raise_error(Discourse::NotLoggedIn)
+ end
+
+ describe 'converting public topic to private message' do
+ let(:user) { Fabricate(:user) }
+ let(:topic) { Fabricate(:topic, user: user) }
+
+ it "raises an error when the user doesn't have permission to convert topic" do
+ log_in
+ xhr :put, :convert_topic, id: topic.id, type: "private"
+ expect(response).to be_forbidden
+ end
+
+ context "success" do
+ before do
+ admin = log_in(:admin)
+ Topic.any_instance.expects(:convert_to_private_message).with(admin).returns(topic)
+ xhr :put, :convert_topic, id: topic.id, type: "private"
+ end
+
+ it "returns success" do
+ expect(response).to be_success
+ result = ::JSON.parse(response.body)
+ expect(result['success']).to eq(true)
+ expect(result['url']).to be_present
+ end
+ end
+ end
+
+ describe 'converting private message to public topic' do
+ let(:user) { Fabricate(:user) }
+ let(:topic) { Fabricate(:topic, user: user) }
+
+ it "raises an error when the user doesn't have permission to convert topic" do
+ log_in
+ xhr :put, :convert_topic, id: topic.id, type: "public"
+ expect(response).to be_forbidden
+ end
+
+ context "success" do
+ before do
+ admin = log_in(:admin)
+ Topic.any_instance.expects(:convert_to_public_topic).with(admin).returns(topic)
+ xhr :put, :convert_topic, id: topic.id, type: "public"
+ end
+
+ it "returns success" do
+ expect(response).to be_success
+ result = ::JSON.parse(response.body)
+ expect(result['success']).to eq(true)
+ expect(result['url']).to be_present
+ end
+ end
+ end
+ end
+
end
diff --git a/spec/models/topic_converter_spec.rb b/spec/models/topic_converter_spec.rb
new file mode 100644
index 000000000..96bd61e91
--- /dev/null
+++ b/spec/models/topic_converter_spec.rb
@@ -0,0 +1,68 @@
+require 'rails_helper'
+
+describe TopicConverter do
+
+ context 'convert_to_public_topic' do
+ let(:admin) { Fabricate(:admin) }
+ let(:author) { Fabricate(:user) }
+ let(:private_message) { Fabricate(:private_message_topic, user: author) }
+
+ context 'success' do
+ it "converts private message to regular topic" do
+ topic = private_message.convert_to_public_topic(admin)
+ expect(topic).to be_valid
+ expect(topic.archetype).to eq("regular")
+ end
+
+ it "updates user stats" do
+ topic_user = TopicUser.create!(user_id: author.id, topic_id: private_message.id, posted: true)
+ expect(private_message.user.user_stat.topic_count).to eq(0)
+ private_message.convert_to_public_topic(admin)
+ expect(private_message.reload.user.user_stat.topic_count).to eq(1)
+ expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching])
+ end
+ end
+ end
+
+ context 'convert_to_private_message' do
+ let(:admin) { Fabricate(:admin) }
+ let(:author) { Fabricate(:user) }
+ let(:topic) { Fabricate(:topic, user: author) }
+
+ context 'success' do
+ it "converts regular topic to private message" do
+ private_message = topic.convert_to_private_message(admin)
+ expect(private_message).to be_valid
+ expect(topic.archetype).to eq("private_message")
+ end
+
+ it "updates user stats" do
+ Fabricate(:post, topic: topic, user: author)
+ topic_user = TopicUser.create!(user_id: author.id, topic_id: topic.id, posted: true)
+ author.user_stat.topic_count = 1
+ author.user_stat.save
+ expect(topic.user.user_stat.topic_count).to eq(1)
+ topic.convert_to_private_message(admin)
+
+ expect(topic.reload.topic_allowed_users.where(user_id: author.id).count).to eq(1)
+ expect(topic.reload.user.user_stat.topic_count).to eq(0)
+ expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching])
+ end
+ end
+
+ context 'topic has replies' do
+ before do
+ @replied_user = Fabricate(:coding_horror)
+ create_post(topic: topic, user: @replied_user)
+ topic.reload
+ end
+
+ it 'adds users who replied to topic in Private Message' do
+ topic.convert_to_private_message(admin)
+
+ expect(topic.reload.topic_allowed_users.where(user_id: @replied_user.id).count).to eq(1)
+ expect(topic.reload.user.user_stat.post_count).to eq(0)
+ end
+ end
+ end
+end