mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:23:25 +08:00
FEATURE: auto-close topics based on last post
This commit is contained in:
parent
ac72b0bcf6
commit
5754e8dd0f
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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'); } );
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
class AddAutoCloseBasedOnLastPostToCategories < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :categories, :auto_close_based_on_last_post, :boolean, default: false
|
||||
end
|
||||
end
|
|
@ -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/, "")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
|
Loading…
Reference in New Issue
Block a user