diff --git a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js index 4d2be762d3d..361a2acbf28 100644 --- a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js +++ b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js @@ -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); - } } }); diff --git a/app/assets/javascripts/discourse/components/future-date-input.js b/app/assets/javascripts/discourse/components/future-date-input.js index d84ab3c3340..d54e68e7826 100644 --- a/app/assets/javascripts/discourse/components/future-date-input.js +++ b/app/assets/javascripts/discourse/components/future-date-input.js @@ -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; } diff --git a/app/assets/javascripts/discourse/components/topic-timer-info.js b/app/assets/javascripts/discourse/components/topic-timer-info.js index 66663b062dd..30eb177ca39 100644 --- a/app/assets/javascripts/discourse/components/topic-timer-info.js +++ b/app/assets/javascripts/discourse/components/topic-timer-info.js @@ -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; diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js index e9117532f05..461fa3c64d1 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js +++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js @@ -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({ - id: DELETE_STATUS_TYPE, - name: I18n.t("topic.auto_delete.title") - }); + 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")); } } }); diff --git a/app/assets/javascripts/discourse/models/topic-timer.js b/app/assets/javascripts/discourse/models/topic-timer.js index 31b51ae9003..1afe0e92dc3 100644 --- a/app/assets/javascripts/discourse/models/topic-timer.js +++ b/app/assets/javascripts/discourse/models/topic-timer.js @@ -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`, diff --git a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs index 1fbfa3f9797..e5ad76f9794 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs @@ -7,46 +7,32 @@ value=selection }} - -
- {{#if showTimeOnly}} + {{#if publishToCategory}} +
+ + {{category-chooser + value=topicTimer.category_id + excludeCategoryId=excludeCategoryId + onChange=(action (mut topicTimer.category_id)) + }} +
+ {{/if}} + {{#if showFutureDateInput}} +
{{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=onChangeUpdateTime - }} - {{else if publishToCategory}} -
- - {{category-chooser - value=topicTimer.category_id - excludeCategoryId=excludeCategoryId - onChange=(action (mut topicTimer.category_id)) - }} -
- - {{future-date-input - input=(readonly topicTimer.updateTime) - label="topic.topic_status_update.when" - statusType=selection - includeWeekend=true + 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}} -
+
+ {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs index c1571ad9c58..38451bea90e 100644 --- a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs @@ -1,4 +1,5 @@
+ {{#unless isBasedOnDuration}}
{{future-date-input-selector @@ -16,6 +17,7 @@ onChange=(action (mut selection)) }}
+ {{/unless}} {{#if displayDateAndTimePicker}}
@@ -33,11 +35,11 @@
{{/if}} - {{#if isBasedOnLastPost}} + {{#if displayNumberInput}}
diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs index 71d74321139..316e51b00f8 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs @@ -17,7 +17,8 @@ timerTypes=selections updateTime=updateTime onChangeStatusType=(action "onChangeStatusType") - onChangeUpdateTime=(action "onChangeUpdateTime") + onChangeInput=(action "onChangeInput") + onChangeDuration=(action "onChangeDuration") }} {{/d-modal-body}} diff --git a/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 index 0c588dbdfed..34efa2551c5 100644 --- a/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 @@ -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, diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index ee7ba1af180..0deb6b2efdb 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -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, diff --git a/app/jobs/regular/delete_replies.rb b/app/jobs/regular/delete_replies.rb new file mode 100644 index 00000000000..b8228c7a047 --- /dev/null +++ b/app/jobs/regular/delete_replies.rb @@ -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 diff --git a/app/models/topic.rb b/app/models/topic.rb index 7947900b133..13f3a8a798b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -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) diff --git a/app/models/topic_timer.rb b/app/models/topic_timer.rb index 1599f77ce54..c8274da9f56 100644 --- a/app/models/topic_timer.rb +++ b/app/models/topic_timer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ef241f4e166..27d207efb74 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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." diff --git a/db/migrate/20200312122846_add_duration_to_topic_timers.rb b/db/migrate/20200312122846_add_duration_to_topic_timers.rb new file mode 100644 index 00000000000..bc4ba1a35f4 --- /dev/null +++ b/db/migrate/20200312122846_add_duration_to_topic_timers.rb @@ -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 diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 845a7e1930a..0f0a921e9e8 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -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: {