FEATURE: automatically delete replies on a topic after N days. (#9209)
This commit is contained in:
parent
0cd502a558
commit
aad12822b7
|
@ -1,18 +1,15 @@
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import { equal, or, readOnly } from "@ember/object/computed";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed, {
|
||||
observes,
|
||||
on
|
||||
} from "discourse-common/utils/decorators";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import {
|
||||
PUBLISH_TO_CATEGORY_STATUS_TYPE,
|
||||
OPEN_STATUS_TYPE,
|
||||
DELETE_STATUS_TYPE,
|
||||
REMINDER_TYPE,
|
||||
CLOSE_STATUS_TYPE,
|
||||
BUMP_TYPE
|
||||
BUMP_TYPE,
|
||||
DELETE_REPLIES_TYPE
|
||||
} from "discourse/controllers/edit-topic-timer";
|
||||
|
||||
export default Component.extend({
|
||||
|
@ -23,15 +20,18 @@ export default Component.extend({
|
|||
autoBump: equal("selection", BUMP_TYPE),
|
||||
publishToCategory: equal("selection", PUBLISH_TO_CATEGORY_STATUS_TYPE),
|
||||
reminder: equal("selection", REMINDER_TYPE),
|
||||
autoDeleteReplies: equal("selection", DELETE_REPLIES_TYPE),
|
||||
showTimeOnly: or("autoOpen", "autoDelete", "reminder", "autoBump"),
|
||||
|
||||
@discourseComputed(
|
||||
"topicTimer.updateTime",
|
||||
showFutureDateInput: or(
|
||||
"showTimeOnly",
|
||||
"publishToCategory",
|
||||
"topicTimer.category_id"
|
||||
)
|
||||
saveDisabled(updateTime, publishToCategory, topicTimerCategoryId) {
|
||||
return isEmpty(updateTime) || (publishToCategory && !topicTimerCategoryId);
|
||||
"autoClose",
|
||||
"autoDeleteReplies"
|
||||
),
|
||||
|
||||
@discourseComputed("autoDeleteReplies")
|
||||
durationType(autoDeleteReplies) {
|
||||
return autoDeleteReplies ? "days" : "hours";
|
||||
},
|
||||
|
||||
@discourseComputed("topic.visible")
|
||||
|
@ -39,25 +39,6 @@ export default Component.extend({
|
|||
if (visible) return this.get("topic.category_id");
|
||||
},
|
||||
|
||||
@on("init")
|
||||
@observes("topicTimer", "topicTimer.execute_at", "topicTimer.duration")
|
||||
_setUpdateTime() {
|
||||
let time = null;
|
||||
const executeAt = this.get("topicTimer.execute_at");
|
||||
|
||||
if (executeAt && this.get("topicTimer.based_on_last_post")) {
|
||||
time = this.get("topicTimer.duration");
|
||||
} else if (executeAt) {
|
||||
const closeTime = moment(executeAt);
|
||||
|
||||
if (closeTime > moment()) {
|
||||
time = closeTime.format("YYYY-MM-DD HH:mm");
|
||||
}
|
||||
}
|
||||
|
||||
this.set("topicTimer.updateTime", time);
|
||||
},
|
||||
|
||||
@observes("selection")
|
||||
_updateBasedOnLastPost() {
|
||||
if (!this.autoClose) {
|
||||
|
@ -79,11 +60,5 @@ export default Component.extend({
|
|||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
onChangeTimerType(value) {
|
||||
this.set("topicTimer.status_type", value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import { equal, and, empty } from "@ember/object/computed";
|
||||
import { equal, and, empty, or } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import { FORMAT } from "select-kit/components/future-date-input-selector";
|
||||
|
@ -10,10 +10,13 @@ export default Component.extend({
|
|||
date: null,
|
||||
time: null,
|
||||
includeDateTime: true,
|
||||
duration: null,
|
||||
durationType: "hours",
|
||||
isCustom: equal("selection", "pick_date_and_time"),
|
||||
isBasedOnLastPost: equal("selection", "set_based_on_last_post"),
|
||||
displayDateAndTimePicker: and("includeDateTime", "isCustom"),
|
||||
displayLabel: null,
|
||||
displayNumberInput: or("isBasedOnLastPost", "isBasedOnDuration"),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
@ -21,6 +24,8 @@ export default Component.extend({
|
|||
if (this.input) {
|
||||
if (this.basedOnLastPost) {
|
||||
this.set("selection", "set_based_on_last_post");
|
||||
} else if (this.isBasedOnDuration) {
|
||||
this.set("selection", null);
|
||||
} else {
|
||||
const datetime = moment(this.input);
|
||||
this.setProperties({
|
||||
|
@ -57,28 +62,44 @@ export default Component.extend({
|
|||
this.set("basedOnLastPost", this.isBasedOnLastPost);
|
||||
},
|
||||
|
||||
@discourseComputed("input", "isBasedOnLastPost")
|
||||
duration(input, isBasedOnLastPost) {
|
||||
const now = moment();
|
||||
|
||||
if (isBasedOnLastPost) {
|
||||
return parseFloat(input);
|
||||
} else {
|
||||
return moment(input) - now;
|
||||
}
|
||||
@observes("duration")
|
||||
_updateDuration() {
|
||||
this.attrs.onChangeDuration &&
|
||||
this.attrs.onChangeDuration(parseInt(this.duration, 0));
|
||||
},
|
||||
|
||||
@discourseComputed("input", "isBasedOnLastPost")
|
||||
executeAt(input, isBasedOnLastPost) {
|
||||
if (isBasedOnLastPost) {
|
||||
return moment()
|
||||
.add(input, "hours")
|
||||
@discourseComputed(
|
||||
"input",
|
||||
"duration",
|
||||
"isBasedOnLastPost",
|
||||
"isBasedOnDuration",
|
||||
"durationType"
|
||||
)
|
||||
executeAt(
|
||||
input,
|
||||
duration,
|
||||
isBasedOnLastPost,
|
||||
isBasedOnDuration,
|
||||
durationType
|
||||
) {
|
||||
if (isBasedOnLastPost || isBasedOnDuration) {
|
||||
return moment(input)
|
||||
.add(parseInt(duration, 0), durationType)
|
||||
.format(FORMAT);
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("durationType")
|
||||
durationLabel(durationType) {
|
||||
return I18n.t(
|
||||
`topic.topic_status_update.num_of_${
|
||||
durationType === "hours" ? "hours" : "days"
|
||||
}`
|
||||
);
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
|
@ -92,7 +113,9 @@ export default Component.extend({
|
|||
"date",
|
||||
"time",
|
||||
"willCloseImmediately",
|
||||
"categoryId"
|
||||
"categoryId",
|
||||
"displayNumberInput",
|
||||
"duration"
|
||||
)
|
||||
showTopicStatusInfo(
|
||||
statusType,
|
||||
|
@ -101,7 +124,9 @@ export default Component.extend({
|
|||
date,
|
||||
time,
|
||||
willCloseImmediately,
|
||||
categoryId
|
||||
categoryId,
|
||||
displayNumberInput,
|
||||
duration
|
||||
) {
|
||||
if (!statusType || willCloseImmediately) return false;
|
||||
|
||||
|
@ -114,6 +139,8 @@ export default Component.extend({
|
|||
return moment(`${date}${time ? " " + time : ""}`).isAfter(moment());
|
||||
}
|
||||
return time;
|
||||
} else if (displayNumberInput) {
|
||||
return duration;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ import { cancel, later } from "@ember/runloop";
|
|||
import Component from "@ember/component";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import Category from "discourse/models/category";
|
||||
import { REMINDER_TYPE } from "discourse/controllers/edit-topic-timer";
|
||||
import {
|
||||
REMINDER_TYPE,
|
||||
DELETE_REPLIES_TYPE
|
||||
} from "discourse/controllers/edit-topic-timer";
|
||||
import ENV from "discourse-common/config/environment";
|
||||
|
||||
export default Component.extend({
|
||||
|
@ -28,7 +31,13 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
renderTopicTimer() {
|
||||
if (!this.executeAt || this.executeAt < moment()) {
|
||||
const isDeleteRepliesType = this.statusType === DELETE_REPLIES_TYPE;
|
||||
|
||||
if (
|
||||
!isDeleteRepliesType &&
|
||||
!this.basedOnLastPost &&
|
||||
(!this.executeAt || this.executeAt < moment())
|
||||
) {
|
||||
this.set("showTopicTimer", null);
|
||||
return;
|
||||
}
|
||||
|
@ -40,7 +49,7 @@ export default Component.extend({
|
|||
const statusUpdateAt = moment(this.executeAt);
|
||||
const duration = moment.duration(statusUpdateAt - moment());
|
||||
const minutesLeft = duration.asMinutes();
|
||||
if (minutesLeft > 0) {
|
||||
if (minutesLeft > 0 || isDeleteRepliesType || this.basedOnLastPost) {
|
||||
let rerenderDelay = 1000;
|
||||
if (minutesLeft > 2160) {
|
||||
rerenderDelay = 12 * 60 * 60000;
|
||||
|
@ -51,11 +60,15 @@ export default Component.extend({
|
|||
} else if (minutesLeft > 2) {
|
||||
rerenderDelay = 60000;
|
||||
}
|
||||
let autoCloseHours = this.duration || 0;
|
||||
let durationHours = parseInt(this.duration, 0) || 0;
|
||||
|
||||
if (isDeleteRepliesType) {
|
||||
durationHours *= 24;
|
||||
}
|
||||
|
||||
let options = {
|
||||
timeLeft: duration.humanize(true),
|
||||
duration: moment.duration(autoCloseHours, "hours").humanize()
|
||||
duration: moment.duration(durationHours, "hours").humanize()
|
||||
};
|
||||
|
||||
const categoryId = this.categoryId;
|
||||
|
|
|
@ -4,6 +4,7 @@ import discourseComputed from "discourse-common/utils/decorators";
|
|||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import TopicTimer from "discourse/models/topic-timer";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { FORMAT } from "select-kit/components/future-date-input-selector";
|
||||
|
||||
export const CLOSE_STATUS_TYPE = "close";
|
||||
export const OPEN_STATUS_TYPE = "open";
|
||||
|
@ -11,6 +12,7 @@ export const PUBLISH_TO_CATEGORY_STATUS_TYPE = "publish_to_category";
|
|||
export const DELETE_STATUS_TYPE = "delete";
|
||||
export const REMINDER_TYPE = "reminder";
|
||||
export const BUMP_TYPE = "bump";
|
||||
export const DELETE_REPLIES_TYPE = "delete_replies";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
loading: false,
|
||||
|
@ -41,10 +43,16 @@ export default Controller.extend(ModalFunctionality, {
|
|||
}
|
||||
];
|
||||
if (this.currentUser.get("staff")) {
|
||||
types.push({
|
||||
types.push(
|
||||
{
|
||||
id: DELETE_STATUS_TYPE,
|
||||
name: I18n.t("topic.auto_delete.title")
|
||||
});
|
||||
},
|
||||
{
|
||||
id: DELETE_REPLIES_TYPE,
|
||||
name: I18n.t("topic.auto_delete_replies.title")
|
||||
}
|
||||
);
|
||||
}
|
||||
return types;
|
||||
},
|
||||
|
@ -68,7 +76,7 @@ export default Controller.extend(ModalFunctionality, {
|
|||
return "true" === isPublic ? publicTopicTimer : privateTopicTimer;
|
||||
},
|
||||
|
||||
_setTimer(time, statusType) {
|
||||
_setTimer(time, duration, statusType) {
|
||||
this.set("loading", true);
|
||||
|
||||
TopicTimer.updateStatus(
|
||||
|
@ -76,10 +84,11 @@ export default Controller.extend(ModalFunctionality, {
|
|||
time,
|
||||
this.get("topicTimer.based_on_last_post"),
|
||||
statusType,
|
||||
this.get("topicTimer.category_id")
|
||||
this.get("topicTimer.category_id"),
|
||||
duration
|
||||
)
|
||||
.then(result => {
|
||||
if (time) {
|
||||
if (time || duration) {
|
||||
this.send("closeModal");
|
||||
|
||||
setProperties(this.topicTimer, {
|
||||
|
@ -103,17 +112,39 @@ export default Controller.extend(ModalFunctionality, {
|
|||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
onShow() {
|
||||
let time = null;
|
||||
const executeAt = this.get("topicTimer.execute_at");
|
||||
|
||||
if (executeAt) {
|
||||
const closeTime = moment(executeAt);
|
||||
|
||||
if (closeTime > moment()) {
|
||||
time = closeTime.format(FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
this.send("onChangeInput", time);
|
||||
},
|
||||
|
||||
actions: {
|
||||
onChangeStatusType(value) {
|
||||
this.set("topicTimer.status_type", value);
|
||||
},
|
||||
|
||||
onChangeUpdateTime(value) {
|
||||
onChangeInput(value) {
|
||||
this.set("topicTimer.updateTime", value);
|
||||
},
|
||||
|
||||
onChangeDuration(value) {
|
||||
this.set("topicTimer.duration", value);
|
||||
},
|
||||
|
||||
saveTimer() {
|
||||
if (!this.get("topicTimer.updateTime")) {
|
||||
if (
|
||||
!this.get("topicTimer.updateTime") &&
|
||||
!this.get("topicTimer.duration")
|
||||
) {
|
||||
this.flash(
|
||||
I18n.t("topic.topic_status_update.time_frame_required"),
|
||||
"alert-error"
|
||||
|
@ -123,12 +154,13 @@ export default Controller.extend(ModalFunctionality, {
|
|||
|
||||
this._setTimer(
|
||||
this.get("topicTimer.updateTime"),
|
||||
this.get("topicTimer.duration"),
|
||||
this.get("topicTimer.status_type")
|
||||
);
|
||||
},
|
||||
|
||||
removeTimer() {
|
||||
this._setTimer(null, this.get("topicTimer.status_type"));
|
||||
this._setTimer(null, null, this.get("topicTimer.status_type"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,7 +4,14 @@ import RestModel from "discourse/models/rest";
|
|||
const TopicTimer = RestModel.extend({});
|
||||
|
||||
TopicTimer.reopenClass({
|
||||
updateStatus(topicId, time, basedOnLastPost, statusType, categoryId) {
|
||||
updateStatus(
|
||||
topicId,
|
||||
time,
|
||||
basedOnLastPost,
|
||||
statusType,
|
||||
categoryId,
|
||||
duration
|
||||
) {
|
||||
let data = {
|
||||
time,
|
||||
status_type: statusType
|
||||
|
@ -12,6 +19,7 @@ TopicTimer.reopenClass({
|
|||
|
||||
if (basedOnLastPost) data.based_on_last_post = basedOnLastPost;
|
||||
if (categoryId) data.category_id = categoryId;
|
||||
if (duration) data.duration = duration;
|
||||
|
||||
return ajax({
|
||||
url: `/t/${topicId}/timer`,
|
||||
|
|
|
@ -7,18 +7,7 @@
|
|||
value=selection
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{#if showTimeOnly}}
|
||||
{{future-date-input
|
||||
input=(readonly topicTimer.updateTime)
|
||||
label="topic.topic_status_update.when"
|
||||
statusType=selection
|
||||
includeWeekend=true
|
||||
basedOnLastPost=topicTimer.based_on_last_post
|
||||
onChangeInput=onChangeUpdateTime
|
||||
}}
|
||||
{{else if publishToCategory}}
|
||||
{{#if publishToCategory}}
|
||||
<div class="control-group">
|
||||
<label>{{i18n 'topic.topic_status_update.publish_to'}}</label>
|
||||
{{category-chooser
|
||||
|
@ -27,26 +16,23 @@
|
|||
onChange=(action (mut topicTimer.category_id))
|
||||
}}
|
||||
</div>
|
||||
|
||||
{{/if}}
|
||||
{{#if showFutureDateInput}}
|
||||
<div class="control-group">
|
||||
{{future-date-input
|
||||
input=(readonly topicTimer.updateTime)
|
||||
duration=(readonly topicTimer.duration)
|
||||
label="topic.topic_status_update.when"
|
||||
statusType=selection
|
||||
includeWeekend=true
|
||||
basedOnLastPost=topicTimer.based_on_last_post
|
||||
onChangeInput=onChangeInput
|
||||
onChangeDuration=onChangeDuration
|
||||
categoryId=topicTimer.category_id
|
||||
basedOnLastPost=topicTimer.based_on_last_post
|
||||
onChangeInput=onChangeUpdateTime
|
||||
}}
|
||||
{{else if autoClose}}
|
||||
{{future-date-input
|
||||
input=topicTimer.updateTime
|
||||
label="topic.topic_status_update.when"
|
||||
statusType=selection
|
||||
includeWeekend=true
|
||||
basedOnLastPost=topicTimer.based_on_last_post
|
||||
lastPostedAt=model.last_posted_at
|
||||
onChangeInput=onChangeUpdateTime
|
||||
isBasedOnDuration=autoDeleteReplies
|
||||
durationType=durationType
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div class="future-date-input">
|
||||
{{#unless isBasedOnDuration}}
|
||||
<div class="control-group">
|
||||
<label>{{displayLabel}}</label>
|
||||
{{future-date-input-selector
|
||||
|
@ -16,6 +17,7 @@
|
|||
onChange=(action (mut selection))
|
||||
}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if displayDateAndTimePicker}}
|
||||
<div class="control-group">
|
||||
|
@ -33,11 +35,11 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isBasedOnLastPost}}
|
||||
{{#if displayNumberInput}}
|
||||
<div class="control-group">
|
||||
<label>
|
||||
{{i18n 'topic.topic_status_update.num_of_hours'}}
|
||||
{{text-field value=input type="number"}}
|
||||
{{durationLabel}}
|
||||
{{text-field value=duration type="number"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
timerTypes=selections
|
||||
updateTime=updateTime
|
||||
onChangeStatusType=(action "onChangeStatusType")
|
||||
onChangeUpdateTime=(action "onChangeUpdateTime")
|
||||
onChangeInput=(action "onChangeInput")
|
||||
onChangeDuration=(action "onChangeDuration")
|
||||
}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
|
|||
return "future-date-input-selector/future-date-input-selector-row";
|
||||
},
|
||||
|
||||
content: computed(function() {
|
||||
content: computed("statusType", function() {
|
||||
const now = moment();
|
||||
const opts = {
|
||||
now,
|
||||
|
|
|
@ -435,16 +435,19 @@ class TopicsController < ApplicationController
|
|||
rescue
|
||||
invalid_param(:status_type)
|
||||
end
|
||||
based_on_last_post = params[:based_on_last_post]
|
||||
params.require(:duration) if based_on_last_post || TopicTimer.types[:delete_replies] == status_type
|
||||
|
||||
topic = Topic.find_by(id: params[:topic_id])
|
||||
guardian.ensure_can_moderate!(topic)
|
||||
|
||||
options = {
|
||||
by_user: current_user,
|
||||
based_on_last_post: params[:based_on_last_post]
|
||||
based_on_last_post: based_on_last_post
|
||||
}
|
||||
|
||||
options.merge!(category_id: params[:category_id]) if !params[:category_id].blank?
|
||||
options.merge!(duration: params[:duration].to_i) if params[:duration].present?
|
||||
|
||||
topic_status_update = topic.set_or_create_timer(
|
||||
status_type,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class DeleteReplies < ::Jobs::Base
|
||||
|
||||
def execute(args)
|
||||
topic_timer = TopicTimer.find_by(id: args[:topic_timer_id] || args[:topic_status_update_id])
|
||||
|
||||
topic = topic_timer&.topic
|
||||
|
||||
if topic_timer.blank? || topic.blank? || topic_timer.execute_at > Time.zone.now
|
||||
return
|
||||
end
|
||||
|
||||
unless Guardian.new(topic_timer.user).is_staff?
|
||||
topic_timer.trash!(Discourse.system_user)
|
||||
return
|
||||
end
|
||||
|
||||
replies = topic.posts.where("posts.post_number > 1")
|
||||
replies.where('posts.created_at < ?', topic_timer.duration.days.ago).each do |post|
|
||||
PostDestroyer.new(topic_timer.user, post, context: I18n.t("topic_statuses.auto_deleted_by_timer")).destroy
|
||||
end
|
||||
|
||||
topic_timer.execute_at = (replies.minimum(:created_at) || Time.zone.now) + topic_timer.duration.days.ago
|
||||
topic_timer.save
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1132,8 +1132,8 @@ class Topic < ActiveRecord::Base
|
|||
# * by_user: User who is setting the topic's status update.
|
||||
# * based_on_last_post: True if time should be based on timestamp of the last post.
|
||||
# * category_id: Category that the update will apply to.
|
||||
def set_or_create_timer(status_type, time, by_user: nil, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id)
|
||||
return delete_topic_timer(status_type, by_user: by_user) if time.blank?
|
||||
def set_or_create_timer(status_type, time, by_user: nil, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id, duration: nil)
|
||||
return delete_topic_timer(status_type, by_user: by_user) if time.blank? && duration.blank?
|
||||
|
||||
public_topic_timer = !!TopicTimer.public_types[status_type]
|
||||
topic_timer_options = { topic: self, public_type: public_topic_timer }
|
||||
|
@ -1143,19 +1143,24 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
time_now = Time.zone.now
|
||||
topic_timer.based_on_last_post = !based_on_last_post.blank?
|
||||
topic_timer.duration = duration
|
||||
|
||||
if status_type == TopicTimer.types[:publish_to_category]
|
||||
topic_timer.category = Category.find_by(id: category_id)
|
||||
end
|
||||
|
||||
if topic_timer.based_on_last_post
|
||||
num_hours = time.to_f
|
||||
|
||||
if num_hours > 0
|
||||
if duration > 0
|
||||
last_post_created_at = self.ordered_posts.last.present? ? self.ordered_posts.last.created_at : time_now
|
||||
topic_timer.execute_at = last_post_created_at + num_hours.hours
|
||||
topic_timer.execute_at = last_post_created_at + duration.hours
|
||||
topic_timer.created_at = last_post_created_at
|
||||
end
|
||||
elsif topic_timer.status_type == TopicTimer.types[:delete_replies]
|
||||
if duration > 0
|
||||
first_reply_created_at = (self.ordered_posts.where("post_number > 1").minimum(:created_at) || time_now)
|
||||
topic_timer.execute_at = first_reply_created_at + duration.days
|
||||
topic_timer.created_at = first_reply_created_at
|
||||
end
|
||||
else
|
||||
utc = Time.find_zone("UTC")
|
||||
is_float = (Float(time) rescue nil)
|
||||
|
|
|
@ -49,7 +49,8 @@ class TopicTimer < ActiveRecord::Base
|
|||
publish_to_category: 3,
|
||||
delete: 4,
|
||||
reminder: 5,
|
||||
bump: 6
|
||||
bump: 6,
|
||||
delete_replies: 7
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -73,14 +74,6 @@ class TopicTimer < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def duration
|
||||
if (self.execute_at && self.created_at)
|
||||
((self.execute_at - self.created_at) / 1.hour).round(2)
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def public_type?
|
||||
!!self.class.public_types[self.status_type]
|
||||
end
|
||||
|
@ -120,6 +113,14 @@ class TopicTimer < ActiveRecord::Base
|
|||
Jobs.cancel_scheduled_job(:bump_topic, topic_timer_id: id)
|
||||
end
|
||||
|
||||
def cancel_auto_delete_replies_job
|
||||
Jobs.cancel_scheduled_job(:delete_replies, topic_timer_id: id)
|
||||
end
|
||||
|
||||
def schedule_auto_delete_replies_job(time)
|
||||
Jobs.enqueue_at(time, :delete_replies, topic_timer_id: id)
|
||||
end
|
||||
|
||||
def schedule_auto_bump_job(time)
|
||||
Jobs.enqueue_at(time, :bump_topic, topic_timer_id: id)
|
||||
end
|
||||
|
|
|
@ -2139,6 +2139,7 @@ en:
|
|||
title: "Topic Timer"
|
||||
save: "Set Timer"
|
||||
num_of_hours: "Number of hours:"
|
||||
num_of_days: "Number of days:"
|
||||
remove: "Remove Timer"
|
||||
publish_to: "Publish To:"
|
||||
when: "When:"
|
||||
|
@ -2181,6 +2182,8 @@ en:
|
|||
title: "Auto-Bump Topic"
|
||||
reminder:
|
||||
title: "Remind Me"
|
||||
auto_delete_replies:
|
||||
title: "Auto-Delete Replies"
|
||||
|
||||
status_update_notice:
|
||||
auto_open: "This topic will automatically open %{timeLeft}."
|
||||
|
@ -2190,6 +2193,7 @@ en:
|
|||
auto_delete: "This topic will be automatically deleted %{timeLeft}."
|
||||
auto_bump: "This topic will be automatically bumped %{timeLeft}."
|
||||
auto_reminder: "You will be reminded about this topic %{timeLeft}."
|
||||
auto_delete_replies: "Replies on this topic are automatically deleted after %{duration}."
|
||||
auto_close_title: "Auto-Close Settings"
|
||||
auto_close_immediate:
|
||||
one: "The last post in the topic is already %{count} hour old, so the topic will be closed immediately."
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDurationToTopicTimers < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :topic_timers, :duration, :integer, null: true
|
||||
end
|
||||
end
|
|
@ -2644,6 +2644,28 @@ RSpec.describe TopicsController do
|
|||
expect(json['closed']).to eq(topic.closed)
|
||||
end
|
||||
|
||||
it 'should be able to create a topic status update with duration' do
|
||||
post "/t/#{topic.id}/timer.json", params: {
|
||||
duration: 5,
|
||||
status_type: TopicTimer.types[7]
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_status_update = TopicTimer.last
|
||||
|
||||
expect(topic_status_update.topic).to eq(topic)
|
||||
expect(topic_status_update.execute_at).to eq_time(5.days.from_now)
|
||||
expect(topic_status_update.duration).to eq(5)
|
||||
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
expect(DateTime.parse(json['execute_at']))
|
||||
.to eq_time(DateTime.parse(topic_status_update.execute_at.to_s))
|
||||
|
||||
expect(json['duration']).to eq(topic_status_update.duration)
|
||||
end
|
||||
|
||||
describe 'publishing topic to category in the future' do
|
||||
it 'should be able to create the topic status update' do
|
||||
post "/t/#{topic.id}/timer.json", params: {
|
||||
|
|
Loading…
Reference in New Issue