mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -05:00
Topic Auto-Close: admins and mods can set a topic to automatically close after a number of days
This commit is contained in:
parent
87469fa4b7
commit
9828c87525
33 changed files with 600 additions and 55 deletions
1
Gemfile
1
Gemfile
|
@ -107,6 +107,7 @@ group :test, :development do
|
||||||
gem 'shoulda', require: false
|
gem 'shoulda', require: false
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
gem 'terminal-notifier-guard', require: false
|
gem 'terminal-notifier-guard', require: false
|
||||||
|
gem 'timecop'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
|
|
@ -432,6 +432,7 @@ GEM
|
||||||
rack (>= 1.0.0)
|
rack (>= 1.0.0)
|
||||||
thor (0.18.1)
|
thor (0.18.1)
|
||||||
tilt (1.3.7)
|
tilt (1.3.7)
|
||||||
|
timecop (0.6.1)
|
||||||
timers (1.1.0)
|
timers (1.1.0)
|
||||||
treetop (1.4.12)
|
treetop (1.4.12)
|
||||||
polyglot
|
polyglot
|
||||||
|
@ -529,6 +530,7 @@ DEPENDENCIES
|
||||||
terminal-notifier-guard
|
terminal-notifier-guard
|
||||||
therubyracer
|
therubyracer
|
||||||
thin
|
thin
|
||||||
|
timecop
|
||||||
turbo-sprockets-rails3
|
turbo-sprockets-rails3
|
||||||
uglifier
|
uglifier
|
||||||
vestal_versions!
|
vestal_versions!
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
**/
|
**/
|
||||||
Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({
|
Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({
|
||||||
visible: false,
|
visible: false,
|
||||||
|
needs: ['modal'],
|
||||||
|
|
||||||
show: function() {
|
show: function() {
|
||||||
this.set('visible', true);
|
this.set('visible', true);
|
||||||
|
@ -15,6 +16,15 @@ Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({
|
||||||
|
|
||||||
hide: function() {
|
hide: function() {
|
||||||
this.set('visible', false);
|
this.set('visible', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
autoClose: function() {
|
||||||
|
var modalController = this.get('controllers.modal');
|
||||||
|
if (modalController) {
|
||||||
|
var v = Discourse.EditTopicAutoCloseView.create();
|
||||||
|
v.set('topic', this.get('content'));
|
||||||
|
modalController.show(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,6 +65,11 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
return false;
|
return false;
|
||||||
}.property('editingPost', 'creatingTopic', 'post.post_number'),
|
}.property('editingPost', 'creatingTopic', 'post.post_number'),
|
||||||
|
|
||||||
|
showAdminOptions: function() {
|
||||||
|
if (this.get('creatingTopic') && Discourse.get('currentUser.staff')) return true;
|
||||||
|
return false;
|
||||||
|
}.property('editTitle'),
|
||||||
|
|
||||||
togglePreview: function() {
|
togglePreview: function() {
|
||||||
this.toggleProperty('showPreview');
|
this.toggleProperty('showPreview');
|
||||||
Discourse.KeyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
Discourse.KeyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||||
|
@ -354,7 +359,8 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
actions_summary: Em.A(),
|
actions_summary: Em.A(),
|
||||||
moderator: currentUser.get('moderator'),
|
moderator: currentUser.get('moderator'),
|
||||||
yours: true,
|
yours: true,
|
||||||
newPost: true
|
newPost: true,
|
||||||
|
auto_close_days: this.get('auto_close_days')
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we're in a topic, we can append the post instantly.
|
// If we're in a topic, we can append the post instantly.
|
||||||
|
@ -532,7 +538,13 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
var reply = this.get('reply') || "";
|
var reply = this.get('reply') || "";
|
||||||
while (Discourse.BBCode.QUOTE_REGEXP.test(reply)) { reply = reply.replace(Discourse.BBCode.QUOTE_REGEXP, ""); }
|
while (Discourse.BBCode.QUOTE_REGEXP.test(reply)) { reply = reply.replace(Discourse.BBCode.QUOTE_REGEXP, ""); }
|
||||||
return reply.replace(/\s+/img, " ").trim().length;
|
return reply.replace(/\s+/img, " ").trim().length;
|
||||||
}.property('reply')
|
}.property('reply'),
|
||||||
|
|
||||||
|
autoCloseChanged: function() {
|
||||||
|
if( this.get('auto_close_days') && this.get('auto_close_days').length > 0 ) {
|
||||||
|
this.set('auto_close_days', this.get('auto_close_days').replace(/[^\d]/g, '') )
|
||||||
|
}
|
||||||
|
}.observes('auto_close_days')
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,8 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
archetype: this.get('archetype'),
|
archetype: this.get('archetype'),
|
||||||
title: this.get('title'),
|
title: this.get('title'),
|
||||||
image_sizes: this.get('imageSizes'),
|
image_sizes: this.get('imageSizes'),
|
||||||
target_usernames: this.get('target_usernames')
|
target_usernames: this.get('target_usernames'),
|
||||||
|
auto_close_days: this.get('auto_close_days')
|
||||||
};
|
};
|
||||||
|
|
||||||
// Put the metaData into the request
|
// Put the metaData into the request
|
||||||
|
|
|
@ -36,8 +36,20 @@
|
||||||
{{#if content.archetype.hasOptions}}
|
{{#if content.archetype.hasOptions}}
|
||||||
<button class='btn' {{action showOptions target="controller"}}>{{i18n topic.options}}</button>
|
<button class='btn' {{action showOptions target="controller"}}>{{i18n topic.options}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if content.showAdminOptions}}
|
||||||
|
<button class="btn no-text" {{action toggleAdminOptions target="view"}}><i class="icon icon-wrench"></i></button>
|
||||||
|
{{/if}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-options-form">
|
||||||
|
<div class="auto-close-fields">
|
||||||
|
<i class="icon icon-time"></i>
|
||||||
|
{{i18n composer.auto_close_label}}
|
||||||
|
{{view Discourse.TextField valueBinding="content.auto_close_days" maxlength="5"}}
|
||||||
|
{{i18n composer.auto_close_units}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class='wmd-controls'>
|
<div class='wmd-controls'>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
<div class="auto-close-fields">
|
||||||
|
<i class="icon icon-time"></i>
|
||||||
|
{{i18n composer.auto_close_label}}
|
||||||
|
{{view Discourse.TextField valueBinding="view.auto_close_days" maxlength="5"}}
|
||||||
|
{{i18n composer.auto_close_units}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class='btn btn-primary' {{action saveAutoClose target="view"}} data-dismiss="modal">{{i18n topic.auto_close_save}}</button>
|
||||||
|
<button class='btn' data-dismiss="modal">{{i18n topic.auto_close_cancel}}</button>
|
||||||
|
<button class='btn pull-right' {{action removeAutoClose target="view"}} data-dismiss="modal">{{i18n topic.auto_close_remove}}</button>
|
||||||
|
</div>
|
|
@ -72,6 +72,9 @@
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if view.fullyLoaded}}
|
{{#if view.fullyLoaded}}
|
||||||
|
|
||||||
|
{{view Discourse.TopicClosingView topicBinding="controller.content"}}
|
||||||
|
|
||||||
{{view Discourse.TopicFooterButtonsView topicBinding="controller.content"}}
|
{{view Discourse.TopicFooterButtonsView topicBinding="controller.content"}}
|
||||||
|
|
||||||
{{#if controller.content.suggested_topics.length}}
|
{{#if controller.content.suggested_topics.length}}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<button {{action toggleClosed}} class='btn btn-admin'><i class='icon-unlock'></i> {{i18n topic.actions.open}}</button>
|
<button {{action toggleClosed}} class='btn btn-admin'><i class='icon-unlock'></i> {{i18n topic.actions.open}}</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button {{action toggleClosed}} class='btn btn-admin'><i class='icon-lock'></i> {{i18n topic.actions.close}}</button>
|
<button {{action toggleClosed}} class='btn btn-admin'><i class='icon-lock'></i> {{i18n topic.actions.close}}</button>
|
||||||
|
<button {{action autoClose}} class='btn btn-admin'><i class='icon-time'></i> {{i18n topic.actions.auto_close}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -57,5 +58,5 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button class='btn' id='show-topic-admin' {{action show}}><i class='icon icon-wrench'></i></button>
|
<button class='btn no-text' id='show-topic-admin' {{action show}}><i class='icon icon-wrench'></i></button>
|
||||||
{{/if}}
|
{{/if}}
|
|
@ -354,6 +354,19 @@ Discourse.ComposerView = Discourse.View.extend({
|
||||||
|
|
||||||
childDidInsertElement: function(e) {
|
childDidInsertElement: function(e) {
|
||||||
return this.initEditor();
|
return this.initEditor();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAdminOptions: function() {
|
||||||
|
var $adminOpts = $('.admin-options-form'),
|
||||||
|
$wmd = $('.wmd-controls'),
|
||||||
|
wmdTop = parseInt($wmd.css('top'),10);
|
||||||
|
if( $adminOpts.is(':visible') ) {
|
||||||
|
$wmd.css('top', wmdTop - parseInt($adminOpts.css('height'),10) + 'px' );
|
||||||
|
$adminOpts.hide();
|
||||||
|
} else {
|
||||||
|
$adminOpts.show();
|
||||||
|
$wmd.css('top', wmdTop + parseInt($adminOpts.css('height'),10) + 'px' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
This view handles a modal to set, edit, and remove a topic's auto-close time.
|
||||||
|
|
||||||
|
@class EditTopicAutoCloseView
|
||||||
|
@extends Discourse.ModalBodyView
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.EditTopicAutoCloseView = Discourse.ModalBodyView.extend({
|
||||||
|
templateName: 'modal/auto_close',
|
||||||
|
title: Em.String.i18n('topic.auto_close_title'),
|
||||||
|
modalClass: 'edit-auto-close-modal',
|
||||||
|
|
||||||
|
setDays: function() {
|
||||||
|
if( this.get('topic.auto_close_at') ) {
|
||||||
|
var closeTime = Date.create( this.get('topic.auto_close_at') );
|
||||||
|
if (closeTime.isFuture()) {
|
||||||
|
this.set('auto_close_days', closeTime.daysSince());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.observes('topic'),
|
||||||
|
|
||||||
|
saveAutoClose: function() {
|
||||||
|
this.setAutoClose( parseFloat(this.get('auto_close_days')) );
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAutoClose: function() {
|
||||||
|
this.setAutoClose(null);
|
||||||
|
},
|
||||||
|
|
||||||
|
setAutoClose: function(days) {
|
||||||
|
Discourse.ajax({
|
||||||
|
url: "/t/" + this.get('topic.id') + "/autoclose",
|
||||||
|
type: 'PUT',
|
||||||
|
dataType: 'json',
|
||||||
|
data: { auto_close_days: days > 0 ? days : null }
|
||||||
|
}).then(function(){
|
||||||
|
window.location.reload();
|
||||||
|
}, function (error) {
|
||||||
|
bootbox.alert(Em.String.i18n('generic_error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
47
app/assets/javascripts/discourse/views/topic_closing_view.js
Normal file
47
app/assets/javascripts/discourse/views/topic_closing_view.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
This view is used for rendering the notification that a topic will
|
||||||
|
automatically close.
|
||||||
|
|
||||||
|
@class TopicClosingView
|
||||||
|
@extends Ember.ContainerView
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.TopicClosingView = Discourse.View.extend({
|
||||||
|
elementId: 'topic-closing-info',
|
||||||
|
templateName: 'topic_closing',
|
||||||
|
|
||||||
|
render: function(buffer) {
|
||||||
|
if (!this.present('topic.auto_close_at')) return;
|
||||||
|
|
||||||
|
var autoCloseAt = Date.create(this.get('topic.auto_close_at'));
|
||||||
|
|
||||||
|
if (autoCloseAt.isPast()) return;
|
||||||
|
|
||||||
|
var timeLeftString, reRenderDelay, minutesLeft = autoCloseAt.minutesSince();
|
||||||
|
|
||||||
|
if (minutesLeft > 1440) {
|
||||||
|
timeLeftString = Em.String.i18n('in_n_days', {count: autoCloseAt.daysSince()});
|
||||||
|
if( minutesLeft > 2160 ) {
|
||||||
|
reRenderDelay = 12 * 60 * 60000;
|
||||||
|
} else {
|
||||||
|
reRenderDelay = 60 * 60000;
|
||||||
|
}
|
||||||
|
} else if (minutesLeft > 90) {
|
||||||
|
timeLeftString = Em.String.i18n('in_n_hours', {count: autoCloseAt.hoursSince()});
|
||||||
|
reRenderDelay = 30 * 60000;
|
||||||
|
} else if (minutesLeft > 2) {
|
||||||
|
timeLeftString = Em.String.i18n('in_n_minutes', {count: autoCloseAt.minutesSince()});
|
||||||
|
reRenderDelay = 60000;
|
||||||
|
} else {
|
||||||
|
timeLeftString = Em.String.i18n('in_n_seconds', {count: autoCloseAt.secondsSince()});
|
||||||
|
reRenderDelay = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push('<h3><i class="icon icon-time"></i> ');
|
||||||
|
buffer.push( Em.String.i18n('topic.auto_close_notice', {timeLeft: timeLeftString}) );
|
||||||
|
buffer.push('</h3>');
|
||||||
|
|
||||||
|
this.rerender.bind(this).delay(reRenderDelay);
|
||||||
|
}
|
||||||
|
});
|
|
@ -2,7 +2,7 @@
|
||||||
This view is used for rendering the buttons at the footer of the topic
|
This view is used for rendering the buttons at the footer of the topic
|
||||||
|
|
||||||
@class TopicFooterButtonsView
|
@class TopicFooterButtonsView
|
||||||
@extends Discourse.View
|
@extends Ember.ContainerView
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
|
|
|
@ -234,6 +234,7 @@
|
||||||
margin: 6px 10px 3px 0;
|
margin: 6px 10px 3px 0;
|
||||||
}
|
}
|
||||||
.wmd-controls {
|
.wmd-controls {
|
||||||
|
@include transition(top 0.3s ease);
|
||||||
top: 100px;
|
top: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,6 +366,7 @@ div.ac-wrap {
|
||||||
|
|
||||||
#reply-control.edit-title.private-message {
|
#reply-control.edit-title.private-message {
|
||||||
.wmd-controls {
|
.wmd-controls {
|
||||||
|
@include transition(top 0.3s ease);
|
||||||
top: 140px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,6 @@
|
||||||
top: 70px;
|
top: 70px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
i {
|
|
||||||
margin: 0px;
|
|
||||||
line-height: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-admin-menu {
|
.topic-admin-menu {
|
||||||
|
|
|
@ -329,6 +329,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#topic-closing-info {
|
||||||
|
margin-left: 103px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
|
|
|
@ -29,6 +29,12 @@
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
}
|
}
|
||||||
|
&.no-text {
|
||||||
|
.icon {
|
||||||
|
margin-right: 0;
|
||||||
|
line-height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default button
|
// Default button
|
||||||
|
|
|
@ -36,7 +36,8 @@ class PostsController < ApplicationController
|
||||||
target_usernames: params[:target_usernames],
|
target_usernames: params[:target_usernames],
|
||||||
reply_to_post_number: params[:post][:reply_to_post_number],
|
reply_to_post_number: params[:post][:reply_to_post_number],
|
||||||
image_sizes: params[:image_sizes],
|
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
|
post = post_creator.create
|
||||||
|
|
||||||
if post_creator.errors.present?
|
if post_creator.errors.present?
|
||||||
|
|
|
@ -15,7 +15,8 @@ class TopicsController < ApplicationController
|
||||||
:unmute,
|
:unmute,
|
||||||
:set_notifications,
|
:set_notifications,
|
||||||
:move_posts,
|
:move_posts,
|
||||||
:clear_pin]
|
:clear_pin,
|
||||||
|
:autoclose]
|
||||||
|
|
||||||
before_filter :consider_user_for_promotion, only: :show
|
before_filter :consider_user_for_promotion, only: :show
|
||||||
|
|
||||||
|
@ -97,6 +98,16 @@ class TopicsController < ApplicationController
|
||||||
toggle_mute(false)
|
toggle_mute(false)
|
||||||
end
|
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
|
def destroy
|
||||||
topic = Topic.where(id: params[:id]).first
|
topic = Topic.where(id: params[:id]).first
|
||||||
guardian.ensure_can_delete!(topic)
|
guardian.ensure_can_delete!(topic)
|
||||||
|
|
|
@ -61,6 +61,7 @@ class Topic < ActiveRecord::Base
|
||||||
belongs_to :featured_user2, class_name: 'User', foreign_key: :featured_user2_id
|
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_user3, class_name: 'User', foreign_key: :featured_user3_id
|
||||||
belongs_to :featured_user4, class_name: 'User', foreign_key: :featured_user4_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_users
|
||||||
has_many :topic_links
|
has_many :topic_links
|
||||||
|
@ -108,6 +109,18 @@ class Topic < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
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
|
# all users (in groups or directly targetted) that are going to get the pm
|
||||||
def all_allowed_users
|
def all_allowed_users
|
||||||
# TODO we should probably change this from 3 queries to 1
|
# TODO we should probably change this from 3 queries to 1
|
||||||
|
@ -264,7 +277,7 @@ class Topic < ActiveRecord::Base
|
||||||
update_pinned(status)
|
update_pinned(status)
|
||||||
else
|
else
|
||||||
# otherwise update the column
|
# otherwise update the column
|
||||||
update_column(property, status)
|
update_column(property == 'autoclosed' ? 'closed' : property, status)
|
||||||
end
|
end
|
||||||
|
|
||||||
key = "topic_statuses.#{property}_"
|
key = "topic_statuses.#{property}_"
|
||||||
|
@ -273,9 +286,11 @@ class Topic < ActiveRecord::Base
|
||||||
opts = {}
|
opts = {}
|
||||||
|
|
||||||
# We don't bump moderator posts except for the re-open post.
|
# 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -712,4 +727,9 @@ class Topic < ActiveRecord::Base
|
||||||
def notify_muted!(user)
|
def notify_muted!(user)
|
||||||
TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:muted])
|
TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:muted])
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -18,7 +18,8 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
:moderator_posts_count,
|
:moderator_posts_count,
|
||||||
:has_best_of,
|
:has_best_of,
|
||||||
:archetype,
|
:archetype,
|
||||||
:slug]
|
:slug,
|
||||||
|
:auto_close_at]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.guardian_attributes
|
def self.guardian_attributes
|
||||||
|
|
|
@ -32,6 +32,19 @@ en:
|
||||||
now: "just now"
|
now: "just now"
|
||||||
read_more: 'read more'
|
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:
|
suggested_topics:
|
||||||
title: "Suggested Topics"
|
title: "Suggested Topics"
|
||||||
|
|
||||||
|
@ -370,6 +383,9 @@ en:
|
||||||
help: "Markdown Editing Help"
|
help: "Markdown Editing Help"
|
||||||
toggler: "hide or show the composer panel"
|
toggler: "hide or show the composer panel"
|
||||||
|
|
||||||
|
auto_close_label: "Auto-close topic after:"
|
||||||
|
auto_close_units: "days"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
title: "notifications of @name mentions, replies to your posts and topics, private messages, etc"
|
title: "notifications of @name mentions, replies to your posts and topics, private messages, etc"
|
||||||
none: "You have no notifications right now."
|
none: "You have no notifications right now."
|
||||||
|
@ -479,6 +495,12 @@ en:
|
||||||
jump_reply_down: jump to later reply
|
jump_reply_down: jump to later reply
|
||||||
deleted: "The topic has been deleted"
|
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:
|
progress:
|
||||||
title: topic progress
|
title: topic progress
|
||||||
jump_top: jump to first post
|
jump_top: jump to first post
|
||||||
|
@ -516,6 +538,7 @@ en:
|
||||||
delete: "Delete Topic"
|
delete: "Delete Topic"
|
||||||
open: "Open Topic"
|
open: "Open Topic"
|
||||||
close: "Close Topic"
|
close: "Close Topic"
|
||||||
|
auto_close: "Auto Close"
|
||||||
unpin: "Un-Pin Topic"
|
unpin: "Un-Pin Topic"
|
||||||
pin: "Pin Topic"
|
pin: "Pin Topic"
|
||||||
unarchive: "Unarchive Topic"
|
unarchive: "Unarchive Topic"
|
||||||
|
|
|
@ -607,6 +607,11 @@ en:
|
||||||
archived_disabled: "This topic is now unarchived. It is no longer frozen, and can be changed."
|
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_enabled: "This topic is now closed. New replies are no longer allowed."
|
||||||
closed_disabled: "This topic is now opened. New replies are 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_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."
|
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."
|
visible_enabled: "This topic is now visible. It will be displayed in topic lists."
|
||||||
|
|
|
@ -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/clear-pin' => 'topics#clear_pin', constraints: {topic_id: /\d+/}
|
||||||
put 't/:topic_id/mute' => 'topics#mute', 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/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/: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+/}
|
get 't/:slug/:topic_id.rss' => 'topics#feed', format: :rss, constraints: {topic_id: /\d+/}
|
||||||
|
|
6
db/migrate/20130506185042_add_auto_close_at_to_topics.rb
Normal file
6
db/migrate/20130506185042_add_auto_close_at_to_topics.rb
Normal file
|
@ -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
|
28
lib/jobs.rb
28
lib/jobs.rb
|
@ -99,6 +99,34 @@ module Jobs
|
||||||
enqueue(job_name, opts.merge!(delay_for: secs))
|
enqueue(job_name, opts.merge!(delay_for: secs))
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# Require all jobs
|
# Require all jobs
|
||||||
|
|
15
lib/jobs/close_topic.rb
Normal file
15
lib/jobs/close_topic.rb
Normal file
|
@ -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
|
|
@ -53,6 +53,11 @@ class PostCreator
|
||||||
|
|
||||||
topic = Topic.new(topic_params)
|
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
|
if @opts[:archetype] == Archetype.private_message
|
||||||
|
|
||||||
topic.subtype = TopicSubtype.user_to_user unless topic.subtype
|
topic.subtype = TopicSubtype.user_to_user unless topic.subtype
|
||||||
|
|
35
spec/components/jobs/close_topic_spec.rb
Normal file
35
spec/components/jobs/close_topic_spec.rb
Normal file
|
@ -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
|
|
@ -75,5 +75,41 @@ describe Jobs do
|
||||||
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,14 @@ describe PostCreator do
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'uniqueness' do
|
context 'uniqueness' do
|
||||||
|
|
|
@ -516,4 +516,40 @@ describe TopicsController do
|
||||||
|
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -584,10 +584,10 @@ describe Topic do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'closed' do
|
shared_examples_for 'a status that closes a topic' do
|
||||||
context 'disable' do
|
context 'disable' do
|
||||||
before do
|
before do
|
||||||
@topic.update_status('closed', false, @user)
|
@topic.update_status(status, false, @user)
|
||||||
@topic.reload
|
@topic.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -602,7 +602,7 @@ describe Topic do
|
||||||
context 'enable' do
|
context 'enable' do
|
||||||
before do
|
before do
|
||||||
@topic.update_attribute :closed, false
|
@topic.update_attribute :closed, false
|
||||||
@topic.update_status('closed', true, @user)
|
@topic.update_status(status, true, @user)
|
||||||
@topic.reload
|
@topic.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -614,6 +614,16 @@ describe Topic do
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
|
@ -943,4 +953,114 @@ describe Topic do
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue