Auto-close time can be entered in 3 ways, so a topic can close at any time

This commit is contained in:
Neil Lalonde 2013-11-26 19:06:20 -05:00
parent 67b6d37da0
commit a9ab98ef9e
19 changed files with 193 additions and 80 deletions

View file

@ -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;
}
}
});

View file

@ -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'); } );
});
}

View file

@ -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;
}
}
}
};

View file

@ -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.

View file

@ -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');

View file

@ -1,6 +0,0 @@
<div class="auto-close-fields">
<i class="icon icon-time"></i>
{{view.label}}
{{textField value=view.autoCloseDays maxlength="3"}}
{{i18n composer.auto_close_units}}
</div>

View file

@ -0,0 +1,11 @@
<div class="auto-close-fields">
<div>
<i class="icon icon-time"></i>
{{label}}
{{textField value=autoCloseTime}}
{{i18n composer.auto_close_units}}
</div>
<div class="examples">
{{i18n composer.auto_close_examples}}
</div>
</div>

View file

@ -48,13 +48,13 @@
<button class='btn' {{action showOptions}}>{{i18n topic.options}}</button>
{{/if}}
{{#if model.showAdminOptions}}
<button {{action toggleAdminOptions target="view"}} class="btn no-text" title='{{i18n composer.admin_options_title}}'><i class="icon icon-wrench"></i></button>
<button {{action toggleAdminOptions target="view"}} class="btn no-text show-admin-options" title='{{i18n composer.admin_options_title}}'><i class="icon icon-wrench"></i></button>
{{/if}}
{{/unless}}
</div>
<div class="admin-options-form">
{{autoCloseForm autoCloseDays=model.auto_close_days}}
{{auto-close-form autoCloseTime=model.auto_close_time}}
</div>
{{/if}}

View file

@ -1,10 +1,10 @@
<div class="modal-body">
<form>
{{autoCloseForm autoCloseDays=auto_close_days}}
{{auto-close-form autoCloseTime=auto_close_time autoCloseValid=auto_close_valid}}
</form>
</div>
<div class="modal-footer">
<button class='btn btn-primary' {{action saveAutoClose}}>{{i18n topic.auto_close_save}}</button>
<button class='btn btn-primary' {{action saveAutoClose}} {{bindAttr disabled="auto_close_invalid"}}>{{i18n topic.auto_close_save}}</button>
<a {{action closeModal}}>{{i18n cancel}}</a>
<button class='btn pull-right' {{action removeAutoClose}}>{{i18n topic.auto_close_remove}}</button>
</div>

View file

@ -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);

View file

@ -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;
}

View file

@ -225,7 +225,7 @@ class PostsController < ApplicationController
:category,
:target_usernames,
:reply_to_post_number,
:auto_close_days,
:auto_close_time,
:auto_track
]

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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