diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 index 0c8795dd1df..9cd0e00e75a 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 @@ -5,6 +5,8 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; export default Ember.Controller.extend(ModalFunctionality, { auto_close_valid: true, auto_close_invalid: Em.computed.not('auto_close_valid'), + disable_submit: Em.computed.or('auto_close_invalid', 'loading'), + loading: false, @observes("model.details.auto_close_at", "model.details.auto_close_hours") setAutoCloseTime() { @@ -29,7 +31,7 @@ export default Ember.Controller.extend(ModalFunctionality, { setAutoClose(time) { const self = this; - this.send('hideModal'); + this.set('loading', true); Discourse.ajax({ url: `/t/${this.get('model.id')}/autoclose`, type: 'PUT', @@ -40,16 +42,34 @@ export default Ember.Controller.extend(ModalFunctionality, { timezone_offset: (new Date().getTimezoneOffset()) } }).then(result => { + self.set('loading', false); if (result.success) { this.send('closeModal'); this.set('model.details.auto_close_at', result.auto_close_at); this.set('model.details.auto_close_hours', result.auto_close_hours); } else { - bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); + bootbox.alert(I18n.t('composer.auto_close.error')); } }).catch(() => { - bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); + // TODO - incorrectly responds to network errors as bad input + bootbox.alert(I18n.t('composer.auto_close.error')); + self.set('loading', false); }); - } + }, + + willCloseImmediately: function() { + if (!this.get('model.details.auto_close_based_on_last_post')) { + return false; + } + let closeDate = new Date(this.get('model.last_posted_at')); + closeDate.setHours(closeDate.getHours() + this.get('model.auto_close_time')); + return closeDate < new Date(); + }.property('model.details.auto_close_based_on_last_post', 'model.auto_close_time', 'model.last_posted_at'), + + willCloseI18n: function() { + if (this.get('model.details.auto_close_based_on_last_post')) { + return I18n.t('topic.auto_close_immediate', {hours: this.get('model.auto_close_time')}); + } + }.property('model.details.auto_close_based_on_last_post', 'model.auto_close_time') }); diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index cd83dd9fb64..6f58ae24d8a 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -340,6 +340,13 @@ const Topic = RestModel.extend({ keys.forEach(key => this.set(key, json[key])); }, + reload() { + const self = this; + return Discourse.ajax('/t/' + this.get('id'), { type: 'GET' }).then(function(topic_json) { + self.updateFromJson(topic_json); + }); + }, + isPinnedUncategorized: function() { return this.get('pinned') && this.get('category.isUncategorizedCategory'); }.property('pinned', 'category.isUncategorizedCategory'), diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs index fcf524920c1..cc1f423cc0e 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs @@ -4,10 +4,17 @@ autoCloseValid=auto_close_valid autoCloseBasedOnLastPost=model.details.auto_close_based_on_last_post limited=model.details.auto_close_based_on_last_post }} + {{#if willCloseImmediately}} +
+ {{fa-icon "warning"}} + {{willCloseI18n}} +
+ {{/if}} diff --git a/app/models/topic.rb b/app/models/topic.rb index 03f415c9c1a..9e4bd7d4c6e 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -835,10 +835,12 @@ class Topic < ActiveRecord::Base self.auto_close_at = utc.local(now.year, now.month, now.day, m[1].to_i, m[2].to_i) self.auto_close_at += offset_minutes * 60 if offset_minutes self.auto_close_at += 1.day if self.auto_close_at < now + self.auto_close_hours = -1 elsif arg.is_a?(String) && arg.include?("-") && timestamp = utc.parse(arg) # a timestamp in client's time zone, like "2015-5-27 12:00" self.auto_close_at = timestamp self.auto_close_at += offset_minutes * 60 if offset_minutes + self.auto_close_hours = -1 self.errors.add(:auto_close_at, :invalid) if timestamp < Time.zone.now else num_hours = arg.to_f @@ -864,6 +866,10 @@ class Topic < ActiveRecord::Base else self.auto_close_user ||= (self.user.staff? || self.user.trust_level == TrustLevel[4] ? self.user : Discourse.system_user) end + + if self.auto_close_at.try(:<, Time.zone.now) + auto_close(auto_close_user) + end end self diff --git a/app/models/topic_status_update.rb b/app/models/topic_status_update.rb index 64dcc256e80..d3befc9f0b7 100644 --- a/app/models/topic_status_update.rb +++ b/app/models/topic_status_update.rb @@ -52,7 +52,16 @@ TopicStatusUpdate = Struct.new(:topic, :user) do end def message_for_autoclosed(locale_key) - num_minutes = topic.auto_close_started_at ? ((Time.zone.now - topic.auto_close_started_at) / 1.minute).round : topic.age_in_minutes + num_minutes = (( + if topic.auto_close_based_on_last_post + topic.auto_close_hours.hours + elsif topic.auto_close_started_at + Time.zone.now - topic.auto_close_started_at + else + Time.zone.now - topic.created_at + end + ) / 1.minute).round + if num_minutes.minutes >= 2.days I18n.t("#{locale_key}_days", count: (num_minutes.minutes / 1.day).round) else diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4e8946d1bb6..eb01802f6a1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1142,6 +1142,7 @@ en: auto_close_title: 'Auto-Close Settings' auto_close_save: "Save" auto_close_remove: "Don't Auto-Close This Topic" + auto_close_immediate: "The last post in the topic is already %{hours} hours old, so the topic will be closed immediately." progress: title: topic progress diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 08da959738e..c0d18be282b 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -1036,6 +1036,46 @@ describe TopicsController 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 diff --git a/spec/models/topic_status_update_spec.rb b/spec/models/topic_status_update_spec.rb index dc69ebfbca7..549adb9e872 100644 --- a/spec/models/topic_status_update_spec.rb +++ b/spec/models/topic_status_update_spec.rb @@ -3,18 +3,21 @@ require 'rails_helper' require_dependency 'post_destroyer' +# TODO - test pinning, create_moderator_post + describe TopicStatusUpdate do let(:user) { Fabricate(:user) } let(:admin) { Fabricate(:admin) } it "avoids notifying on automatically closed topics" do - # TODO: TopicStatusUpdate should supress message bus updates from the users it "pretends to read" + # 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 TopicStatusUpdate.new(post.topic, admin).update!("autoclosed", true) @@ -27,6 +30,7 @@ describe TopicStatusUpdate do it "adds an autoclosed message" do topic = create_topic + topic.set_auto_close('10') TopicStatusUpdate.new(topic, admin).update!("autoclosed", true) @@ -39,13 +43,14 @@ describe TopicStatusUpdate do 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') TopicStatusUpdate.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_minutes", count: 0)) + expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_lastpost_hours", count: 10)) end end