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