mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
Auto-close time can be entered in 3 ways, so a topic can close at any time
This commit is contained in:
parent
67b6d37da0
commit
a9ab98ef9e
19 changed files with 193 additions and 80 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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(){
|
||||
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', moment().add('days', days).format());
|
||||
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'); } );
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ class PostsController < ApplicationController
|
|||
:category,
|
||||
:target_usernames,
|
||||
:reply_to_post_number,
|
||||
:auto_close_days,
|
||||
:auto_close_time,
|
||||
:auto_track
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue