FEATURE: auto-close topics based on last post

This commit is contained in:
Régis Hanol 2014-10-10 18:21:44 +02:00
parent ac72b0bcf6
commit 5754e8dd0f
28 changed files with 242 additions and 146 deletions

View File

@ -1,27 +1,40 @@
export default Ember.Component.extend({
autoCloseValid: false,
limited: false,
label: function() {
return I18n.t( this.get('labelKey') || 'composer.auto_close_label' );
}.property('labelKey'),
autoCloseUnits: function() {
var key = this.get("limited") ? "composer.auto_close.limited.units"
: "composer.auto_close.all.units";
return I18n.t(key);
}.property("limited"),
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'),
autoCloseExamples: function() {
var key = this.get("limited") ? "composer.auto_close.limited.examples"
: "composer.auto_close.all.examples";
return I18n.t(key);
}.property("limited"),
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 {
_updateAutoCloseValid: function() {
var isValid = this._isAutoCloseValid(this.get("autoCloseTime"), this.get("limited"));
this.set("autoCloseValid", isValid);
}.observes("autoCloseTime", "limited"),
_isAutoCloseValid: function(autoCloseTime, limited) {
var t = (autoCloseTime || "").trim();
if (t.length === 0) {
// "empty" is always valid
return true;
} else if (limited) {
// only # of hours in limited mode
return t.match(/^(\d+\.)?\d+$/);
} else {
if (t.match(/^\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}(\s?[AP]M)?$/i)) {
// timestamp must be in the future
return moment(t).isAfter();
} else {
// either # of hours or absolute time
return (t.match(/^(\d+\.)?\d+$/) || t.match(/^\d{1,2}:\d{2}(\s?[AP]M)?$/i)) !== null;
}
}
}
});

View File

@ -1,5 +1,4 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
/**
@ -16,24 +15,23 @@ export default ObjectController.extend(ModalFunctionality, {
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') );
var autoCloseTime = null;
if (this.get("details.auto_close_based_on_last_post")) {
autoCloseTime = this.get("details.auto_close_hours");
} else 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_time', moment(closeTime).format("YYYY-MM-DD HH:mm"));
autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm");
}
} else {
this.set('details.auto_close_time', '');
}
}.observes('details.auto_close_at'),
this.set("auto_close_time", autoCloseTime);
}.observes("details.{auto_close_at,auto_close_hours}"),
actions: {
saveAutoClose: function() {
this.setAutoClose( this.get('auto_close_time') );
},
removeAutoClose: function() {
this.setAutoClose(null);
}
saveAutoClose: function() { this.setAutoClose(this.get("auto_close_time")); },
removeAutoClose: function() { this.setAutoClose(null); }
},
setAutoClose: function(time) {
@ -43,16 +41,20 @@ export default ObjectController.extend(ModalFunctionality, {
url: '/t/' + this.get('id') + '/autoclose',
type: 'PUT',
dataType: 'json',
data: { auto_close_time: Discourse.Utilities.timestampFromAutocloseString(time) }
data: {
auto_close_time: time,
auto_close_based_on_last_post: this.get("details.auto_close_based_on_last_post"),
}
}).then(function(result){
if (result.success) {
self.send('closeModal');
self.set('details.auto_close_at', result.auto_close_at);
self.set('details.auto_close_hours', result.auto_close_hours);
} else {
bootbox.alert(I18n.t('composer.auto_close_error'), function() { self.send('showModal'); } );
bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('showModal'); } );
}
}, function () {
bootbox.alert(I18n.t('composer.auto_close_error'), function() { self.send('showModal'); } );
bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('showModal'); } );
});
}

View File

@ -353,26 +353,6 @@ Discourse.Utilities = {
}
},
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 === '' ? null : arg);
}
}
},
defaultHomepage: function() {
// the homepage is the first item of the 'top_menu' site setting
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];

View File

@ -163,7 +163,6 @@ Discourse.Post = Discourse.Model.extend({
title: this.get('title'),
image_sizes: this.get('imageSizes'),
target_usernames: this.get('target_usernames'),
auto_close_time: Discourse.Utilities.timestampFromAutocloseString(this.get('auto_close_time'))
};
var metaData = this.get('metaData');

View File

@ -63,6 +63,7 @@ Discourse.Category = Discourse.Model.extend({
secure: this.get('secure'),
permissions: this.get('permissionsForUpdate'),
auto_close_hours: this.get('auto_close_hours'),
auto_close_based_on_last_post: this.get("auto_close_based_on_last_post"),
position: this.get('position'),
email_in: this.get('email_in'),
email_in_allow_strangers: this.get('email_in_allow_strangers'),

View File

@ -521,7 +521,6 @@ Discourse.Composer = Discourse.Model.extend({
admin: currentUser.get('admin'),
yours: true,
newPost: true,
auto_close_time: Discourse.Utilities.timestampFromAutocloseString(this.get('auto_close_time'))
});
if(post) {
@ -562,6 +561,7 @@ Discourse.Composer = Discourse.Model.extend({
// It's no longer a new post
createdPost.set('newPost', false);
topic.set('draft_sequence', result.draft_sequence);
topic.set('details.auto_close_at', result.topic_auto_close_at);
postStream.commitPost(createdPost);
addedToStream = true;
} else {

View File

@ -760,6 +760,9 @@ Discourse.PostStream = Em.Object.extend({
return existing;
}
// Update the auto_close_at value of the topic
this.set("topic.details.auto_close_at", post.get("topic_auto_close_at"));
post.set('topic', this.get('topic'));
postIdentityMap.set(post.get('id'), post);
@ -822,7 +825,6 @@ Discourse.PostStream = Em.Object.extend({
@returns {Promise} a promise that will resolve to the posts in the order requested.
**/
loadIntoIdentityMap: function(postIds) {
// If we don't want any posts, return a promise that resolves right away
if (Em.isEmpty(postIds)) {
return Ember.RSVP.resolve();

View File

@ -1,11 +1,19 @@
<div class="auto-close-fields">
<div>
<i class="fa fa-clock-o"></i>
{{label}}
{{text-field value=autoCloseTime}}
{{i18n composer.auto_close_units}}
<label>
{{fa-icon clock-o}}
{{i18n composer.auto_close.label}}
{{text-field value=autoCloseTime}}
{{autoCloseUnits}}
</label>
</div>
<div class="examples">
{{i18n composer.auto_close_examples}}
{{autoCloseExamples}}
</div>
<div>
<label>
{{input type="checkbox" name="autoCloseBasedOnLastPost" checked=autoCloseBasedOnLastPost}}
{{i18n composer.auto_close.based_on_last_post}}
</label>
</div>
</div>

View File

@ -1,6 +1,9 @@
<form {{action "saveAutoClose" on="submit"}}>
<div class="modal-body">
{{auto-close-form autoCloseTime=auto_close_time autoCloseValid=auto_close_valid}}
{{auto-close-form autoCloseTime=auto_close_time
autoCloseValid=auto_close_valid
autoCloseBasedOnLastPost=details.auto_close_based_on_last_post
limited=details.auto_close_based_on_last_post }}
</div>
<div class="modal-footer">
<button class='btn btn-primary' type='submit' {{bind-attr disabled="auto_close_invalid"}}>{{i18n topic.auto_close_save}}</button>

View File

@ -1,12 +1,7 @@
<section class='field'>
<div class="auto-close-fields">
<div>
<i class="fa fa-clock-o"></i>
{{i18n category.auto_close_label}}
{{text-field value=auto_close_hours}}
{{i18n category.auto_close_units}}
</div>
</div>
{{auto-close-form autoCloseTime=auto_close_hours
autoCloseBasedOnLastPost=auto_close_based_on_last_post
limited="true" }}
</section>
<section class='field'>

View File

@ -93,18 +93,27 @@ div.ac-wrap {
display: none;
}
.auto-close-fields {
div:not(:first-child) {
margin-top: 10px;
}
label {
font-size: 14px;
}
input {
text-align: center;
width: 150px;
}
.examples {
margin: 10px 0 0 0;
color: $primary;
font-style: italic;
}
}
.edit-auto-close-modal {
.btn.pull-right {
margin-right: 10px;
}
form {
margin: 0;
}
@ -118,3 +127,11 @@ div.ac-wrap {
}
}
}
.edit-category-modal {
.auto-close-fields {
input[type=text] {
width: 50px;
}
}
}

View File

@ -354,7 +354,6 @@ class ApplicationController < ActionController::Base
def render_post_json(post, add_raw=true)
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
post_serializer.add_raw = add_raw
post_serializer.topic_slug = post.topic.slug if post.topic.present?
counts = PostAction.counts_for([post], current_user)
if counts && counts = counts[post.id]

View File

@ -75,15 +75,20 @@ class CategoriesController < ApplicationController
def update
guardian.ensure_can_edit!(@category)
json_result(@category, serializer: CategorySerializer) { |cat|
json_result(@category, serializer: CategorySerializer) do |cat|
cat.move_to(category_params[:position].to_i) if category_params[:position]
if category_params.key? :email_in and category_params[:email_in].length == 0
# properly null the value so the database constrain doesn't catch us
category_params[:email_in] = nil
end
category_params.delete(:position)
cat.update_attributes(category_params)
}
end
end
def set_notifications
@ -125,6 +130,7 @@ class CategoriesController < ApplicationController
:email_in_allow_strangers,
:parent_category_id,
:auto_close_hours,
:auto_close_based_on_last_post,
:logo_url,
:background_url,
:allow_badges,

View File

@ -79,7 +79,6 @@ class PostsController < ApplicationController
else
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
post_serializer.topic_slug = post.topic.slug if post.topic.present?
post_serializer.draft_sequence = DraftSequence.current(current_user, post.topic.draft_key)
[true, MultiJson.dump(post_serializer)]
end
@ -132,7 +131,6 @@ class PostsController < ApplicationController
post_serializer.draft_sequence = DraftSequence.current(current_user, post.topic.draft_key)
link_counts = TopicLink.counts_for(guardian,post.topic, [post])
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
post_serializer.topic_slug = post.topic.slug if post.topic.present?
result = {post: post_serializer.as_json}
if revisor.category_changed.present?
@ -346,7 +344,6 @@ class PostsController < ApplicationController
:category,
:target_usernames,
:reply_to_post_number,
:auto_close_time,
:auto_track
]

View File

@ -178,12 +178,20 @@ class TopicsController < ApplicationController
end
def autoclose
raise Discourse::InvalidParameters.new(:auto_close_time) unless params.has_key?(:auto_close_time)
params.permit(:auto_close_time)
params.require(:auto_close_based_on_last_post)
topic = Topic.find_by(id: params[:topic_id].to_i)
guardian.ensure_can_moderate!(topic)
topic.auto_close_based_on_last_post = params[:auto_close_based_on_last_post]
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)
render json: success_json.merge!({
auto_close_at: topic.auto_close_at,
auto_close_hours: topic.auto_close_hours
})
else
render_json_error(topic)
end

View File

@ -170,7 +170,8 @@ SQL
def create_category_definition
t = Topic.new(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now, category_id: id)
t.skip_callbacks = true
t.auto_close_hours = nil
t.ignore_category_auto_close = true
t.set_auto_close(nil)
t.save!(validate: false)
update_column(:topic_id, t.id)
t.posts.create(raw: post_template, user: user)

View File

@ -147,10 +147,14 @@ class Topic < ActiveRecord::Base
end
end
attr_accessor :ignore_category_auto_close
before_create do
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_hours and self.auto_close_at.nil?
self.auto_close_based_on_last_post = self.category.auto_close_based_on_last_post
set_auto_close(self.category.auto_close_hours)
end
end
@ -158,7 +162,6 @@ class Topic < ActiveRecord::Base
attr_accessor :skip_callbacks
after_create do
unless skip_callbacks
changed_to_category(category)
if archetype == Archetype.private_message
@ -167,22 +170,18 @@ class Topic < ActiveRecord::Base
DraftSequence.next!(user, Draft::NEW_TOPIC)
end
end
end
before_save do
unless skip_callbacks
if (auto_close_at_changed? and !auto_close_at_was.nil?) or (auto_close_user_id_changed? and auto_close_at)
self.auto_close_started_at ||= Time.zone.now if auto_close_at
Jobs.cancel_scheduled_job(:close_topic, {topic_id: id})
true
end
if category_id.nil? && (archetype.nil? || archetype == Archetype.default)
self.category_id = SiteSetting.uncategorized_category_id
end
end
end
after_save do
@ -193,7 +192,6 @@ class Topic < ActiveRecord::Base
Jobs.enqueue_at(auto_close_at, :close_topic, {topic_id: id, user_id: auto_close_user_id || user_id})
end
end
end
# TODO move into PostRevisor or TopicRevisor
@ -745,11 +743,6 @@ class Topic < ActiveRecord::Base
end
end
def auto_close_hours=(num_hours)
@ignore_category_auto_close = true
set_auto_close( num_hours )
end
def self.auto_close
Topic.where("NOT closed AND auto_close_at < ? AND auto_close_user_id IS NOT NULL", 1.minute.ago).each do |t|
t.auto_close
@ -773,28 +766,51 @@ class Topic < ActiveRecord::Base
# * 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) && 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) && arg.include?('-') && timestamp = Time.zone.parse(arg)
self.auto_close_at = timestamp
self.errors.add(:auto_close_at, :invalid) if timestamp < Time.zone.now
self.auto_close_hours = nil
if self.auto_close_based_on_last_post
num_hours = arg.to_f
if num_hours > 0
last_post_created_at = self.ordered_posts.last.try(:created_at) || Time.zone.now
self.auto_close_at = last_post_created_at + num_hours.hours
self.auto_close_hours = num_hours
else
self.auto_close_at = nil
end
else
num_hours = arg.to_i
self.auto_close_at = (num_hours > 0 ? num_hours.hours.from_now : nil)
if arg.is_a?(String) && m = /^(\d{1,2}):(\d{2})(?:\s*[AP]M)?$/i.match(arg.strip)
now = Time.zone.now
self.auto_close_at = Time.zone.local(now.year, now.month, now.day, m[1].to_i, m[2].to_i)
self.auto_close_at += 1.day if self.auto_close_at < now
elsif arg.is_a?(String) && arg.include?("-") && 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_f
if num_hours > 0
self.auto_close_at = num_hours.hours.from_now
self.auto_close_hours = num_hours
else
self.auto_close_at = nil
end
end
end
unless self.auto_close_at.nil?
self.auto_close_started_at ||= Time.zone.now
if by_user && by_user.staff?
if self.auto_close_at.nil?
self.auto_close_started_at = nil
else
if self.auto_close_based_on_last_post
self.auto_close_started_at = Time.zone.now
else
self.auto_close_started_at ||= Time.zone.now
end
if by_user.try(:staff?)
self.auto_close_user = by_user
else
self.auto_close_user ||= (self.user.staff? ? self.user : Discourse.system_user)
end
else
self.auto_close_started_at = nil
end
self
end

View File

@ -3,6 +3,7 @@ class CategorySerializer < BasicCategorySerializer
attributes :read_restricted,
:available_groups,
:auto_close_hours,
:auto_close_based_on_last_post,
:group_permissions,
:position,
:email_in,
@ -20,7 +21,7 @@ class CategorySerializer < BasicCategorySerializer
}
end
if perms.length == 0 && !object.read_restricted
perms << {permission_type: CategoryGroup.permission_types[:full], group_name: :everyone}
perms << { permission_type: CategoryGroup.permission_types[:full], group_name: :everyone }
end
perms
end
@ -30,7 +31,6 @@ class CategorySerializer < BasicCategorySerializer
Group.order(:name).pluck(:name) - group_permissions.map{|g| g[:group_name]}
end
def can_delete
true
end

View File

@ -1,8 +1,7 @@
class PostSerializer < BasicPostSerializer
# To pass in additional information we might need
attr_accessor :topic_slug,
:topic_view,
attr_accessor :topic_view,
:parent_post,
:add_raw,
:single_post_link_counts,
@ -20,8 +19,9 @@ class PostSerializer < BasicPostSerializer
:reads,
:score,
:yours,
:topic_slug,
:topic_id,
:topic_slug,
:topic_auto_close_at,
:display_username,
:primary_group_name,
:version,
@ -53,6 +53,14 @@ class PostSerializer < BasicPostSerializer
:static_doc,
:via_email
def topic_slug
object.try(:topic).try(:slug)
end
def topic_auto_close_at
object.try(:topic).try(:auto_close_at)
end
def moderator?
!!(object.try(:user).try(:moderator?))
end

View File

@ -21,7 +21,6 @@ module PostStreamSerializerMixin
object.posts.each_with_index do |p, idx|
highest_number_in_posts = p.post_number if p.post_number > highest_number_in_posts
ps = PostSerializer.new(p, scope: scope, root: false)
ps.topic_slug = object.topic.slug
ps.topic_view = object
p.topic = object.topic

View File

@ -57,6 +57,8 @@ class TopicViewSerializer < ApplicationSerializer
def details
result = {
auto_close_at: object.topic.auto_close_at,
auto_close_hours: object.topic.auto_close_hours,
auto_close_based_on_last_post: object.topic.auto_close_based_on_last_post,
created_by: BasicUserSerializer.new(object.topic.user, scope: scope, root: false),
last_poster: BasicUserSerializer.new(object.topic.last_poster, scope: scope, root: false)
}

View File

@ -705,10 +705,16 @@ en:
toggler: "hide or show the composer panel"
admin_options_title: "Optional staff settings for this topic"
auto_close_label: "Auto-close topic time:"
auto_close_units: "(# of hours, a time, or a timestamp)"
auto_close_examples: 'enter absolute time or number of hours — 24, 17:00, 2013-11-22 14:00'
auto_close_error: "Please enter a valid value."
auto_close:
label: "Auto-close topic time:"
error: "Please enter a valid value."
based_on_last_post: "Auto-close based on last post."
all:
units: "(# of hours, a time or a timestamp)"
examples: 'Enter number of hours (24), absolute time (17:30) or timestamp (2013-11-22 14:00).'
limited:
units: "(# of hours)"
examples: 'Enter number of hours (24).'
notifications:
title: "notifications of @name mentions, replies to your posts and topics, private messages, etc"

View File

@ -0,0 +1,6 @@
class AddAutoCloseBasedOnLastPostAndAutoCloseHoursToTopics < ActiveRecord::Migration
def change
add_column :topics, :auto_close_based_on_last_post, :boolean, default: false
add_column :topics, :auto_close_hours, :float
end
end

View File

@ -0,0 +1,5 @@
class AddAutoCloseBasedOnLastPostToCategories < ActiveRecord::Migration
def change
add_column :categories, :auto_close_based_on_last_post, :boolean, default: false
end
end

View File

@ -76,6 +76,7 @@ class PostCreator
store_unique_post_key
track_topic
update_topic_stats
update_topic_auto_close
update_user_counts
create_embedded_topic
@ -208,6 +209,12 @@ class PostCreator
@topic.update_attributes(attrs)
end
def update_topic_auto_close
if @topic.auto_close_based_on_last_post && @topic.auto_close_hours
@topic.set_auto_close(@topic.auto_close_hours).save
end
end
def setup_post
@opts[:raw] = TextCleaner.normalize_whitespaces(@opts[:raw]).gsub(/\s+\z/, "")

View File

@ -201,6 +201,31 @@ describe PostCreator do
topic.reload
}.to_not change { topic.excerpt }
end
describe "topic's auto close" do
it "doesn't update topic's auto close when it's not based on last post" do
auto_close_time = 1.day.from_now
topic = Fabricate(:topic, auto_close_at: auto_close_time, auto_close_hours: 12)
PostCreator.new(topic.user, topic_id: topic.id, raw: "this is a second post").create
topic.reload
topic.auto_close_at.should be_within(1.second).of(auto_close_time)
end
it "updates topic's auto close date when it's based on last post" do
auto_close_time = 1.day.from_now
topic = Fabricate(:topic, auto_close_at: auto_close_time, auto_close_hours: 12, auto_close_based_on_last_post: true)
PostCreator.new(topic.user, topic_id: topic.id, raw: "this is a second post").create
topic.reload
topic.auto_close_at.should_not be_within(1.second).of(auto_close_time)
end
end
end
context 'when auto-close param is given' do
@ -526,4 +551,3 @@ describe PostCreator do
end
end

View File

@ -865,12 +865,14 @@ describe TopicsController do
describe 'autoclose' do
it 'needs you to be logged in' do
lambda { xhr :put, :autoclose, topic_id: 99, auto_close_time: '24'}.should raise_error(Discourse::NotLoggedIn)
-> {
xhr :put, :autoclose, topic_id: 99, auto_close_time: '24', auto_close_based_on_last_post: false
}.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_time: '24'
xhr :put, :autoclose, topic_id: 99, auto_close_time: '24', auto_close_based_on_last_post: false
response.should be_forbidden
end
@ -880,16 +882,17 @@ describe TopicsController do
@topic = Fabricate(:topic, user: @admin)
end
it "can set a topic's auto close time" do
it "can set a topic's auto close time and 'based on last post' property" do
Topic.any_instance.expects(:set_auto_close).with("24", @admin)
xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: '24'
xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: '24', auto_close_based_on_last_post: true
json = ::JSON.parse(response.body)
json.should have_key('auto_close_at')
json.should have_key('auto_close_hours')
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_time: nil
xhr :put, :autoclose, topic_id: @topic.id, auto_close_time: nil, auto_close_based_on_last_post: false
end
end

View File

@ -990,7 +990,8 @@ describe Topic do
it 'queues a job to close the topic' do
Timecop.freeze(now) do
Jobs.expects(:enqueue_at).with(7.hours.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
Fabricate(:topic, auto_close_hours: 7, user: Fabricate(:admin))
topic = Fabricate(:topic, user: Fabricate(:admin))
topic.set_auto_close(7).save
end
end
@ -999,7 +1000,8 @@ describe Topic do
Jobs.expects(:enqueue_at).with do |datetime, job_name, job_args|
job_args[:user_id] == topic_creator.id
end
Fabricate(:topic, auto_close_hours: 7, user: topic_creator)
topic = Fabricate(:topic, user: topic_creator)
topic.set_auto_close(7).save
end
it 'when auto_close_user_id is set, it will use it as the topic closer' do
@ -1008,19 +1010,22 @@ describe Topic do
Jobs.expects(:enqueue_at).with do |datetime, job_name, job_args|
job_args[:user_id] == topic_closer.id
end
Fabricate(:topic, auto_close_hours: 7, auto_close_user: topic_closer, user: topic_creator)
topic = Fabricate(:topic, user: topic_creator)
topic.set_auto_close(7, topic_closer).save
end
it "ignores the category's default auto-close" do
Timecop.freeze(now) do
Jobs.expects(:enqueue_at).with(7.hours.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
Fabricate(:topic, auto_close_hours: 7, user: Fabricate(:admin), category_id: Fabricate(:category, auto_close_hours: 2).id)
topic = Fabricate(:topic, user: Fabricate(:admin), ignore_category_auto_close: true, category_id: Fabricate(:category, auto_close_hours: 2).id)
topic.set_auto_close(7).save
end
end
it 'sets the time when auto_close timer starts' do
Timecop.freeze(now) do
topic = Fabricate(:topic, auto_close_hours: 7, user: Fabricate(:admin))
topic = Fabricate(:topic, user: Fabricate(:admin))
topic.set_auto_close(7).save
expect(topic.auto_close_started_at).to eq(now)
end
end
@ -1124,25 +1129,9 @@ describe Topic do
end
end
describe "auto_close_hours=" do
subject(:topic) { Fabricate.build(:topic) }
it 'can take a number' do
Timecop.freeze(now) do
topic.auto_close_hours = 2
topic.auto_close_at.should be_within_one_second_of(2.hours.from_now)
end
end
it 'can take nil' do
topic.auto_close_hours = nil
topic.auto_close_at.should == nil
end
end
describe 'set_auto_close' do
let(:topic) { Fabricate.build(:topic) }
let(:closing_topic) { Fabricate.build(:topic, auto_close_hours: 5) }
let(:closing_topic) { Fabricate.build(:topic, auto_close_hours: 5, auto_close_at: 5.hours.from_now, auto_close_started_at: 5.hours.from_now) }
let(:admin) { Fabricate.build(:user, id: 123) }
before { Discourse.stubs(:system_user).returns(admin) }