diff --git a/app/assets/javascripts/discourse/components/auto_close_form_component.js b/app/assets/javascripts/discourse/components/auto_close_form_component.js new file mode 100644 index 000000000..30f46c30f --- /dev/null +++ b/app/assets/javascripts/discourse/components/auto_close_form_component.js @@ -0,0 +1,28 @@ +Discourse.AutoCloseFormComponent = Ember.Component.extend({ + + autoCloseValid: false, + + label: function() { + return I18n.t( this.get('labelKey') || 'composer.auto_close_label' ); + }.property('labelKey'), + + autoCloseChanged: function() { + if( this.get('autoCloseTime') && this.get('autoCloseTime').length > 0 ) { + this.set('autoCloseTime', this.get('autoCloseTime').replace(/[^\d:-\s]/g, '') ); + } + this.set('autoCloseValid', this.isAutoCloseValid()); + }.observes('autoCloseTime'), + + isAutoCloseValid: function() { + if (this.get('autoCloseTime')) { + var t = this.get('autoCloseTime').trim(); + if (t.match(/^[\d]{4}-[\d]{1,2}-[\d]{1,2} [\d]{1,2}:[\d]{2}/)) { + return moment(t).isAfter(); // In the future + } else { + return (t.match(/^[\d]+$/) || t.match(/^[\d]{1,2}:[\d]{2}$/)) !== null; + } + } else { + return true; + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js index ccb6dc36d..92391867f 100644 --- a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js +++ b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js @@ -9,20 +9,23 @@ **/ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, { - setDays: function() { + auto_close_valid: true, + auto_close_invalid: Em.computed.not('auto_close_valid'), + + setAutoCloseTime: function() { if( this.get('details.auto_close_at') ) { var closeTime = new Date( this.get('details.auto_close_at') ); if (closeTime > new Date()) { - this.set('auto_close_days', Math.round(moment(closeTime).diff(new Date(), 'days', true))); + this.set('auto_close_time', moment(closeTime).format("YYYY-MM-DD HH:mm")); } } else { - this.set('details.auto_close_days', ''); + this.set('details.auto_close_time', ''); } }.observes('details.auto_close_at'), actions: { saveAutoClose: function() { - this.setAutoClose( parseFloat(this.get('auto_close_days')) ); + this.setAutoClose( this.get('auto_close_time') ); }, removeAutoClose: function() { @@ -30,19 +33,23 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco } }, - setAutoClose: function(days) { + setAutoClose: function(time) { var self = this; this.send('hideModal'); Discourse.ajax({ url: '/t/' + this.get('id') + '/autoclose', type: 'PUT', - dataType: 'html', // no custom errors, jquery 1.9 enforces json - data: { auto_close_days: days > 0 ? days : null } - }).then(function(){ - self.send('closeModal'); - self.set('details.auto_close_at', moment().add('days', days).format()); + dataType: 'json', + data: { auto_close_time: Discourse.Utilities.timestampFromAutocloseString(time) } + }).then(function(result){ + if (result.success) { + self.send('closeModal'); + self.set('details.auto_close_at', result.auto_close_at); + } else { + bootbox.alert(I18n.t('composer.auto_close_error'), function() { self.send('showModal'); } ); + } }, function (error) { - bootbox.alert(I18n.t('generic_error'), function() { self.send('showModal'); } ); + bootbox.alert(I18n.t('composer.auto_close_error'), function() { self.send('showModal'); } ); }); } diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index 45a712d13..52cc93303 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -322,6 +322,26 @@ Discourse.Utilities = { image.src = url; }); } + }, + + timestampFromAutocloseString: function(arg) { + if (!arg) return null; + if (arg.match(/^[\d]{4}-[\d]{1,2}-[\d]{1,2} [\d]{1,2}:[\d]{2}/)) { + return moment(arg).toJSON(); // moment will add the timezone + } else { + var matches = arg.match(/^([\d]{1,2}):([\d]{2})$/); // just the time HH:MM + if (matches) { + var now = moment(), + t = moment(new Date(now.year(), now.month(), now.date(), matches[1], matches[2])); + if (t.isAfter()) { + return t.toJSON(); + } else { + return t.add('days', 1).toJSON(); + } + } else { + return arg; + } + } } }; diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js index da254910b..d199e3326 100644 --- a/app/assets/javascripts/discourse/models/composer.js +++ b/app/assets/javascripts/discourse/models/composer.js @@ -465,7 +465,7 @@ Discourse.Composer = Discourse.Model.extend({ moderator: currentUser.get('moderator'), yours: true, newPost: true, - auto_close_days: this.get('auto_close_days') + auto_close_time: Discourse.Utilities.timestampFromAutocloseString(this.get('auto_close_time')) }); // If we're in a topic, we can append the post instantly. diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js index fcaa533a6..877f48e33 100644 --- a/app/assets/javascripts/discourse/models/post.js +++ b/app/assets/javascripts/discourse/models/post.js @@ -180,7 +180,7 @@ Discourse.Post = Discourse.Model.extend({ title: this.get('title'), image_sizes: this.get('imageSizes'), target_usernames: this.get('target_usernames'), - auto_close_days: this.get('auto_close_days') + auto_close_time: Discourse.Utilities.timestampFromAutocloseString(this.get('auto_close_time')) }; var metaData = this.get('metaData'); diff --git a/app/assets/javascripts/discourse/templates/auto_close_form.js.handlebars b/app/assets/javascripts/discourse/templates/auto_close_form.js.handlebars deleted file mode 100644 index ce93f2e90..000000000 --- a/app/assets/javascripts/discourse/templates/auto_close_form.js.handlebars +++ /dev/null @@ -1,6 +0,0 @@ -
- - {{view.label}} - {{textField value=view.autoCloseDays maxlength="3"}} - {{i18n composer.auto_close_units}} -
diff --git a/app/assets/javascripts/discourse/templates/components/auto-close-form.js.handlebars b/app/assets/javascripts/discourse/templates/components/auto-close-form.js.handlebars new file mode 100644 index 000000000..ad20447f9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/auto-close-form.js.handlebars @@ -0,0 +1,11 @@ +
+
+ + {{label}} + {{textField value=autoCloseTime}} + {{i18n composer.auto_close_units}} +
+
+ {{i18n composer.auto_close_examples}} +
+
diff --git a/app/assets/javascripts/discourse/templates/composer.js.handlebars b/app/assets/javascripts/discourse/templates/composer.js.handlebars index 1da3a5ced..a49df2147 100644 --- a/app/assets/javascripts/discourse/templates/composer.js.handlebars +++ b/app/assets/javascripts/discourse/templates/composer.js.handlebars @@ -48,13 +48,13 @@ {{/if}} {{#if model.showAdminOptions}} - + {{/if}} {{/unless}}
- {{autoCloseForm autoCloseDays=model.auto_close_days}} + {{auto-close-form autoCloseTime=model.auto_close_time}}
{{/if}} diff --git a/app/assets/javascripts/discourse/templates/modal/auto_close.js.handlebars b/app/assets/javascripts/discourse/templates/modal/auto_close.js.handlebars index 98bc8338c..ce11d43a6 100644 --- a/app/assets/javascripts/discourse/templates/modal/auto_close.js.handlebars +++ b/app/assets/javascripts/discourse/templates/modal/auto_close.js.handlebars @@ -1,10 +1,10 @@ diff --git a/app/assets/javascripts/discourse/views/auto_close_form_view.js b/app/assets/javascripts/discourse/views/auto_close_form_view.js deleted file mode 100644 index 8bfc25f73..000000000 --- a/app/assets/javascripts/discourse/views/auto_close_form_view.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - This view renders the form to set or change a topic or category's auto-close setting. - - @class AutoCloseFormView - @extends Ember.View - @namespace Discourse - @module Discourse - **/ -Discourse.AutoCloseFormView = Ember.View.extend({ - templateName: 'auto_close_form', - - label: function() { - return I18n.t( this.get('labelKey') || 'composer.auto_close_label' ); - }.property('labelKey'), - - autoCloseChanged: function() { - if( this.get('autoCloseDays') && this.get('autoCloseDays').length > 0 ) { - this.set('autoCloseDays', this.get('autoCloseDays').replace(/[^\d]/g, '') ); - } - }.observes('autoCloseDays') -}); - -Discourse.View.registerHelper('autoCloseForm', Discourse.AutoCloseFormView); diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index d395f21c5..e4d7ae085 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -334,8 +334,12 @@ display: block; bottom: 8px; } + .auto-close-fields .examples { + margin-top: 0; + padding-bottom: 8px; + } } - .title-input, .category-input { + .title-input, .category-input, .show-admin-options { position: relative; display: inline; } @@ -523,7 +527,11 @@ div.ac-wrap { .auto-close-fields { input { - width: 50px; + width: 150px; + } + .examples { + margin: 12px 0 0 17px; + color: $dark_gray; } } @@ -543,7 +551,7 @@ div.ac-wrap { } #reply-control button.btn.no-text { - margin: 7px 0 0 5px; - position: absolute; + margin: 7px 0 0 5px; // works in safari, but not chrome and firefox + position: relative; } diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 070e022c5..2a0f35291 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -225,7 +225,7 @@ class PostsController < ApplicationController :category, :target_usernames, :reply_to_post_number, - :auto_close_days, + :auto_close_time, :auto_track ] diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index c2b2daff4..58c32a75d 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -153,12 +153,15 @@ class TopicsController < ApplicationController end def autoclose - raise Discourse::InvalidParameters.new(:auto_close_days) unless params.has_key?(:auto_close_days) - @topic = Topic.where(id: params[:topic_id].to_i).first - guardian.ensure_can_moderate!(@topic) - @topic.set_auto_close(params[:auto_close_days], current_user) - @topic.save - render nothing: true + raise Discourse::InvalidParameters.new(:auto_close_time) unless params.has_key?(:auto_close_time) + topic = Topic.where(id: params[:topic_id].to_i).first + guardian.ensure_can_moderate!(topic) + topic.set_auto_close(params[:auto_close_time], current_user) + if topic.save + render json: success_json.merge!(auto_close_at: topic.auto_close_at) + else + render_json_error(topic) + end end def destroy diff --git a/app/models/topic.rb b/app/models/topic.rb index 33eb6ad35..301c83400 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -146,7 +146,7 @@ class Topic < ActiveRecord::Base self.bumped_at ||= Time.now self.last_post_user_id ||= user_id if !@ignore_category_auto_close and self.category and self.category.auto_close_days and self.auto_close_at.nil? - set_auto_close(self.category.auto_close_days) + set_auto_close(self.category.auto_close_days * 24) end end @@ -602,9 +602,10 @@ class Topic < ActiveRecord::Base end end + # TODO: change this method, along with category's auto_close_days. Use hours. def auto_close_days=(num_days) @ignore_category_auto_close = true - set_auto_close(num_days) + set_auto_close(num_days * 24) end def self.auto_close @@ -622,10 +623,27 @@ class Topic < ActiveRecord::Base end end - def set_auto_close(num_days, by_user=nil) - num_days = num_days.to_i - self.auto_close_at = (num_days > 0 ? num_days.days.from_now : nil) - if num_days > 0 + # Valid arguments for the auto close time: + # * An integer, which is the number of hours from now to close the topic. + # * A time, like "12:00", which is the time at which the topic will close in the current day + # or the next day if that time has already passed today. + # * A timestamp, like "2013-11-25 13:00", when the topic should close. + # * A timestamp with timezone in JSON format. (e.g., "2013-11-26T21:00:00.000Z") + # * nil, to prevent the topic from automatically closing. + def set_auto_close(arg, by_user=nil) + if arg.is_a?(String) and matches = /^([\d]{1,2}):([\d]{1,2})$/.match(arg.strip) + now = Time.zone.now + self.auto_close_at = Time.zone.local(now.year, now.month, now.day, matches[1].to_i, matches[2].to_i) + self.auto_close_at += 1.day if self.auto_close_at < now + elsif arg.is_a?(String) and arg.include?('-') and timestamp = Time.zone.parse(arg) + self.auto_close_at = timestamp + self.errors.add(:auto_close_at, :invalid) if timestamp < Time.zone.now + else + num_hours = arg.to_i + self.auto_close_at = (num_hours > 0 ? num_hours.hours.from_now : nil) + end + + unless self.auto_close_at.nil? self.auto_close_started_at ||= Time.zone.now if by_user and by_user.staff? self.auto_close_user = by_user diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8225d4c92..c2e693b70 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -517,8 +517,10 @@ en: toggler: "hide or show the composer panel" admin_options_title: "Optional staff settings for this topic" - auto_close_label: "Auto-close topic after:" - auto_close_units: "days" + auto_close_label: "Auto-close topic time:" + auto_close_units: "(# of hours, a time, or a timestamp)" + auto_close_examples: 'Examples: 24, 17:00, 2013-11-22 14:00' + auto_close_error: "Please enter a valid value." notifications: title: "notifications of @name mentions, replies to your posts and topics, private messages, etc" diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb index e60051e1a..0965683b5 100644 --- a/lib/topic_creator.rb +++ b/lib/topic_creator.rb @@ -16,7 +16,7 @@ class TopicCreator topic_params = setup @topic = Topic.new(topic_params) - setup_auto_close_days if @opts[:auto_close_days] + setup_auto_close_time if @opts[:auto_close_time] process_private_message if @opts[:archetype] == Archetype.private_message save_topic @@ -55,9 +55,9 @@ class TopicCreator topic_params end - def setup_auto_close_days + def setup_auto_close_time @guardian.ensure_can_moderate!(@topic) - @topic.auto_close_days = @opts[:auto_close_days] + @topic.set_auto_close(@opts[:auto_close_time], @user) end def process_private_message diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index fed5a5cff..9ecbdf4f0 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -190,7 +190,7 @@ describe PostCreator 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 + PostCreator.new(user, basic_topic_params.merge(auto_close_time: 2)).create }.to raise_error(Discourse::InvalidAccess) end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 9c54976c9..e8500332f 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -739,12 +739,12 @@ describe TopicsController do 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) + lambda { xhr :put, :autoclose, topic_id: 99, auto_close_time: '24'}.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 + xhr :put, :autoclose, topic_id: 99, auto_close_time: '24' response.should be_forbidden end @@ -755,13 +755,15 @@ describe TopicsController do end it "can set a topic's auto close time" do - Topic.any_instance.expects(:set_auto_close).with("3", @admin) - xhr :put, :autoclose, topic_id: @topic.id, auto_close_days: 3 + Topic.any_instance.expects(:set_auto_close).with("24", @admin) + xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: '24' + json = ::JSON.parse(response.body) + json.should have_key('auto_close_at') 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_days: nil + xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: nil end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 394d68add..e4055793d 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1074,13 +1074,56 @@ describe Topic do before { Discourse.stubs(:system_user).returns(admin) } - it 'sets auto_close_at' do + it 'can take a number of hours as an integer' do Timecop.freeze(Time.zone.now) do - topic.set_auto_close(3, admin) + topic.set_auto_close(72, admin) expect(topic.auto_close_at).to eq(3.days.from_now) end end + it 'can take a number of hours as a string' do + Timecop.freeze(Time.zone.now) do + topic.set_auto_close('18', admin) + expect(topic.auto_close_at).to eq(18.hours.from_now) + end + end + + it "can take a time later in the day" do + Timecop.freeze(Time.zone.local(2013,11,20,8,0)) do + topic.set_auto_close('13:00', admin) + topic.auto_close_at.should == Time.zone.local(2013,11,20,13,0) + end + end + + it "can take a time for the next day" do + Timecop.freeze(Time.zone.local(2013,11,20,8,0)) do + topic.set_auto_close('5:00', admin) + topic.auto_close_at.should == Time.zone.local(2013,11,21,5,0) + end + end + + it "can take a timestamp for a future time" do + Timecop.freeze(Time.zone.local(2013,11,20,8,0)) do + topic.set_auto_close('2013-11-22 5:00', admin) + topic.auto_close_at.should == Time.zone.local(2013,11,22,5,0) + end + end + + it "sets a validation error when given a timestamp in the past" do + Timecop.freeze(Time.zone.local(2013,11,20,8,0)) do + topic.set_auto_close('2013-11-19 5:00', admin) + topic.auto_close_at.should == Time.zone.local(2013,11,19,5,0) + topic.errors[:auto_close_at].should be_present + end + end + + it "can take a timestamp with timezone" do + Timecop.freeze(Time.utc(2013,11,20,12,0)) do + topic.set_auto_close('2013-11-25T01:35:00-08:00', admin) + topic.auto_close_at.should == Time.utc(2013,11,25,9,35) + end + end + it 'sets auto_close_user to given user if it is a staff user' do topic.set_auto_close(3, admin) expect(topic.auto_close_user_id).to eq(admin.id) @@ -1102,20 +1145,20 @@ describe Topic do expect(staff_topic.auto_close_user_id).to eq(999) end - it 'clears auto_close_at if num_days is nil' do + 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 end - it 'clears auto_close_started_at if num_days is nil' do + 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 Timecop.freeze(Time.zone.now) do - closing_topic.set_auto_close(14) - expect(closing_topic.auto_close_at).to eq(14.days.from_now) + closing_topic.set_auto_close(48) + expect(closing_topic.auto_close_at).to eq(2.days.from_now) end end