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