"
@@ -1610,6 +1614,26 @@ en:
autoclosed_enabled_lastpost_minutes:
one: "This topic was automatically closed 1 minute after the last reply. New replies are no longer allowed."
other: "This topic was automatically closed %{count} minutes after the last reply. New replies are no longer allowed."
+
+ autoclosed_disabled_days:
+ one: "This topic was automatically opened after 1 day."
+ other: "This topic was automatically opened after %{count} days."
+ autoclosed_disabled_hours:
+ one: "This topic was automatically opened after 1 hour."
+ other: "This topic was automatically opened after %{count} hours."
+ autoclosed_disabled_minutes:
+ one: "This topic was automatically opened after 1 minute."
+ other: "This topic was automatically opened after %{count} minutes."
+ autoclosed_disabled_lastpost_days:
+ one: "This topic was automatically opened 1 day after the last reply."
+ other: "This topic was automatically opened %{count} days after the last reply."
+ autoclosed_disabled_lastpost_hours:
+ one: "This topic was automatically opened 1 hour after the last reply."
+ other: "This topic was automatically opened %{count} hours after the last reply."
+ autoclosed_disabled_lastpost_minutes:
+ one: "This topic was automatically opened 1 minute after the last reply."
+ other: "This topic was automatically opened %{count} minutes after the last reply."
+
autoclosed_disabled: "This topic is now opened. New replies are allowed."
autoclosed_disabled_lastpost: "This topic is now opened. New replies are allowed."
pinned_enabled: "This topic is now pinned. It will appear at the top of its category until it is unpinned by staff for everyone, or by individual users for themselves."
diff --git a/config/routes.rb b/config/routes.rb
index ed26fd02dc6..b33b21830cb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -578,7 +578,7 @@ Discourse::Application.routes.draw do
put "t/:topic_id/re-pin" => "topics#re_pin", constraints: {topic_id: /\d+/}
put "t/:topic_id/mute" => "topics#mute", constraints: {topic_id: /\d+/}
put "t/:topic_id/unmute" => "topics#unmute", constraints: {topic_id: /\d+/}
- put "t/:topic_id/autoclose" => "topics#autoclose", constraints: {topic_id: /\d+/}
+ post "t/:topic_id/status_update" => "topics#status_update", constraints: {topic_id: /\d+/}
put "t/:topic_id/make-banner" => "topics#make_banner", constraints: {topic_id: /\d+/}
put "t/:topic_id/remove-banner" => "topics#remove_banner", constraints: {topic_id: /\d+/}
put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: {topic_id: /\d+/}
diff --git a/db/migrate/20170322065911_create_topic_status_updates.rb b/db/migrate/20170322065911_create_topic_status_updates.rb
new file mode 100644
index 00000000000..36f81a6fa8e
--- /dev/null
+++ b/db/migrate/20170322065911_create_topic_status_updates.rb
@@ -0,0 +1,16 @@
+class CreateTopicStatusUpdates < ActiveRecord::Migration
+ def change
+ create_table :topic_status_updates do |t|
+ t.datetime :execute_at, null: false
+ t.integer :status_type, null: false
+ t.integer :user_id, null: false
+ t.integer :topic_id, null: false
+ t.boolean :based_on_last_post, null: false, default: false
+ t.datetime :deleted_at
+ t.integer :deleted_by_id
+ t.timestamps
+ end
+
+ add_index :topic_status_updates, :user_id
+ end
+end
diff --git a/db/migrate/20170324032913_move_auto_close_columns_to_topic_status_update.rb b/db/migrate/20170324032913_move_auto_close_columns_to_topic_status_update.rb
new file mode 100644
index 00000000000..d476468be77
--- /dev/null
+++ b/db/migrate/20170324032913_move_auto_close_columns_to_topic_status_update.rb
@@ -0,0 +1,39 @@
+class MoveAutoCloseColumnsToTopicStatusUpdate < ActiveRecord::Migration
+ def up
+ execute <<~SQL
+ INSERT INTO topic_status_updates(topic_id, user_id, execute_at, status_type, based_on_last_post, created_at, updated_at)
+ SELECT
+ t.id,
+ t.auto_close_user_id,
+ t.auto_close_at,
+ #{TopicStatusUpdate.types[:close]},
+ t.auto_close_based_on_last_post,
+ t.auto_close_started_at,
+ t.auto_close_started_at
+ FROM topics t
+ WHERE t.auto_close_at IS NOT NULL
+ AND t.auto_close_user_id IS NOT NULL
+ AND t.auto_close_started_at IS NOT NULL
+ AND t.deleted_at IS NULL
+ SQL
+
+ execute <<~SQL
+ WITH selected AS (
+ SELECT tsp.id
+ FROM topic_status_updates tsp
+ JOIN topics t
+ ON t.id = tsp.topic_id
+ WHERE tsp.execute_at < now()
+ OR (t.closed AND tsp.execute_at >= now())
+ )
+
+ UPDATE topic_status_updates
+ SET deleted_at = now(), deleted_by_id = #{Discourse::SYSTEM_USER_ID}
+ WHERE id in (SELECT * FROM selected)
+ SQL
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/migrate/20170330041605_add_index_to_topic_status_updates.rb b/db/migrate/20170330041605_add_index_to_topic_status_updates.rb
new file mode 100644
index 00000000000..12a981b5fe3
--- /dev/null
+++ b/db/migrate/20170330041605_add_index_to_topic_status_updates.rb
@@ -0,0 +1,13 @@
+class AddIndexToTopicStatusUpdates < ActiveRecord::Migration
+ def up
+ execute <<~SQL
+ CREATE UNIQUE INDEX idx_topic_id_status_type_deleted_at
+ ON topic_status_updates(topic_id, status_type)
+ WHERE deleted_at IS NULL
+ SQL
+ end
+
+ def down
+ execute "DROP INDEX idx_topic_id_status_type_deleted_at"
+ end
+end
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index 928c8473623..06a0c651b55 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -390,8 +390,16 @@ class PostCreator
end
def update_topic_auto_close
- if @topic.auto_close_based_on_last_post && @topic.auto_close_hours
- @topic.set_auto_close(@topic.auto_close_hours).save
+ topic_status_update = @topic.topic_status_update
+
+ if topic_status_update &&
+ topic_status_update.based_on_last_post &&
+ topic_status_update.duration > 0
+
+ @topic.set_or_create_status_update(TopicStatusUpdate.types[:close],
+ topic_status_update.duration,
+ based_on_last_post: topic_status_update.based_on_last_post
+ )
end
end
diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb
index 87569632382..65085906b61 100644
--- a/spec/components/post_creator_spec.rb
+++ b/spec/components/post_creator_spec.rb
@@ -262,23 +262,38 @@ describe PostCreator do
describe "topic's auto close" do
it "doesn't update topic's auto close when it's not based on last post" do
- auto_close_time = 1.day.from_now
- topic = Fabricate(:topic, auto_close_at: auto_close_time, auto_close_hours: 12)
+ Timecop.freeze do
+ topic = Fabricate(:topic).set_or_create_status_update(TopicStatusUpdate.types[:close], 12)
- PostCreator.new(topic.user, topic_id: topic.id, raw: "this is a second post").create
- topic.reload
+ PostCreator.new(topic.user, topic_id: topic.id, raw: "this is a second post").create
+ topic.reload
- expect(topic.auto_close_at).to be_within(1.second).of(auto_close_time)
+ topic_status_update = TopicStatusUpdate.last
+ expect(topic_status_update.execute_at).to be_within(1.second).of(Time.zone.now + 12.hours)
+ expect(topic_status_update.created_at).to be_within(1.second).of(Time.zone.now)
+ end
end
it "updates topic's auto close date when it's based on last post" do
- auto_close_time = 1.day.from_now
- topic = Fabricate(:topic, auto_close_at: auto_close_time, auto_close_hours: 12, auto_close_based_on_last_post: true)
+ SiteSetting.queue_jobs = true
- PostCreator.new(topic.user, topic_id: topic.id, raw: "this is a second post").create
- topic.reload
+ Timecop.freeze do
+ topic = Fabricate(:topic,
+ topic_status_updates: [Fabricate(:topic_status_update,
+ based_on_last_post: true,
+ execute_at: Time.zone.now - 12.hours,
+ created_at: Time.zone.now - 24.hours
+ )]
+ )
- expect(topic.auto_close_at).not_to be_within(1.second).of(auto_close_time)
+ Fabricate(:post, topic: topic)
+
+ PostCreator.new(topic.user, topic_id: topic.id, raw: "this is a second post").create
+
+ topic_status_update = TopicStatusUpdate.last
+ expect(topic_status_update.execute_at).to be_within(1.second).of(Time.zone.now + 12.hours)
+ expect(topic_status_update.created_at).to be_within(1.second).of(Time.zone.now)
+ end
end
end
@@ -341,8 +356,9 @@ describe PostCreator do
context 'when auto-close param is given' do
it 'ensures the user can auto-close the topic, but ignores auto-close param silently' do
Guardian.any_instance.stubs(:can_moderate?).returns(false)
- post = PostCreator.new(user, basic_topic_params.merge(auto_close_time: 2)).create
- expect(post.topic.auto_close_at).to eq(nil)
+ expect {
+ PostCreator.new(user, basic_topic_params.merge(auto_close_time: 2)).create!
+ }.to_not change { TopicStatusUpdate.count }
end
end
end
diff --git a/spec/components/topic_creator_spec.rb b/spec/components/topic_creator_spec.rb
index dcca1dd101a..d47cc8dc112 100644
--- a/spec/components/topic_creator_spec.rb
+++ b/spec/components/topic_creator_spec.rb
@@ -39,7 +39,7 @@ describe TopicCreator do
it "ignores auto_close_time without raising an error" do
topic = TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(auto_close_time: '24'))
expect(topic).to be_valid
- expect(topic.auto_close_at).to eq(nil)
+ expect(topic.topic_status_update).to eq(nil)
end
it "category name is case insensitive" do
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 4a5885ff60d..6377403e32b 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -1112,82 +1112,6 @@ describe TopicsController do
end
- describe 'autoclose' do
-
- it 'needs you to be logged in' do
- expect {
- xhr :put, :autoclose, topic_id: 99, auto_close_time: '24', auto_close_based_on_last_post: false
- }.to raise_error(Discourse::NotLoggedIn)
- end
-
- it 'needs you to be an admin or mod' do
- log_in
- xhr :put, :autoclose, topic_id: 99, auto_close_time: '24', auto_close_based_on_last_post: false
- expect(response).to be_forbidden
- end
-
- describe 'when logged in' do
- before do
- @admin = log_in(:admin)
- @topic = Fabricate(:topic, user: @admin)
- end
-
- it "can set a topic's auto close time and 'based on last post' property" do
- Topic.any_instance.expects(:set_auto_close).with("24", {by_user: @admin, timezone_offset: -240})
- xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: '24', auto_close_based_on_last_post: true, timezone_offset: -240
- json = ::JSON.parse(response.body)
- expect(json).to have_key('auto_close_at')
- expect(json).to have_key('auto_close_hours')
- end
-
- it "can remove a topic's auto close time" do
- Topic.any_instance.expects(:set_auto_close).with(nil, anything)
- xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: nil, auto_close_based_on_last_post: false, timezone_offset: -240
- end
-
- it "will close a topic when the time expires" do
- topic = Fabricate(:topic)
- Timecop.freeze(20.hours.ago) do
- create_post(topic: topic, raw: "This is the body of my cool post in the topic, but it's a bit old now")
- end
- topic.save
-
- Jobs.expects(:enqueue_at).at_least_once
- xhr :put, :autoclose, topic_id: topic.id, auto_close_time: 24, auto_close_based_on_last_post: true
-
- topic.reload
- expect(topic.closed).to eq(false)
- expect(topic.posts.last.raw).to match(/cool post/)
-
- Timecop.freeze(5.hours.from_now) do
- Jobs::CloseTopic.new.execute({topic_id: topic.id, user_id: @admin.id})
- end
-
- topic.reload
- expect(topic.closed).to eq(true)
- expect(topic.posts.last.raw).to match(/automatically closed/)
- end
-
- it "will immediately close if the last post is old enough" do
- topic = Fabricate(:topic)
- Timecop.freeze(20.hours.ago) do
- create_post(topic: topic)
- end
- topic.save
- Topic.reset_highest(topic.id)
- topic.reload
-
- xhr :put, :autoclose, topic_id: topic.id, auto_close_time: 10, auto_close_based_on_last_post: true
-
- topic.reload
- expect(topic.closed).to eq(true)
- expect(topic.posts.last.raw).to match(/after the last reply/)
- expect(topic.posts.last.raw).to match(/10 hours/)
- end
- end
-
- end
-
describe 'make_banner' do
it 'needs you to be a staff member' do
diff --git a/spec/fabricators/topic_status_update_fabricator.rb b/spec/fabricators/topic_status_update_fabricator.rb
new file mode 100644
index 00000000000..c07a92d7d0b
--- /dev/null
+++ b/spec/fabricators/topic_status_update_fabricator.rb
@@ -0,0 +1,6 @@
+Fabricator(:topic_status_update) do
+ user
+ topic
+ execute_at Time.zone.now + 1.hour
+ status_type TopicStatusUpdate.types[:close]
+end
diff --git a/spec/integration/managing_topic_status_spec.rb b/spec/integration/managing_topic_status_spec.rb
new file mode 100644
index 00000000000..4e59f9c3df7
--- /dev/null
+++ b/spec/integration/managing_topic_status_spec.rb
@@ -0,0 +1,93 @@
+require 'rails_helper'
+
+RSpec.describe "Managing a topic's status update", type: :request do
+ let(:topic) { Fabricate(:topic) }
+ let(:user) { Fabricate(:user) }
+
+ context 'when a user is not logged in' do
+ it 'should return the right response' do
+ expect do
+ post "/t/#{topic.id}/status_update.json",
+ time: '24',
+ status_type: TopicStatusUpdate.types[1]
+ end.to raise_error(Discourse::NotLoggedIn)
+ end
+ end
+
+ context 'when does not have permission' do
+ it 'should return the right response' do
+ sign_in(user)
+
+ post "/t/#{topic.id}/status_update.json",
+ time: '24',
+ status_type: TopicStatusUpdate.types[1]
+
+ expect(response.status).to eq(403)
+ expect(JSON.parse(response.body)["error_type"]).to eq('invalid_access')
+ end
+ end
+
+ context 'when logged in as an admin' do
+ let(:admin) { Fabricate(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'should be able to create a topic status update' do
+ time = 24
+
+ post "/t/#{topic.id}/status_update.json",
+ time: 24,
+ status_type: TopicStatusUpdate.types[1]
+
+ expect(response).to be_success
+
+ topic_status_update = TopicStatusUpdate.last
+
+ expect(topic_status_update.topic).to eq(topic)
+
+ expect(topic_status_update.execute_at)
+ .to be_within(1.second).of(24.hours.from_now)
+
+ json = JSON.parse(response.body)
+
+ expect(DateTime.parse(json['execute_at']))
+ .to be_within(1.seconds).of(DateTime.parse(topic_status_update.execute_at.to_s))
+
+ expect(json['duration']).to eq(topic_status_update.duration)
+ end
+
+ it 'should be able to delete a topic status update' do
+ topic.update!(topic_status_updates: [Fabricate(:topic_status_update)])
+
+ post "/t/#{topic.id}/status_update.json",
+ time: nil,
+ status_type: TopicStatusUpdate.types[1]
+
+ expect(response).to be_success
+ expect(topic.reload.topic_status_update).to eq(nil)
+
+ json = JSON.parse(response.body)
+
+ expect(json['execute_at']).to eq(nil)
+ expect(json['duration']).to eq(nil)
+ end
+
+ describe 'invalid status type' do
+ it 'should raise the right error' do
+ expect do
+ post "/t/#{topic.id}/status_update.json",
+ time: 10,
+ status_type: 'something'
+ end.to raise_error(Discourse::InvalidParameters)
+ end
+ end
+
+ describe 'when the last post is old enough' do
+ it 'should close the topic immediately' do
+
+ end
+ end
+ end
+end
diff --git a/spec/integration/topic_auto_close_spec.rb b/spec/integration/topic_auto_close_spec.rb
index c57c505c658..3f302c4eeb9 100644
--- a/spec/integration/topic_auto_close_spec.rb
+++ b/spec/integration/topic_auto_close_spec.rb
@@ -3,29 +3,10 @@
require 'rails_helper'
describe Topic do
-
- def scheduled_jobs_for(job_name, params={})
- "Jobs::#{job_name.to_s.camelcase}".constantize.jobs.select do |job|
- job_args = job['args'][0]
- matched = true
- params.each do |key, value|
- unless job_args[key.to_s] == value
- matched = false
- break
- end
- end
- matched
- end
- end
+ let(:job_klass) { Jobs::ToggleTopicClosed }
before do
- @original_value = SiteSetting.queue_jobs
- SiteSetting.queue_jobs = true
- Jobs::CloseTopic.jobs.clear
- end
-
- after do
- SiteSetting.queue_jobs = @original_value
+ job_klass.jobs.clear
end
context 'creating a topic without auto-close' do
@@ -35,8 +16,8 @@ describe Topic do
let(:category) { nil }
it 'should not schedule the topic to auto-close' do
- expect(topic.auto_close_at).to eq(nil)
- expect(scheduled_jobs_for(:close_topic)).to be_empty
+ expect(topic.topic_status_update).to eq(nil)
+ expect(job_klass.jobs).to eq([])
end
end
@@ -44,29 +25,36 @@ describe Topic do
let(:category) { Fabricate(:category, auto_close_hours: nil) }
it 'should not schedule the topic to auto-close' do
- expect(topic.auto_close_at).to eq(nil)
- expect(scheduled_jobs_for(:close_topic)).to be_empty
+ expect(topic.topic_status_update).to eq(nil)
+ expect(job_klass.jobs).to eq([])
end
end
context 'jobs may be queued' do
before do
+ SiteSetting.queue_jobs = true
Timecop.freeze(Time.zone.now)
end
after do
Timecop.return
- Sidekiq::Extensions::DelayedClass.jobs.clear
end
context 'category has a default auto-close' do
let(:category) { Fabricate(:category, auto_close_hours: 2.0) }
it 'should schedule the topic to auto-close' do
- expect(topic.auto_close_at).to be_within_one_second_of(2.hours.from_now)
- expect(topic.auto_close_started_at).to eq(Time.zone.now)
- expect(scheduled_jobs_for(:close_topic, {topic_id: topic.id}).size).to eq(1)
- expect(scheduled_jobs_for(:close_topic, {topic_id: category.topic.id})).to be_empty
+ topic
+
+ topic_status_update = TopicStatusUpdate.last
+
+ expect(topic_status_update.topic).to eq(topic)
+ expect(topic.topic_status_update.execute_at).to be_within_one_second_of(2.hours.from_now)
+
+ args = job_klass.jobs.last['args'].first
+
+ expect(args["topic_status_update_id"]).to eq(topic.topic_status_update.id)
+ expect(args["state"]).to eq(true)
end
context 'topic was created by staff user' do
@@ -74,14 +62,28 @@ describe Topic do
let(:staff_topic) { Fabricate(:topic, user: admin, category: category) }
it 'should schedule the topic to auto-close' do
- expect(scheduled_jobs_for(:close_topic, {topic_id: staff_topic.id, user_id: admin.id}).size).to eq(1)
+ staff_topic
+
+ topic_status_update = TopicStatusUpdate.last
+
+ expect(topic_status_update.topic).to eq(staff_topic)
+ expect(topic_status_update.execute_at).to be_within_one_second_of(2.hours.from_now)
+ expect(topic_status_update.user).to eq(admin)
+
+ args = job_klass.jobs.last['args'].first
+
+ expect(args["topic_status_update_id"]).to eq(topic_status_update.id)
+ expect(args["state"]).to eq(true)
end
context 'topic is closed manually' do
it 'should remove the schedule to auto-close the topic' do
- staff_topic.update_status('closed', true, admin)
- expect(staff_topic.reload.auto_close_at).to eq(nil)
- expect(staff_topic.auto_close_started_at).to eq(nil)
+ Timecop.freeze do
+ staff_topic.update_status('closed', true, admin)
+
+ expect(staff_topic.topic_status_update.reload.deleted_at)
+ .to be_within(1.second).of(Time.zone.now)
+ end
end
end
end
@@ -91,38 +93,20 @@ describe Topic do
let(:regular_user_topic) { Fabricate(:topic, user: regular_user, category: category) }
it 'should schedule the topic to auto-close' do
- expect(scheduled_jobs_for(:close_topic, {topic_id: regular_user_topic.id, user_id: Discourse.system_user.id}).size).to eq(1)
+ regular_user_topic
+
+ topic_status_update = TopicStatusUpdate.last
+
+ expect(topic_status_update.topic).to eq(regular_user_topic)
+ expect(topic_status_update.execute_at).to be_within_one_second_of(2.hours.from_now)
+ expect(topic_status_update.user).to eq(Discourse.system_user)
+
+ args = job_klass.jobs.last['args'].first
+
+ expect(args["topic_status_update_id"]).to eq(topic_status_update.id)
+ expect(args["state"]).to eq(true)
end
end
-
- context 'auto_close_hours of topic was set to 0' do
- let(:dont_close_topic) { Fabricate(:topic, auto_close_hours: 0, category: category) }
-
- it 'should not schedule the topic to auto-close' do
- expect(scheduled_jobs_for(:close_topic)).to be_empty
- end
- end
-
- context 'two topics in the category' do
- let!(:other_topic) { Fabricate(:topic, category: category) }
-
- it 'should schedule the topic to auto-close' do
- topic
-
- expect(scheduled_jobs_for(:close_topic).size).to eq(2)
- end
- end
- end
-
- context 'a topic that has been auto-closed' do
- let(:admin) { Fabricate(:admin) }
- let!(:auto_closed_topic) { Fabricate(:topic, user: admin, closed: true, auto_close_at: 1.day.ago, auto_close_user_id: admin.id, auto_close_started_at: 6.days.ago) }
-
- it 'should set the right attributes' do
- auto_closed_topic.update_status('closed', false, admin)
- expect(auto_closed_topic.reload.auto_close_at).to eq(nil)
- expect(auto_closed_topic.auto_close_started_at).to eq(nil)
- end
end
end
end
diff --git a/spec/jobs/close_topic_spec.rb b/spec/jobs/close_topic_spec.rb
deleted file mode 100644
index 9efb67ca48d..00000000000
--- a/spec/jobs/close_topic_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'rails_helper'
-require_dependency 'jobs/base'
-
-describe Jobs::CloseTopic do
-
- let(:admin) { Fabricate.build(:admin) }
-
- it 'closes a topic that is set to auto-close' do
- topic = Fabricate.build(:topic, auto_close_at: Time.zone.now, user: admin)
- topic.expects(:update_status).with('autoclosed', true, admin)
- Topic.stubs(:find_by).returns(topic)
- User.stubs(:find_by).returns(admin)
- Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
- end
-
- shared_examples_for "cases when CloseTopic does nothing" do
- it 'does nothing to the topic' do
- topic.expects(:update_status).never
- Topic.stubs(:find_by).returns(topic)
- User.stubs(:find_by).returns(admin)
- Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
- end
- end
-
- context 'when topic is not set to auto-close' do
- subject(:topic) { Fabricate.build(:topic, auto_close_at: nil, user: admin) }
- it_behaves_like 'cases when CloseTopic does nothing'
- end
-
- context 'when user is not authorized to close topics' do
- subject(:topic) { Fabricate.build(:topic, auto_close_at: 2.days.from_now, user: admin) }
- before { Guardian.any_instance.stubs(:can_moderate?).returns(false) }
- it_behaves_like 'cases when CloseTopic does nothing'
- end
-
- context 'the topic is already closed' do
- subject(:topic) { Fabricate.build(:topic, auto_close_at: 2.days.from_now, user: admin, closed: true) }
- it_behaves_like 'cases when CloseTopic does nothing'
- end
-
- context 'the topic has been deleted' do
- subject(:topic) { Fabricate.build(:deleted_topic, auto_close_at: 2.days.from_now, user: admin) }
- it_behaves_like 'cases when CloseTopic does nothing'
- end
-
-end
diff --git a/spec/jobs/toggle_topic_closed_spec.rb b/spec/jobs/toggle_topic_closed_spec.rb
new file mode 100644
index 00000000000..c98f70d562c
--- /dev/null
+++ b/spec/jobs/toggle_topic_closed_spec.rb
@@ -0,0 +1,79 @@
+require 'rails_helper'
+
+describe Jobs::ToggleTopicClosed do
+ let(:admin) { Fabricate(:admin) }
+
+ let(:topic) do
+ Fabricate(:topic,
+ topic_status_updates: [Fabricate(:topic_status_update, user: admin)]
+ )
+ end
+
+ before do
+ SiteSetting.queue_jobs = true
+ end
+
+ it 'should be able to close a topic' do
+ topic
+
+ Timecop.travel(1.hour.from_now) do
+ described_class.new.execute(
+ topic_status_update_id: topic.topic_status_update.id,
+ state: true
+ )
+
+ expect(topic.reload.closed).to eq(true)
+
+ expect(Post.last.raw).to eq(I18n.t(
+ 'topic_statuses.autoclosed_enabled_minutes', count: 60
+ ))
+ end
+ end
+
+ it 'should be able to open a topic' do
+ topic.update!(closed: true)
+
+ Timecop.travel(1.hour.from_now) do
+ described_class.new.execute(
+ topic_status_update_id: topic.topic_status_update.id,
+ state: false
+ )
+
+ expect(topic.reload.closed).to eq(false)
+
+ expect(Post.last.raw).to eq(I18n.t(
+ 'topic_statuses.autoclosed_disabled_minutes', count: 60
+ ))
+ end
+ end
+
+ describe 'when trying to close a topic that has been deleted' do
+ it 'should not do anything' do
+ topic.trash!
+
+ Topic.any_instance.expects(:update_status).never
+
+ described_class.new.execute(
+ topic_status_update_id: topic.topic_status_update.id,
+ state: true
+ )
+ end
+ end
+
+ describe 'when user is not authorized to close topics' do
+ let(:topic) do
+ Fabricate(:topic,
+ topic_status_updates: [Fabricate(:topic_status_update, execute_at: 2.hours.from_now)]
+ )
+ end
+
+ it 'should not do anything' do
+ described_class.new.execute(
+ topic_status_update_id: topic.topic_status_update.id,
+ state: false
+ )
+
+ expect(topic.reload.closed).to eq(false)
+ end
+ end
+end
diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb
index f8c547a2682..d18d30a63e1 100644
--- a/spec/models/category_spec.rb
+++ b/spec/models/category_spec.rb
@@ -343,7 +343,7 @@ describe Category do
it "should not set its description topic to auto-close" do
category = Fabricate(:category, name: 'Closing Topics', auto_close_hours: 1)
- expect(category.topic.auto_close_at).to be_nil
+ expect(category.topic.topic_status_update).to eq(nil)
end
describe "creating a new category with the same slug" do
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index 6a85a4c50ae..41916d78a83 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -742,6 +742,7 @@ describe Topic do
expect(@topic).to be_closed
expect(@topic.bumped_at.to_f).to eq(@original_bumped_at)
expect(@topic.moderator_posts_count).to eq(1)
+ expect(@topic.topic_status_updates.first).to eq(nil)
end
end
end
@@ -766,20 +767,19 @@ describe Topic do
context 'topic was set to close after it was created' do
it 'puts the autoclose duration in the moderator post' do
-
freeze_time(Time.new(2000,1,1))
@topic.created_at = 7.days.ago
freeze_time(2.days.ago)
- @topic.set_auto_close(48)
+ @topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 48)
+ @topic.save!
freeze_time(2.days.from_now)
@topic.update_status(status, true, @user)
expect(@topic.posts.last.raw).to include "closed after 2 days"
-
end
end
end
@@ -1096,297 +1096,173 @@ describe Topic do
end
end
- describe 'auto-close' do
- context 'a new topic' do
- context 'auto_close_at is set' do
- it 'queues a job to close the topic' do
- Timecop.freeze(now) do
- Jobs.expects(:enqueue_at).with(7.hours.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
- topic = Fabricate(:topic, user: Fabricate(:admin))
- topic.set_auto_close(7).save
- end
- end
+ describe '#set_or_create_status_update' do
+ let(:topic) { Fabricate.build(:topic) }
- it 'when auto_close_user_id is nil, it will use the topic creator as the topic closer' do
- topic_creator = Fabricate(:admin)
- Jobs.expects(:enqueue_at).with do |datetime, job_name, job_args|
- job_args[:user_id] == topic_creator.id
- end
- topic = Fabricate(:topic, user: topic_creator)
- topic.set_auto_close(7).save
- end
-
- it 'when auto_close_user_id is set, it will use it as the topic closer' do
- topic_creator = Fabricate(:admin)
- topic_closer = Fabricate(:user, admin: true)
- Jobs.expects(:enqueue_at).with do |datetime, job_name, job_args|
- job_args[:user_id] == topic_closer.id
- end
- topic = Fabricate(:topic, user: topic_creator)
- topic.set_auto_close(7, {by_user: topic_closer}).save
- end
-
- it "ignores the category's default auto-close" do
- Timecop.freeze(now) do
- Jobs.expects(:enqueue_at).with(7.hours.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
- topic = Fabricate(:topic, user: Fabricate(:admin), ignore_category_auto_close: true, category_id: Fabricate(:category, auto_close_hours: 2).id)
- topic.set_auto_close(7).save
- end
- end
-
- it 'sets the time when auto_close timer starts' do
- Timecop.freeze(now) do
- topic = Fabricate(:topic, user: Fabricate(:admin))
- topic.set_auto_close(7).save
- expect(topic.auto_close_started_at).to eq(now)
- end
- end
- end
+ let(:closing_topic) do
+ Fabricate(:topic,
+ topic_status_updates: [Fabricate(:topic_status_update, execute_at: 5.hours.from_now)]
+ )
end
- context 'an existing topic' do
- it 'when auto_close_at is set, it queues a job to close the topic' do
- Timecop.freeze(now) do
- topic = Fabricate(:topic)
- Jobs.expects(:enqueue_at).with(12.hours.from_now, :close_topic, has_entries(topic_id: topic.id, user_id: topic.user_id))
- topic.auto_close_at = 12.hours.from_now
- expect(topic.save).to eq(true)
- end
- end
-
- it 'when auto_close_at and auto_closer_user_id are set, it queues a job to close the topic' do
- Timecop.freeze(now) do
- topic = Fabricate(:topic)
- closer = Fabricate(:admin)
- Jobs.expects(:enqueue_at).with(12.hours.from_now, :close_topic, has_entries(topic_id: topic.id, user_id: closer.id))
- topic.auto_close_at = 12.hours.from_now
- topic.auto_close_user = closer
- expect(topic.save).to eq(true)
- end
- end
-
- it 'when auto_close_at is removed, it cancels the job to close the topic' do
- Jobs.stubs(:enqueue_at).returns(true)
- topic = Fabricate(:topic, auto_close_at: 1.day.from_now)
- Jobs.expects(:cancel_scheduled_job).with(:close_topic, {topic_id: topic.id})
- topic.auto_close_at = nil
- expect(topic.save).to eq(true)
- expect(topic.auto_close_user).to eq(nil)
- end
-
- it 'when auto_close_user is removed, it updates the job' do
- Timecop.freeze(now) do
- Jobs.stubs(:enqueue_at).with(1.day.from_now, :close_topic, anything).returns(true)
- topic = Fabricate(:topic, auto_close_at: 1.day.from_now, auto_close_user: Fabricate(:admin))
- Jobs.expects(:cancel_scheduled_job).with(:close_topic, {topic_id: topic.id})
- Jobs.expects(:enqueue_at).with(1.day.from_now, :close_topic, has_entries(topic_id: topic.id, user_id: topic.user_id))
- topic.auto_close_user = nil
- expect(topic.save).to eq(true)
- end
- end
-
- it 'when auto_close_at value is changed, it reschedules the job' do
- Timecop.freeze(now) do
- Jobs.stubs(:enqueue_at).returns(true)
- topic = Fabricate(:topic, auto_close_at: 1.day.from_now)
- Jobs.expects(:cancel_scheduled_job).with(:close_topic, {topic_id: topic.id})
- Jobs.expects(:enqueue_at).with(3.days.from_now, :close_topic, has_entry(topic_id: topic.id))
- topic.auto_close_at = 3.days.from_now
- expect(topic.save).to eq(true)
- end
- end
-
- it 'when auto_close_user_id is changed, it updates the job' do
- Timecop.freeze(now) do
- admin = Fabricate(:admin)
- Jobs.stubs(:enqueue_at).returns(true)
- topic = Fabricate(:topic, auto_close_at: 1.day.from_now)
- Jobs.expects(:cancel_scheduled_job).with(:close_topic, {topic_id: topic.id})
- Jobs.expects(:enqueue_at).with(1.day.from_now, :close_topic, has_entries(topic_id: topic.id, user_id: admin.id))
- topic.auto_close_user = admin
- expect(topic.save).to eq(true)
- end
- end
-
- it 'when auto_close_at and auto_close_user_id are not changed, it should not schedule another CloseTopic job' do
- Timecop.freeze(now) do
- Jobs.expects(:enqueue_at).with(1.day.from_now, :close_topic, has_key(:topic_id)).once.returns(true)
- Jobs.expects(:cancel_scheduled_job).never
- topic = Fabricate(:topic, auto_close_at: 1.day.from_now)
- topic.title = 'A new title that is long enough'
- expect(topic.save).to eq(true)
- end
- end
-
- it "ignores the category's default auto-close" do
- Timecop.freeze(now) do
- mod = Fabricate(:moderator)
- # NOTE, only moderators can auto-close, if missing system user is used
- topic = Fabricate(:topic, category: Fabricate(:category, auto_close_hours: 14), user: mod)
- Jobs.expects(:enqueue_at).with(12.hours.from_now, :close_topic, has_entries(topic_id: topic.id, user_id: topic.user_id))
- topic.auto_close_at = 12.hours.from_now
- topic.save
-
- topic.reload
- expect(topic.closed).to eq(false)
-
- Timecop.freeze(24.hours.from_now) do
- Topic.auto_close
- topic.reload
- expect(topic.closed).to eq(true)
- end
-
- end
- end
- end
- end
-
- describe 'set_auto_close' do
- let(:topic) { Fabricate.build(:topic) }
- let(:closing_topic) { Fabricate.build(:topic, auto_close_hours: 5, auto_close_at: 5.hours.from_now, auto_close_started_at: 5.hours.from_now) }
- let(:admin) { Fabricate.build(:user, id: 123) }
- let(:trust_level_4) { Fabricate.build(:trust_level_4) }
+ let(:admin) { Fabricate(:admin) }
+ let(:trust_level_4) { Fabricate(:trust_level_4) }
before { Discourse.stubs(:system_user).returns(admin) }
it 'can take a number of hours as an integer' do
Timecop.freeze(now) do
- topic.set_auto_close(72, {by_user: admin})
- expect(topic.auto_close_at).to eq(3.days.from_now)
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 72, by_user: admin)
+ expect(topic.topic_status_updates.first.execute_at).to eq(3.days.from_now)
end
end
it 'can take a number of hours as an integer, with timezone offset' do
Timecop.freeze(now) do
- topic.set_auto_close(72, {by_user: admin, timezone_offset: 240})
- expect(topic.auto_close_at).to eq(3.days.from_now)
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 72, {by_user: admin, timezone_offset: 240})
+ expect(topic.topic_status_updates.first.execute_at).to eq(3.days.from_now)
end
end
it 'can take a number of hours as a string' do
Timecop.freeze(now) do
- topic.set_auto_close('18', {by_user: admin})
- expect(topic.auto_close_at).to eq(18.hours.from_now)
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '18', by_user: admin)
+ expect(topic.topic_status_updates.first.execute_at).to eq(18.hours.from_now)
end
end
it 'can take a number of hours as a string, with timezone offset' do
Timecop.freeze(now) do
- topic.set_auto_close('18', {by_user: admin, timezone_offset: 240})
- expect(topic.auto_close_at).to eq(18.hours.from_now)
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '18', {by_user: admin, timezone_offset: 240})
+ expect(topic.topic_status_updates.first.execute_at).to eq(18.hours.from_now)
end
end
it "can take a time later in the day" do
Timecop.freeze(now) do
- topic.set_auto_close('13:00', {by_user: admin})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,20,13,0))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '13:00', {by_user: admin})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,20,13,0))
end
end
it "can take a time later in the day, with timezone offset" do
Timecop.freeze(now) do
- topic.set_auto_close('13:00', {by_user: admin, timezone_offset: 240})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,20,17,0))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '13:00', {by_user: admin, timezone_offset: 240})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,20,17,0))
end
end
it "can take a time for the next day" do
Timecop.freeze(now) do
- topic.set_auto_close('5:00', {by_user: admin})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,21,5,0))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '5:00', {by_user: admin})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,21,5,0))
end
end
it "can take a time for the next day, with timezone offset" do
Timecop.freeze(now) do
- topic.set_auto_close('1:00', {by_user: admin, timezone_offset: 240})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,21,5,0))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '1:00', {by_user: admin, timezone_offset: 240})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,21,5,0))
end
end
it "can take a timestamp for a future time" do
Timecop.freeze(now) do
- topic.set_auto_close('2013-11-22 5:00', {by_user: admin})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,22,5,0))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '2013-11-22 5:00', {by_user: admin})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,22,5,0))
end
end
it "can take a timestamp for a future time, with timezone offset" do
Timecop.freeze(now) do
- topic.set_auto_close('2013-11-22 5:00', {by_user: admin, timezone_offset: 240})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,22,9,0))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '2013-11-22 5:00', {by_user: admin, timezone_offset: 240})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,22,9,0))
end
end
it "sets a validation error when given a timestamp in the past" do
Timecop.freeze(now) do
- topic.set_auto_close('2013-11-19 5:00', {by_user: admin})
- expect(topic.auto_close_at).to eq(Time.zone.local(2013,11,19,5,0))
- expect(topic.errors[:auto_close_at]).to be_present
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '2013-11-19 5:00', {by_user: admin})
+
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.zone.local(2013,11,19,5,0))
+ expect(topic.topic_status_updates.first.errors[:execute_at]).to be_present
end
end
it "can take a timestamp with timezone" do
Timecop.freeze(now) do
- topic.set_auto_close('2013-11-25T01:35:00-08:00', {by_user: admin})
- expect(topic.auto_close_at).to eq(Time.utc(2013,11,25,9,35))
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '2013-11-25T01:35:00-08:00', {by_user: admin})
+ expect(topic.topic_status_updates.first.execute_at).to eq(Time.utc(2013,11,25,9,35))
end
end
- it 'sets auto_close_user to given user if it is a staff or TL4 user' do
- topic.set_auto_close(3, {by_user: admin})
- expect(topic.auto_close_user_id).to eq(admin.id)
+ it 'sets topic status update user to given user if it is a staff or TL4 user' do
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 3, {by_user: admin})
+ expect(topic.topic_status_updates.first.user).to eq(admin)
end
- it 'sets auto_close_user to given user if it is a TL4 user' do
- topic.set_auto_close(3, {by_user: trust_level_4})
- expect(topic.auto_close_user_id).to eq(trust_level_4.id)
+ it 'sets topic status update user to given user if it is a TL4 user' do
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 3, {by_user: trust_level_4})
+ expect(topic.topic_status_updates.first.user).to eq(trust_level_4)
end
- it 'sets auto_close_user to system user if given user is not staff or a TL4 user' do
- topic.set_auto_close(3, {by_user: Fabricate.build(:user, id: 444)})
- expect(topic.auto_close_user_id).to eq(admin.id)
+ it 'sets topic status update user to system user if given user is not staff or a TL4 user' do
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 3, {by_user: Fabricate.build(:user, id: 444)})
+ expect(topic.topic_status_updates.first.user).to eq(admin)
end
- it 'sets auto_close_user to system user if user is not given and topic creator is not staff nor TL4 user' do
- topic.set_auto_close(3)
- expect(topic.auto_close_user_id).to eq(admin.id)
+ it 'sets topic status update user to system user if user is not given and topic creator is not staff nor TL4 user' do
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 3)
+ expect(topic.topic_status_updates.first.user).to eq(admin)
end
- it 'sets auto_close_user to topic creator if it is a staff user' do
+ it 'sets topic status update user to topic creator if it is a staff user' do
staff_topic = Fabricate.build(:topic, user: Fabricate.build(:admin, id: 999))
- staff_topic.set_auto_close(3)
- expect(staff_topic.auto_close_user_id).to eq(999)
+ staff_topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 3)
+ expect(staff_topic.topic_status_updates.first.user_id).to eq(999)
end
- it 'sets auto_close_user to topic creator if it is a TL4 user' do
+ it 'sets topic status update user to topic creator if it is a TL4 user' do
tl4_topic = Fabricate.build(:topic, user: Fabricate.build(:trust_level_4, id: 998))
- tl4_topic.set_auto_close(3)
- expect(tl4_topic.auto_close_user_id).to eq(998)
+ tl4_topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 3)
+ expect(tl4_topic.topic_status_updates.first.user_id).to eq(998)
end
- it 'clears auto_close_at if arg is nil' do
- closing_topic.set_auto_close(nil)
- expect(closing_topic.auto_close_at).to be_nil
+ it 'removes close topic status update if arg is nil' do
+ closing_topic.set_or_create_status_update(TopicStatusUpdate.types[:close], nil)
+ closing_topic.reload
+ expect(closing_topic.topic_status_updates.first).to be_nil
end
- it 'clears auto_close_started_at if arg is nil' do
- closing_topic.set_auto_close(nil)
- expect(closing_topic.auto_close_started_at).to be_nil
- end
-
- it 'updates auto_close_at if it was already set to close' do
+ it 'updates topic status update execute_at if it was already set to close' do
Timecop.freeze(now) do
- closing_topic.set_auto_close(48)
- expect(closing_topic.auto_close_at).to eq(2.days.from_now)
+ closing_topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 48)
+ expect(closing_topic.reload.topic_status_update.execute_at).to eq(2.days.from_now)
end
end
- it 'does not update auto_close_started_at if it was already set to close' do
+ it "does not update topic's topic status created_at it was already set to close" do
expect{
- closing_topic.set_auto_close(14)
- }.to_not change(closing_topic, :auto_close_started_at)
+ closing_topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 14)
+ }.to_not change { closing_topic.topic_status_updates.first.created_at }
+ end
+
+ describe "when category's default auto close is set" do
+ let(:category) { Fabricate(:category, auto_close_hours: 4) }
+ let(:topic) { Fabricate(:topic, category: category) }
+
+ it "should be able to override category's default auto close" do
+ expect(topic.topic_status_updates.first.duration).to eq(4)
+
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], 2, by_user: admin)
+
+ expect(topic.reload.closed).to eq(false)
+
+ Timecop.freeze(3.hours.from_now) do
+ TopicStatusUpdate.ensure_consistency!
+ expect(topic.reload.closed).to eq(true)
+ end
+ end
end
end
diff --git a/spec/models/topic_status_update_spec.rb b/spec/models/topic_status_update_spec.rb
index 549adb9e872..f9b1c9a895d 100644
--- a/spec/models/topic_status_update_spec.rb
+++ b/spec/models/topic_status_update_spec.rb
@@ -1,56 +1,187 @@
-# encoding: UTF-8
-
require 'rails_helper'
-require_dependency 'post_destroyer'
-# TODO - test pinning, create_moderator_post
+RSpec.describe TopicStatusUpdate, type: :model do
+ let(:topic_status_update) { Fabricate(:topic_status_update) }
+ let(:topic) { Fabricate(:topic) }
-describe TopicStatusUpdate do
+ context "validations" do
+ describe '#status_type' do
+ it 'should ensure that only one active topic status update exists' do
+ topic_status_update.update!(topic: topic)
+ Fabricate(:topic_status_update, deleted_at: Time.zone.now, topic: topic)
- let(:user) { Fabricate(:user) }
- let(:admin) { Fabricate(:admin) }
+ expect { Fabricate(:topic_status_update, topic: topic) }
+ .to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
- it "avoids notifying on automatically closed topics" do
- # TODO: TopicStatusUpdate should suppress message bus updates from the users it "pretends to read"
- post = PostCreator.create(user,
- raw: "this is a test post 123 this is a test post",
- title: "hello world title",
- )
- # TODO needed so counts sync up, PostCreator really should not give back out-of-date Topic
- post.topic.set_auto_close('10')
- post.topic.reload
+ describe '#execute_at' do
+ describe 'when #execute_at is greater than #created_at' do
+ it 'should be valid' do
+ topic_status_update = Fabricate.build(:topic_status_update,
+ execute_at: Time.zone.now + 1.hour,
+ user: Fabricate(:user),
+ topic: Fabricate(:topic)
+ )
- TopicStatusUpdate.new(post.topic, admin).update!("autoclosed", true)
+ expect(topic_status_update).to be_valid
+ end
+ end
- expect(post.topic.posts.count).to eq(2)
+ describe 'when #execute_at is smaller than #created_at' do
+ it 'should not be valid' do
+ topic_status_update = Fabricate.build(:topic_status_update,
+ execute_at: Time.zone.now - 1.hour,
+ created_at: Time.zone.now,
+ user: Fabricate(:user),
+ topic: Fabricate(:topic)
+ )
- tu = TopicUser.find_by(user_id: user.id)
- expect(tu.last_read_post_number).to eq(2)
+ expect(topic_status_update).to_not be_valid
+ end
+ end
+ end
end
- it "adds an autoclosed message" do
- topic = create_topic
- topic.set_auto_close('10')
+ context 'callbacks' do
+ describe 'when #execute_at and #user_id are not changed' do
+ it 'should not schedule another to update topic' do
+ Jobs.expects(:enqueue_at).with(
+ topic_status_update.execute_at,
+ :toggle_topic_closed,
+ topic_status_update_id: topic_status_update.id,
+ state: true
+ ).once
- TopicStatusUpdate.new(topic, admin).update!("autoclosed", true)
+ topic_status_update
- last_post = topic.posts.last
- expect(last_post.post_type).to eq(Post.types[:small_action])
- expect(last_post.action_code).to eq('autoclosed.enabled')
- expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_minutes", count: 0))
+ Jobs.expects(:cancel_scheduled_job).never
+
+ topic_status_update.update!(topic: Fabricate(:topic))
+ end
+ end
+
+ describe 'when #execute_at value is changed' do
+ it 'reschedules the job' do
+ Timecop.freeze do
+ topic_status_update
+
+ Jobs.expects(:cancel_scheduled_job).with(
+ :toggle_topic_closed, topic_status_update_id: topic_status_update.id
+ )
+
+ Jobs.expects(:enqueue_at).with(
+ 3.days.from_now, :toggle_topic_closed,
+ topic_status_update_id: topic_status_update.id,
+ state: true
+ )
+
+ topic_status_update.update!(execute_at: 3.days.from_now, created_at: Time.zone.now)
+ end
+ end
+
+ describe 'when execute_at is smaller than the current time' do
+ it 'should enqueue the job immediately' do
+ Timecop.freeze do
+ topic_status_update
+
+ Jobs.expects(:enqueue_at).with(
+ Time.zone.now, :toggle_topic_closed,
+ topic_status_update_id: topic_status_update.id,
+ state: true
+ )
+
+ topic_status_update.update!(
+ execute_at: Time.zone.now - 1.hour,
+ created_at: Time.zone.now - 2.hour
+ )
+ end
+ end
+ end
+ end
+
+ describe 'when user is changed' do
+ it 'should update the job' do
+ Timecop.freeze do
+ topic_status_update
+
+ Jobs.expects(:cancel_scheduled_job).with(
+ :toggle_topic_closed, topic_status_update_id: topic_status_update.id
+ )
+
+ admin = Fabricate(:admin)
+
+ Jobs.expects(:enqueue_at).with(
+ topic_status_update.execute_at,
+ :toggle_topic_closed,
+ topic_status_update_id: topic_status_update.id,
+ state: true
+ )
+
+ topic_status_update.update!(user: admin)
+ end
+ end
+ end
end
- it "adds an autoclosed message based on last post" do
- topic = create_topic
- topic.auto_close_based_on_last_post = true
- topic.set_auto_close('10')
+ describe '.ensure_consistency!' do
+ before do
+ SiteSetting.queue_jobs = true
+ Jobs::ToggleTopicClosed.jobs.clear
+ end
- TopicStatusUpdate.new(topic, admin).update!("autoclosed", true)
+ it 'should enqueue jobs that have been missed' do
+ close_topic_status_update = Fabricate(:topic_status_update,
+ execute_at: Time.zone.now - 1.hour,
+ created_at: Time.zone.now - 2.hour
+ )
- last_post = topic.posts.last
- expect(last_post.post_type).to eq(Post.types[:small_action])
- expect(last_post.action_code).to eq('autoclosed.enabled')
- expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_lastpost_hours", count: 10))
+ open_topic_status_update = Fabricate(:topic_status_update,
+ status_type: described_class.types[:open],
+ execute_at: Time.zone.now - 1.hour,
+ created_at: Time.zone.now - 2.hour
+ )
+
+ Fabricate(:topic_status_update)
+
+ expect { described_class.ensure_consistency! }
+ .to change { Jobs::ToggleTopicClosed.jobs.count }.by(2)
+
+ job_args = Jobs::ToggleTopicClosed.jobs.first["args"].first
+
+ expect(job_args["topic_status_update_id"]).to eq(close_topic_status_update.id)
+ expect(job_args["state"]).to eq(true)
+
+ job_args = Jobs::ToggleTopicClosed.jobs.last["args"].first
+
+ expect(job_args["topic_status_update_id"]).to eq(open_topic_status_update.id)
+ expect(job_args["state"]).to eq(false)
+ end
end
+ describe 'when a open topic status update is created for an open topic' do
+ it 'should close the topic' do
+ topic = Fabricate(:topic, closed: false)
+
+ Fabricate(:topic_status_update,
+ status_type: described_class.types[:open],
+ topic: topic
+ )
+
+ expect(topic.reload.closed).to eq(true)
+ end
+ end
+
+ describe 'when a close topic status update is created for a closed topic' do
+ it 'should open the topic' do
+ topic = Fabricate(:topic, closed: true)
+
+ Fabricate(:topic_status_update,
+ status_type: described_class.types[:close],
+ topic: topic
+ )
+
+ expect(topic.reload.closed).to eq(false)
+ end
+ end
end
diff --git a/spec/services/topic_status_updater_spec.rb b/spec/services/topic_status_updater_spec.rb
new file mode 100644
index 00000000000..3609082ec64
--- /dev/null
+++ b/spec/services/topic_status_updater_spec.rb
@@ -0,0 +1,59 @@
+# encoding: UTF-8
+
+require 'rails_helper'
+require_dependency 'post_destroyer'
+
+# TODO - test pinning, create_moderator_post
+
+describe TopicStatusUpdater do
+
+ let(:user) { Fabricate(:user) }
+ let(:admin) { Fabricate(:admin) }
+
+ it "avoids notifying on automatically closed topics" do
+ # TODO: TopicStatusUpdater should suppress message bus updates from the users it "pretends to read"
+ post = PostCreator.create(user,
+ raw: "this is a test post 123 this is a test post",
+ title: "hello world title",
+ )
+ # TODO needed so counts sync up, PostCreator really should not give back out-of-date Topic
+ post.topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '10')
+ post.topic.reload
+
+ TopicStatusUpdater.new(post.topic, admin).update!("autoclosed", true)
+
+ expect(post.topic.posts.count).to eq(2)
+
+ tu = TopicUser.find_by(user_id: user.id)
+ expect(tu.last_read_post_number).to eq(2)
+ end
+
+ it "adds an autoclosed message" do
+ topic = create_topic
+ topic.set_or_create_status_update(TopicStatusUpdate.types[:close], '10')
+
+ TopicStatusUpdater.new(topic, admin).update!("autoclosed", true)
+
+ last_post = topic.posts.last
+ expect(last_post.post_type).to eq(Post.types[:small_action])
+ expect(last_post.action_code).to eq('autoclosed.enabled')
+ expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_minutes", count: 0))
+ end
+
+ it "adds an autoclosed message based on last post" do
+ topic = create_topic
+ Fabricate(:post, topic: topic)
+
+ topic.set_or_create_status_update(
+ TopicStatusUpdate.types[:close], '10', based_on_last_post: true
+ )
+
+ TopicStatusUpdater.new(topic, admin).update!("autoclosed", true)
+
+ last_post = topic.posts.last
+ expect(last_post.post_type).to eq(Post.types[:small_action])
+ expect(last_post.action_code).to eq('autoclosed.enabled')
+ expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_lastpost_hours", count: 10))
+ end
+
+end