');
+
+ this.rerender.bind(this).delay(reRenderDelay);
+ }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js
index b3cca664928..f278a182153 100644
--- a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js
+++ b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js
@@ -2,7 +2,7 @@
This view is used for rendering the buttons at the footer of the topic
@class TopicFooterButtonsView
- @extends Discourse.View
+ @extends Ember.ContainerView
@namespace Discourse
@module Discourse
**/
diff --git a/app/assets/stylesheets/application/compose.css.scss b/app/assets/stylesheets/application/compose.css.scss
index 5d816216ef6..c0cefe544ee 100644
--- a/app/assets/stylesheets/application/compose.css.scss
+++ b/app/assets/stylesheets/application/compose.css.scss
@@ -234,6 +234,7 @@
margin: 6px 10px 3px 0;
}
.wmd-controls {
+ @include transition(top 0.3s ease);
top: 100px;
}
}
@@ -365,6 +366,7 @@ div.ac-wrap {
#reply-control.edit-title.private-message {
.wmd-controls {
+ @include transition(top 0.3s ease);
top: 140px;
}
}
@@ -466,3 +468,29 @@ div.ac-wrap {
}
}
}
+
+.admin-options-form {
+ margin-top: 8px;
+ display: none;
+}
+
+.auto-close-fields {
+ input {
+ width: 50px;
+ }
+}
+
+.edit-auto-close-modal {
+ form {
+ margin: 0;
+ }
+ .auto-close-fields {
+ i.icon-time {
+ font-size: 16px;
+ line-height: 8px;
+ }
+ input {
+ margin: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/application/topic-admin-menu.css.scss b/app/assets/stylesheets/application/topic-admin-menu.css.scss
index 427d65efb2e..78d0f7c93d4 100644
--- a/app/assets/stylesheets/application/topic-admin-menu.css.scss
+++ b/app/assets/stylesheets/application/topic-admin-menu.css.scss
@@ -8,11 +8,6 @@
top: 70px;
right: 10px;
z-index: 1000;
-
- i {
- margin: 0px;
- line-height: 10px;
- }
}
.topic-admin-menu {
diff --git a/app/assets/stylesheets/application/topic.css.scss b/app/assets/stylesheets/application/topic.css.scss
index 02cc7485e48..972d38978f0 100644
--- a/app/assets/stylesheets/application/topic.css.scss
+++ b/app/assets/stylesheets/application/topic.css.scss
@@ -329,6 +329,10 @@
}
}
}
+
+ #topic-closing-info {
+ margin-left: 103px;
+ }
}
kbd {
diff --git a/app/assets/stylesheets/components/buttons.css.scss b/app/assets/stylesheets/components/buttons.css.scss
index 0f052094514..d65cd88dcbc 100644
--- a/app/assets/stylesheets/components/buttons.css.scss
+++ b/app/assets/stylesheets/components/buttons.css.scss
@@ -29,6 +29,12 @@
.icon {
margin-right: 7px;
}
+ &.no-text {
+ .icon {
+ margin-right: 0;
+ line-height: 10px;
+ }
+ }
}
// Default button
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index c4fb6c7b8fb..b6c78eb38e1 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -36,7 +36,8 @@ class PostsController < ApplicationController
target_usernames: params[:target_usernames],
reply_to_post_number: params[:post][:reply_to_post_number],
image_sizes: params[:image_sizes],
- meta_data: params[:meta_data])
+ meta_data: params[:meta_data],
+ auto_close_days: params[:auto_close_days])
post = post_creator.create
if post_creator.errors.present?
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index e1ca6a65f0f..145e19939ac 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -15,7 +15,8 @@ class TopicsController < ApplicationController
:unmute,
:set_notifications,
:move_posts,
- :clear_pin]
+ :clear_pin,
+ :autoclose]
before_filter :consider_user_for_promotion, only: :show
@@ -97,6 +98,16 @@ class TopicsController < ApplicationController
toggle_mute(false)
end
+ def autoclose
+ requires_parameter(:auto_close_days)
+ @topic = Topic.where(id: params[:topic_id].to_i).first
+ guardian.ensure_can_moderate!(@topic)
+ @topic.auto_close_days = params[:auto_close_days]
+ @topic.auto_close_user = current_user
+ @topic.save
+ render nothing: true
+ end
+
def destroy
topic = Topic.where(id: params[:id]).first
guardian.ensure_can_delete!(topic)
diff --git a/app/models/topic.rb b/app/models/topic.rb
index fb96ceb6455..27a3d1f13b0 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -61,6 +61,7 @@ class Topic < ActiveRecord::Base
belongs_to :featured_user2, class_name: 'User', foreign_key: :featured_user2_id
belongs_to :featured_user3, class_name: 'User', foreign_key: :featured_user3_id
belongs_to :featured_user4, class_name: 'User', foreign_key: :featured_user4_id
+ belongs_to :auto_close_user, class_name: 'User', foreign_key: :auto_close_user_id
has_many :topic_users
has_many :topic_links
@@ -108,6 +109,18 @@ class Topic < ActiveRecord::Base
end
end
+ before_save do
+ if (auto_close_at_changed? and !auto_close_at_was.nil?) or (auto_close_user_id_changed? and auto_close_at)
+ Jobs.cancel_scheduled_job(:close_topic, {topic_id: id})
+ end
+ end
+
+ after_save do
+ if auto_close_at and (auto_close_at_changed? or auto_close_user_id_changed?)
+ Jobs.enqueue_at(auto_close_at, :close_topic, {topic_id: id, user_id: auto_close_user_id || user_id})
+ end
+ end
+
# all users (in groups or directly targetted) that are going to get the pm
def all_allowed_users
# TODO we should probably change this from 3 queries to 1
@@ -264,7 +277,7 @@ class Topic < ActiveRecord::Base
update_pinned(status)
else
# otherwise update the column
- update_column(property, status)
+ update_column(property == 'autoclosed' ? 'closed' : property, status)
end
key = "topic_statuses.#{property}_"
@@ -273,9 +286,11 @@ class Topic < ActiveRecord::Base
opts = {}
# We don't bump moderator posts except for the re-open post.
- opts[:bump] = true if property == 'closed' and (!status)
+ opts[:bump] = true if (property == 'closed' or property == 'autoclosed') and (!status)
- add_moderator_post(user, I18n.t(key), opts)
+ message = property != 'autoclosed' ? I18n.t(key) : I18n.t(key, count: (((self.auto_close_at||Time.zone.now) - self.created_at) / 86_400).round )
+
+ add_moderator_post(user, message, opts)
end
end
@@ -712,4 +727,9 @@ class Topic < ActiveRecord::Base
def notify_muted!(user)
TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:muted])
end
+
+ def auto_close_days=(num_days)
+ self.auto_close_at = (num_days and num_days.to_i > 0.0 ? num_days.to_i.days.from_now : nil)
+ end
+
end
diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb
index ef3753a6628..843fdb44a1a 100644
--- a/app/serializers/topic_view_serializer.rb
+++ b/app/serializers/topic_view_serializer.rb
@@ -18,7 +18,8 @@ class TopicViewSerializer < ApplicationSerializer
:moderator_posts_count,
:has_best_of,
:archetype,
- :slug]
+ :slug,
+ :auto_close_at]
end
def self.guardian_attributes
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index a67f0d455e0..7c5ff5c5bc6 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -32,6 +32,19 @@ en:
now: "just now"
read_more: 'read more'
+ in_n_seconds:
+ one: "in 1 second"
+ other: "in {{count}} seconds"
+ in_n_minutes:
+ one: "in 1 minute"
+ other: "in {{count}} minutes"
+ in_n_hours:
+ one: "in 1 hour"
+ other: "in {{count}} hours"
+ in_n_days:
+ one: "in 1 day"
+ other: "in {{count}} days"
+
suggested_topics:
title: "Suggested Topics"
@@ -370,6 +383,9 @@ en:
help: "Markdown Editing Help"
toggler: "hide or show the composer panel"
+ auto_close_label: "Auto-close topic after:"
+ auto_close_units: "days"
+
notifications:
title: "notifications of @name mentions, replies to your posts and topics, private messages, etc"
none: "You have no notifications right now."
@@ -479,6 +495,12 @@ en:
jump_reply_down: jump to later reply
deleted: "The topic has been deleted"
+ auto_close_notice: "This topic will close %{timeLeft}"
+ auto_close_title: 'Auto-Close Settings'
+ auto_close_save: "Save"
+ auto_close_cancel: "Cancel"
+ auto_close_remove: "Don't Auto-Close This Topic"
+
progress:
title: topic progress
jump_top: jump to first post
@@ -516,6 +538,7 @@ en:
delete: "Delete Topic"
open: "Open Topic"
close: "Close Topic"
+ auto_close: "Auto Close"
unpin: "Un-Pin Topic"
pin: "Pin Topic"
unarchive: "Unarchive Topic"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 4e3ba554476..7c02e0ebcda 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -607,6 +607,11 @@ en:
archived_disabled: "This topic is now unarchived. It is no longer frozen, and can be changed."
closed_enabled: "This topic is now closed. New replies are no longer allowed."
closed_disabled: "This topic is now opened. New replies are allowed."
+ autoclosed_enabled:
+ zero: "This topic was automatically closed after 1 day. New replies are no longer allowed."
+ one: "This topic was automatically closed after 1 day. New replies are no longer allowed."
+ other: "This topic was automatically closed after %{count} days. New replies are no longer allowed."
+ autoclosed_disabled: "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 either unpinned by a moderator, or the Clear Pin button is pressed."
pinned_disabled: "This topic is now unpinned. It will no longer appear at the top of its category."
visible_enabled: "This topic is now visible. It will be displayed in topic lists."
diff --git a/config/routes.rb b/config/routes.rb
index f70bb870cf1..e8c8b4e0a79 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -203,6 +203,7 @@ Discourse::Application.routes.draw do
put 't/:topic_id/clear-pin' => 'topics#clear_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+/}
get 't/:topic_id/:post_number' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/}
get 't/:slug/:topic_id.rss' => 'topics#feed', format: :rss, constraints: {topic_id: /\d+/}
diff --git a/db/migrate/20130506185042_add_auto_close_at_to_topics.rb b/db/migrate/20130506185042_add_auto_close_at_to_topics.rb
new file mode 100644
index 00000000000..c4f9987f58c
--- /dev/null
+++ b/db/migrate/20130506185042_add_auto_close_at_to_topics.rb
@@ -0,0 +1,6 @@
+class AddAutoCloseAtToTopics < ActiveRecord::Migration
+ def change
+ add_column :topics, :auto_close_at, :datetime
+ add_column :topics, :auto_close_user_id, :integer
+ end
+end
diff --git a/lib/jobs.rb b/lib/jobs.rb
index 340d5a55249..f413cf1e597 100644
--- a/lib/jobs.rb
+++ b/lib/jobs.rb
@@ -99,6 +99,34 @@ module Jobs
enqueue(job_name, opts.merge!(delay_for: secs))
end
+ def self.enqueue_at(datetime, job_name, opts={})
+ enqueue_in( [(datetime - Time.zone.now).to_i, 0].max, job_name, opts )
+ end
+
+ # TODO: should take job_name like enqueue methods
+ def self.cancel_scheduled_job(job_name, params={})
+ job_class = "Jobs::#{job_name.to_s.camelcase}"
+ matched = true
+ Sidekiq::ScheduledSet.new.each do |scheduled_job|
+ if scheduled_job.klass == 'Sidekiq::Extensions::DelayedClass'
+ job_args = YAML.load(scheduled_job.args[0])
+ if job_args[0] == job_class
+ next unless job_args[2] and job_args[2][0]
+ matched = true
+ params.each do |key, value|
+ unless job_args[2][0][key] == value
+ matched = false
+ break
+ end
+ end
+ next unless matched
+ end
+ scheduled_job.delete
+ break
+ end
+ end
+ matched
+ end
end
# Require all jobs
diff --git a/lib/jobs/close_topic.rb b/lib/jobs/close_topic.rb
new file mode 100644
index 00000000000..61c8d42b5fb
--- /dev/null
+++ b/lib/jobs/close_topic.rb
@@ -0,0 +1,15 @@
+module Jobs
+ class CloseTopic < Jobs::Base
+
+ def execute(args)
+ topic = Topic.find(args[:topic_id])
+ if topic.auto_close_at
+ closer = User.find(args[:user_id])
+ if Guardian.new(closer).can_moderate?(topic)
+ topic.update_status('autoclosed', true, closer)
+ end
+ end
+ end
+
+ end
+end
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index c48073f593c..beb927ece29 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -53,6 +53,11 @@ class PostCreator
topic = Topic.new(topic_params)
+ if @opts[:auto_close_days]
+ guardian.ensure_can_moderate!(topic)
+ topic.auto_close_days = @opts[:auto_close_days]
+ end
+
if @opts[:archetype] == Archetype.private_message
topic.subtype = TopicSubtype.user_to_user unless topic.subtype
diff --git a/spec/components/jobs/close_topic_spec.rb b/spec/components/jobs/close_topic_spec.rb
new file mode 100644
index 00000000000..3850917ba02
--- /dev/null
+++ b/spec/components/jobs/close_topic_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require 'jobs'
+
+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).returns(topic)
+ User.stubs(:find).returns(admin)
+ Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
+ end
+
+ it 'does nothing if the topic is not set to auto-close' do
+ topic = Fabricate.build(:topic, auto_close_at: nil, user: admin)
+ topic.expects(:update_status).never
+ Topic.stubs(:find).returns(topic)
+ User.stubs(:find).returns(admin)
+ Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
+ end
+
+ it 'does nothing if the user is not authorized to close the topic' do
+ topic = Fabricate.build(:topic, auto_close_at: Time.zone.now, user: admin)
+ topic.expects(:update_status).never
+ Topic.stubs(:find).returns(topic)
+ User.stubs(:find).returns(admin)
+ Guardian.any_instance.stubs(:can_moderate?).returns(false)
+ Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
+ end
+
+ it 'does nothing if the topic is already closed'
+
+end
\ No newline at end of file
diff --git a/spec/components/jobs_spec.rb b/spec/components/jobs_spec.rb
index b769c4c1f9b..cfde2214841 100644
--- a/spec/components/jobs_spec.rb
+++ b/spec/components/jobs_spec.rb
@@ -75,5 +75,41 @@ describe Jobs do
end
+ describe 'cancel_scheduled_job' do
+ it 'deletes the matching job' do
+ job_to_delete = stub_everything(klass: 'Sidekiq::Extensions::DelayedClass', args: [YAML.dump(['Jobs::DrinkBeer', :delayed_perform, [{beer_id: 42}]])])
+ job_to_delete.expects(:delete)
+ job_to_keep1 = stub_everything(klass: 'Sidekiq::Extensions::DelayedClass', args: [YAML.dump(['Jobs::DrinkBeer', :delayed_perform, [{beer_id: 43}]])])
+ job_to_keep1.expects(:delete).never
+ job_to_keep2 = stub_everything(klass: 'Sidekiq::Extensions::DelayedClass', args: [YAML.dump(['Jobs::DrinkBeer', :delayed_perform, [{beer_id: 44}]])])
+ job_to_keep2.expects(:delete).never
+ Sidekiq::ScheduledSet.stubs(:new).returns( [job_to_keep1, job_to_delete, job_to_keep2] )
+ Jobs.cancel_scheduled_job(:drink_beer, {beer_id: 42}).should be_true
+ end
+
+ it 'returns false when no matching job is scheduled' do
+ job_to_keep = stub_everything(klass: 'Sidekiq::Extensions::DelayedClass', args: [YAML.dump(['Jobs::DrinkBeer', :delayed_perform, [{beer_id: 43}]])])
+ job_to_keep.expects(:delete).never
+ Sidekiq::ScheduledSet.stubs(:new).returns( [job_to_keep] )
+ Jobs.cancel_scheduled_job(:drink_beer, {beer_id: 42}).should be_false
+ end
+ end
+
+ describe 'enqueue_at' do
+ it 'calls enqueue_in for you' do
+ Timecop.freeze(Time.zone.now) do
+ Jobs.expects(:enqueue_in).with(3 * 60 * 60, :eat_lunch, {}).returns(true)
+ Jobs.enqueue_at(3.hours.from_now, :eat_lunch, {})
+ end
+ end
+
+ it 'handles datetimes that are in the past' do
+ Timecop.freeze(Time.zone.now) do
+ Jobs.expects(:enqueue_in).with(0, :eat_lunch, {}).returns(true)
+ Jobs.enqueue_at(3.hours.ago, :eat_lunch, {})
+ end
+ end
+ end
+
end
diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb
index d159e5f7ebd..c152a5addc3 100644
--- a/spec/components/post_creator_spec.rb
+++ b/spec/components/post_creator_spec.rb
@@ -98,6 +98,14 @@ describe PostCreator do
end
end
+ context 'when auto-close param is given' do
+ it 'ensures the user can auto-close the topic' do
+ Guardian.any_instance.stubs(:can_moderate?).returns(false)
+ expect {
+ PostCreator.new(user, basic_topic_params.merge(auto_close_days: 2)).create
+ }.to raise_error(Discourse::InvalidAccess)
+ end
+ end
end
context 'uniqueness' do
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 3c824e7643c..a7409a1d797 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -516,4 +516,40 @@ describe TopicsController do
end
+ describe 'autoclose' do
+
+ it 'needs you to be logged in' do
+ lambda { xhr :put, :autoclose, topic_id: 99, auto_close_days: 3}.should raise_error(Discourse::NotLoggedIn)
+ end
+
+ it 'needs you to be an admin or mod' do
+ user = log_in
+ xhr :put, :autoclose, topic_id: 99, auto_close_days: 3
+ response.should 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" do
+ Topic.any_instance.expects(:auto_close_days=).with { |arg| arg.to_i == 3 }
+ xhr :put, :autoclose, topic_id: @topic.id, auto_close_days: 3
+ end
+
+ it "can remove a topic's auto close time" do
+ Topic.any_instance.expects(:auto_close_days=).with(nil)
+ xhr :put, :autoclose, topic_id: @topic.id, auto_close_days: nil
+ end
+
+ it "sets the topic closer to the current user" do
+ Topic.any_instance.expects(:auto_close_user=).with(@admin)
+ xhr :put, :autoclose, topic_id: @topic.id, auto_close_days: nil
+ end
+ end
+
+ end
+
end
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index 90028e25cc3..6cb35ef8cdf 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -584,10 +584,10 @@ describe Topic do
end
end
- context 'closed' do
+ shared_examples_for 'a status that closes a topic' do
context 'disable' do
before do
- @topic.update_status('closed', false, @user)
+ @topic.update_status(status, false, @user)
@topic.reload
end
@@ -602,7 +602,7 @@ describe Topic do
context 'enable' do
before do
@topic.update_attribute :closed, false
- @topic.update_status('closed', true, @user)
+ @topic.update_status(status, true, @user)
@topic.reload
end
@@ -614,6 +614,16 @@ describe Topic do
end
end
+ context 'closed' do
+ let(:status) { 'closed' }
+ it_should_behave_like 'a status that closes a topic'
+ end
+
+ context 'autoclosed' do
+ let(:status) { 'autoclosed' }
+ it_should_behave_like 'a status that closes a topic'
+ end
+
end
@@ -943,4 +953,114 @@ describe Topic do
end
end
+ describe 'auto-close' do
+ context 'a new topic' do
+ it 'when auto_close_at is not present, it does not queue a job to close the topic' do
+ Jobs.expects(:enqueue_at).never
+ Fabricate(:topic)
+ end
+
+ context 'auto_close_at is set' do
+ it 'queues a job to close the topic' do
+ Timecop.freeze(Time.zone.now) do
+ Jobs.expects(:enqueue_at).with(7.days.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
+ Fabricate(:topic, auto_close_at: 7.days.from_now, user: Fabricate(:admin))
+ end
+ end
+
+ 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
+ Fabricate(:topic, auto_close_at: 7.days.from_now, user: topic_creator)
+ 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
+ Fabricate(:topic, auto_close_at: 7.days.from_now, auto_close_user: topic_closer, user: topic_creator)
+ end
+ end
+ end
+
+ context 'an existing topic' do
+ it 'when auto_close_at is set, it queues a job to close the topic' do
+ Timecop.freeze(Time.zone.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
+ topic.save.should be_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(Time.zone.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
+ topic.save.should be_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
+ topic.save.should be_true
+ topic.auto_close_user.should be_nil
+ end
+
+ it 'when auto_close_user is removed, it updates the job' do
+ Timecop.freeze(Time.zone.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
+ topic.save.should be_true
+ end
+ end
+
+ it 'when auto_close_at value is changed, it reschedules the job' do
+ Timecop.freeze(Time.zone.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
+ topic.save.should be_true
+ end
+ end
+
+ it 'when auto_close_user_id is changed, it updates the job' do
+ Timecop.freeze(Time.zone.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
+ topic.save.should be_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(Time.zone.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'
+ topic.save.should be_true
+ end
+ end
+ end
+ end
+
end