Merge pull request #4790 from tgxworld/add_publish_to_topic_status_update

FEATURE: Allow admins to schedule a topic to be published in the future.
This commit is contained in:
Guo Xiang Tan 2017-04-04 11:18:53 +08:00 committed by GitHub
commit ad44d2a400
23 changed files with 359 additions and 177 deletions

View File

@ -1,9 +1,12 @@
import computed from "ember-addons/ember-computed-decorators";
import { observes } from "ember-addons/ember-computed-decorators";
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
limited: false,
inputValid: false,
didInsertElement() {
this._super();
this._updateInputValid();
},
@computed("limited")
inputUnitsKey(limited) {

View File

@ -3,6 +3,7 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import computed from 'ember-addons/ember-computed-decorators';
import { observes, on } from 'ember-addons/ember-computed-decorators';
import PermissionType from 'discourse/models/permission-type';
import Category from 'discourse/models/category';
export default ComboboxView.extend({
classNames: ['combobox category-combobox'],
@ -14,13 +15,16 @@ export default ComboboxView.extend({
content(scopedCategoryId, categories) {
// Always scope to the parent of a category, if present
if (scopedCategoryId) {
const scopedCat = Discourse.Category.findById(scopedCategoryId);
const scopedCat = Category.findById(scopedCategoryId);
scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id');
}
const excludeCategoryId = this.get('excludeCategoryId');
return categories.filter(c => {
if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory')) { return false; }
const categoryId = c.get('id');
if (scopedCategoryId && categoryId !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory') || excludeCategoryId === categoryId) { return false; }
return c.get('permission') === PermissionType.FULL;
});
},
@ -30,8 +34,8 @@ export default ComboboxView.extend({
_updateCategories() {
if (!this.get('categories')) {
const categories = Discourse.SiteSettings.fixed_category_positions_on_create ?
Discourse.Category.list() :
Discourse.Category.listByActivity();
Category.list() :
Category.listByActivity();
this.set('categories', categories);
}
},
@ -42,7 +46,7 @@ export default ComboboxView.extend({
if (rootNone) {
return "category.none";
} else {
return Discourse.Category.findUncategorized();
return Category.findUncategorized();
}
} else {
return 'category.choose';
@ -54,12 +58,12 @@ export default ComboboxView.extend({
// If we have no id, but text with the uncategorized name, we can use that badge.
if (Ember.isEmpty(item.id)) {
const uncat = Discourse.Category.findUncategorized();
const uncat = Category.findUncategorized();
if (uncat && uncat.get('name') === item.text) {
category = uncat;
}
} else {
category = Discourse.Category.findById(parseInt(item.id,10));
category = Category.findById(parseInt(item.id,10));
}
if (!category) return item.text;
@ -67,7 +71,7 @@ export default ComboboxView.extend({
const parentCategoryId = category.get('parent_category_id');
if (parentCategoryId) {
result = categoryBadgeHTML(Discourse.Category.findById(parentCategoryId), {link: false}) + " " + result;
result = categoryBadgeHTML(Category.findById(parentCategoryId), {link: false}) + " " + result;
}
result += ` <span class='topic-count'>&times; ${category.get('topic_count')}</span>`;

View File

@ -1,4 +1,5 @@
import { bufferedRender } from 'discourse-common/lib/buffered-render';
import Category from 'discourse/models/category';
export default Ember.Component.extend(bufferedRender({
elementId: 'topic-status-info',
@ -8,7 +9,8 @@ export default Ember.Component.extend(bufferedRender({
'topic.topic_status_update',
'topic.topic_status_update.execute_at',
'topic.topic_status_update.based_on_last_post',
'topic.topic_status_update.duration'
'topic.topic_status_update.duration',
'topic.topic_status_update.category_id',
],
buildBuffer(buffer) {
@ -35,11 +37,23 @@ export default Ember.Component.extend(bufferedRender({
buffer.push('<h3><i class="fa fa-clock-o"></i> ');
buffer.push(I18n.t(this._noticeKey(), {
let options = {
timeLeft: duration.humanize(true),
duration: moment.duration(autoCloseHours, "hours").humanize()
}));
duration: moment.duration(autoCloseHours, "hours").humanize(),
};
const categoryId = this.get('topic.topic_status_update.category_id');
if (categoryId) {
const category = Category.findById(categoryId);
options = _.assign({
categoryName: category.get('slug'),
categoryUrl: category.get('url')
}, options);
}
buffer.push(I18n.t(this._noticeKey(), options));
buffer.push('</h3>');
// TODO Sam: concerned this can cause a heavy rerender loop

View File

@ -5,18 +5,36 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
const CLOSE_STATUS_TYPE = 'close';
const OPEN_STATUS_TYPE = 'open';
const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category';
export default Ember.Controller.extend(ModalFunctionality, {
closeStatusType: CLOSE_STATUS_TYPE,
openStatusType: OPEN_STATUS_TYPE,
publishToCategoryStatusType: PUBLISH_TO_CATEGORY_STATUS_TYPE,
updateTimeValid: null,
updateTimeInvalid: Em.computed.not('updateTimeValid'),
loading: false,
updateTime: null,
topicStatusUpdate: Ember.computed.alias("model.topic_status_update"),
selection: Ember.computed.alias('model.topic_status_update.status_type'),
autoReopen: Ember.computed.equal('selection', OPEN_STATUS_TYPE),
autoOpen: Ember.computed.equal('selection', OPEN_STATUS_TYPE),
autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE),
disableAutoReopen: Ember.computed.and('autoClose', 'updateTime'),
disableAutoClose: Ember.computed.and('autoReopen', 'updateTime'),
publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE),
@computed('autoClose', 'updateTime')
disableAutoClose(autoClose, updateTime) {
return updateTime && !autoClose;
},
@computed('autoOpen', 'updateTime')
disableAutoOpen(autoOpen, updateTime) {
return updateTime && !autoOpen;
},
@computed('publishToCatgory', 'updateTime')
disablePublishToCategory(publishToCatgory, updateTime) {
return updateTime && !publishToCatgory;
},
@computed('topicStatusUpdate.based_on_last_post', 'updateTime', 'model.last_posted_at')
willCloseImmediately(basedOnLastPost, updateTime, lastPostedAt) {
@ -42,7 +60,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
@observes("topicStatusUpdate.execute_at", "topicStatusUpdate.duration")
setAutoCloseTime() {
_setUpdateTime() {
let time = null;
if (this.get("topicStatusUpdate.based_on_last_post")) {
@ -65,12 +83,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.get('model.id'),
time,
this.get('topicStatusUpdate.based_on_last_post'),
status_type
status_type,
this.get('categoryId')
).then(result => {
if (time) {
this.send('closeModal');
this.set('topicStatusUpdate.execute_at', result.execute_at);
this.set('topicStatusUpdate.duration', result.duration);
this.get("topicStatusUpdate").setProperties({
execute_at: result.execute_at,
duration: result.duration,
category_id: result.category_id
});
this.set('model.closed', result.closed);
} else {
this.set('topicStatusUpdate', Ember.Object.create({}));

View File

@ -4,7 +4,7 @@ import RestModel from 'discourse/models/rest';
const TopicStatusUpdate = RestModel.extend({});
TopicStatusUpdate.reopenClass({
updateStatus(topicId, time, basedOnLastPost, statusType) {
updateStatus(topicId, time, basedOnLastPost, statusType, categoryId) {
let data = {
time: time,
timezone_offset: (new Date().getTimezoneOffset()),
@ -12,6 +12,7 @@ TopicStatusUpdate.reopenClass({
};
if (basedOnLastPost) data.based_on_last_post = basedOnLastPost;
if (categoryId) data.category_id = categoryId;
return ajax({
url: `/t/${topicId}/status_update`,

View File

@ -52,7 +52,7 @@ const TopicRoute = Discourse.Route.extend({
showTopicStatusUpdate() {
const model = this.modelFor('topic');
if (!model.get('topic_status_update')) model.set('topic_status_update', Ember.Object.create());
model.set('topic_status_update', Ember.Object.create(model.get('topic_status_update')));
showModal('edit-topic-status-update', { model });
this.controllerFor('modal').set('modalClass', 'topic-close-modal');
},

View File

@ -1,21 +1,20 @@
<div class="auto-update-input">
<div>
<div class="control-group">
<label>
{{i18n inputLabelKey}}
{{text-field value=input}}
{{i18n inputUnitsKey}}
</label>
{{#if inputExamplesKey}}
<div class="examples">
{{i18n inputExamplesKey}}
</div>
{{/if}}
</div>
{{#if inputExamplesKey}}
<div class="examples">
{{i18n inputExamplesKey}}
</div>
{{/if}}
{{#unless hideBasedOnLastPost}}
<div>
<div class="control-group">
<label>
{{input type="checkbox" checked=basedOnLastPost}}
{{i18n 'topic.auto_close.based_on_last_post'}}

View File

@ -5,7 +5,7 @@
disabled=disableAutoClose
name="auto-close"
id="auto-close"
value="close"
value=closeStatusType
selection=selection}}
<label class="radio" for="auto-close">
@ -19,46 +19,70 @@
</label>
{{radio-button
disabled=disableAutoReopen
disabled=disableAutoOpen
name="auto-reopen"
id="auto-reopen"
value="open"
value=openStatusType
selection=selection}}
<label class="radio" for="auto-reopen">
{{fa-icon "clock-o"}} {{fa-icon "unlock"}}
{{#if model.closed}}
{{i18n 'topic.auto_reopen.title'}}
{{else}}
{{i18n 'topic.temp_close.title'}}
{{/if}}
</label>
{{radio-button
disabled=disablePublishToCategory
name="publish-to-category"
id="publish-to-category"
value=publishToCategoryStatusType
selection=selection}}
<label class="radio" for="publish-to-category">
{{fa-icon "clock-o"}} {{i18n 'topic.publish_to_category.title'}}
</label>
</div>
{{#if autoReopen}}
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
hideBasedOnLastPost=true
basedOnLastPost=false}}
{{else if autoClose}}
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
limited=topicStatusUpdate.based_on_last_post
basedOnLastPost=topicStatusUpdate.based_on_last_post}}
{{#if willCloseImmediately}}
<div class="warning">
{{fa-icon "warning"}}
{{willCloseI18n}}
<div>
{{#if autoOpen}}
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
hideBasedOnLastPost=true
basedOnLastPost=false}}
{{else if publishToCategory}}
<div class="control-group">
<label>{{i18n 'topic.topic_status_update.publish_to'}}</label>
{{category-chooser valueAttribute="id" value=categoryId excludeCategoryId=model.category_id}}
</div>
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
hideBasedOnLastPost=true
basedOnLastPost=false}}
{{else if autoClose}}
{{auto-update-input
inputLabelKey='topic.topic_status_update.time'
input=updateTime
inputValid=updateTimeValid
limited=topicStatusUpdate.based_on_last_post
basedOnLastPost=topicStatusUpdate.based_on_last_post}}
{{#if willCloseImmediately}}
<div class="warning">
{{fa-icon "warning"}}
{{willCloseI18n}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
{{/d-modal-body}}
<div class="modal-footer">

View File

@ -134,12 +134,6 @@ div.ac-wrap {
}
.auto-update-input {
div:not(:first-child) {
margin-top: 10px;
}
label {
font-size: 1em;
}
.examples {
color: lighten($primary, 40%);
}

View File

@ -295,38 +295,6 @@
}
}
.topic-close-modal {
.radios {
padding-bottom: 20px;
display: inline-block;
input[type='radio'] {
vertical-align: middle;
margin: 0px;
}
label {
padding: 0 10px 0px 5px;
display: inline-block;
}
}
.btn.pull-right {
margin-right: 10px;
}
form {
margin: 0;
}
.auto-update-input {
i.fa-clock-o {
font-size: 1.143em;
}
input {
margin: 0;
}
}
}
.edit-category-modal {
.auto-update-input, .num-featured-topics-fields, .position-fields {
input[type=text] {
@ -334,10 +302,6 @@
}
}
.auto-update-input label {
font-size: .929em;
}
.subcategory-list-style-field {
margin-left: 16px;
}

View File

@ -0,0 +1,29 @@
.topic-close-modal {
label {
display: inline-block;
}
.radios {
padding-bottom: 20px;
display: inline-block;
input[type='radio'] {
vertical-align: middle;
margin: 0px;
}
label {
padding: 0 10px 0px 5px;
}
}
.btn.pull-right {
margin-right: 10px;
}
.auto-update-input {
input {
margin: 0;
}
}
}

View File

@ -290,7 +290,7 @@ class TopicsController < ApplicationController
end
def status_update
params.permit(:time, :timezone_offset, :based_on_last_post)
params.permit(:time, :timezone_offset, :based_on_last_post, :category_id)
params.require(:status_type)
status_type =
@ -303,12 +303,18 @@ class TopicsController < ApplicationController
topic = Topic.find_by(id: params[:topic_id])
guardian.ensure_can_moderate!(topic)
topic_status_update = topic.set_or_create_status_update(
status_type,
params[:time],
options = {
by_user: current_user,
timezone_offset: params[:timezone_offset]&.to_i,
based_on_last_post: params[:based_on_last_post]
}
options.merge!(category_id: params[:category_id]) if !params[:category_id].blank?
topic_status_update = topic.set_or_create_status_update(
status_type,
params[:time],
options
)
if topic.save
@ -316,7 +322,8 @@ class TopicsController < ApplicationController
execute_at: topic_status_update&.execute_at,
duration: topic_status_update&.duration,
based_on_last_post: topic_status_update&.based_on_last_post,
closed: topic.closed
closed: topic.closed,
category_id: topic_status_update&.category_id
})
else
render_json_error(topic)

View File

@ -0,0 +1,15 @@
module Jobs
class PublishTopicToCategory < Jobs::Base
def execute(args)
topic_status_update = TopicStatusUpdate.find_by(id: args[:topic_status_update_id])
raise Discourse::InvalidParameters.new(:topic_status_update_id) if topic_status_update.blank?
topic = topic_status_update.topic
return if topic.blank?
PostTimestampChanger.new(timestamp: Time.zone.now, topic: topic).change! do
topic.change_category_to_id(topic_status_update.category_id)
end
end
end
end

View File

@ -951,8 +951,10 @@ SQL
# * `nil` to delete the topic's status update.
# Options:
# * by_user: User who is setting the topic's status update.
# * timezone_offset: (Integer) offset from UTC in minutes of the given argument. Default 0.
def set_or_create_status_update(status_type, time, by_user: nil, timezone_offset: 0, based_on_last_post: false)
# * timezone_offset: (Integer) offset from UTC in minutes of the given argument.
# * 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_status_update(status_type, time, by_user: nil, timezone_offset: 0, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id)
topic_status_update = TopicStatusUpdate.find_or_initialize_by(
status_type: status_type,
topic: self
@ -966,6 +968,10 @@ SQL
time_now = Time.zone.now
topic_status_update.based_on_last_post = !based_on_last_post.blank?
if status_type == TopicStatusUpdate.types[:publish_to_category]
topic_status_update.category_id = category_id
end
if topic_status_update.based_on_last_post
num_hours = time.to_f
@ -1191,51 +1197,56 @@ end
#
# Table name: topics
#
# id :integer not null, primary key
# title :string not null
# last_posted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# views :integer default(0), not null
# posts_count :integer default(0), not null
# user_id :integer
# last_post_user_id :integer not null
# reply_count :integer default(0), not null
# featured_user1_id :integer
# featured_user2_id :integer
# featured_user3_id :integer
# avg_time :integer
# deleted_at :datetime
# highest_post_number :integer default(0), not null
# image_url :string
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# category_id :integer
# visible :boolean default(TRUE), not null
# moderator_posts_count :integer default(0), not null
# closed :boolean default(FALSE), not null
# archived :boolean default(FALSE), not null
# bumped_at :datetime not null
# has_summary :boolean default(FALSE), not null
# vote_count :integer default(0), not null
# archetype :string default("regular"), not null
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# pinned_at :datetime
# score :float
# percent_rank :float default(1.0), not null
# subtype :string
# slug :string
# deleted_by_id :integer
# participant_count :integer default(1)
# word_count :integer
# excerpt :string(1000)
# pinned_globally :boolean default(FALSE), not null
# pinned_until :datetime
# fancy_title :string(400)
# highest_staff_post_number :integer default(0), not null
# featured_link :string
# id :integer not null, primary key
# title :string not null
# last_posted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# views :integer default(0), not null
# posts_count :integer default(0), not null
# user_id :integer
# last_post_user_id :integer not null
# reply_count :integer default(0), not null
# featured_user1_id :integer
# featured_user2_id :integer
# featured_user3_id :integer
# avg_time :integer
# deleted_at :datetime
# highest_post_number :integer default(0), not null
# image_url :string
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# category_id :integer
# visible :boolean default(TRUE), not null
# moderator_posts_count :integer default(0), not null
# closed :boolean default(FALSE), not null
# archived :boolean default(FALSE), not null
# bumped_at :datetime not null
# has_summary :boolean default(FALSE), not null
# vote_count :integer default(0), not null
# archetype :string default("regular"), not null
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# pinned_at :datetime
# score :float
# percent_rank :float default(1.0), not null
# subtype :string
# slug :string
# auto_close_at :datetime
# auto_close_user_id :integer
# auto_close_started_at :datetime
# deleted_by_id :integer
# participant_count :integer default(1)
# word_count :integer
# excerpt :string(1000)
# pinned_globally :boolean default(FALSE), not null
# auto_close_based_on_last_post :boolean default(FALSE)
# auto_close_hours :float
# pinned_until :datetime
# fancy_title :string(400)
# highest_staff_post_number :integer default(0), not null
# featured_link :string
#
# Indexes
#

View File

@ -8,9 +8,7 @@ class TopicStatusUpdate < ActiveRecord::Base
validates :topic_id, presence: true
validates :execute_at, presence: true
validates :status_type, presence: true
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }
validate :ensure_update_will_happen
before_save do
@ -22,7 +20,7 @@ class TopicStatusUpdate < ActiveRecord::Base
end
after_save do
if execute_at_changed? || user_id_changed?
if (execute_at_changed? || user_id_changed?) && topic
now = Time.zone.now
time = execute_at < now ? now : execute_at
@ -33,7 +31,8 @@ class TopicStatusUpdate < ActiveRecord::Base
def self.types
@types ||= Enum.new(
close: 1,
open: 2
open: 2,
publish_to_category: 3
)
end
@ -69,26 +68,30 @@ class TopicStatusUpdate < ActiveRecord::Base
end
alias_method :cancel_auto_open_job, :cancel_auto_close_job
def schedule_auto_open_job(time)
if topic
topic.update_status('closed', true, user) if !topic.closed
def cancel_auto_publish_to_category_job
Jobs.cancel_scheduled_job(:publish_topic_to_category, topic_status_update_id: id)
end
Jobs.enqueue_at(time, :toggle_topic_closed,
topic_status_update_id: id,
state: false
)
end
def schedule_auto_open_job(time)
topic.update_status('closed', true, user) if !topic.closed
Jobs.enqueue_at(time, :toggle_topic_closed,
topic_status_update_id: id,
state: false
)
end
def schedule_auto_close_job(time)
if topic
topic.update_status('closed', false, user) if topic.closed
topic.update_status('closed', false, user) if topic.closed
Jobs.enqueue_at(time, :toggle_topic_closed,
topic_status_update_id: id,
state: true
)
end
Jobs.enqueue_at(time, :toggle_topic_closed,
topic_status_update_id: id,
state: true
)
end
def schedule_auto_publish_to_category_job(time)
Jobs.enqueue_at(time, :publish_topic_to_category, topic_status_update_id: id)
end
end
@ -106,6 +109,7 @@ end
# deleted_by_id :integer
# created_at :datetime
# updated_at :datetime
# category_id :integer
#
# Indexes
#

View File

@ -3,7 +3,8 @@ class TopicStatusUpdateSerializer < ApplicationSerializer
:execute_at,
:duration,
:based_on_last_post,
:status_type
:status_type,
:category_id
def status_type
TopicStatusUpdate.types[object.status_type]

View File

@ -1,6 +1,6 @@
class PostTimestampChanger
def initialize(params)
@topic = Topic.with_deleted.find(params[:topic_id])
@topic = params[:topic] || Topic.with_deleted.find(params[:topic_id])
@posts = @topic.posts
@timestamp = Time.at(params[:timestamp])
@time_difference = calculate_time_difference
@ -21,6 +21,8 @@ class PostTimestampChanger
end
update_topic(last_posted_at)
yield(@topic) if block_given?
end
# Burst the cache for stats

View File

@ -1474,6 +1474,7 @@ en:
save: "Set Timer"
time: "Time:"
remove: "Remove Timer"
publish_to: "Publish To:"
auto_update_input:
limited:
units: "(# of hours)"
@ -1481,6 +1482,8 @@ en:
all:
units: ""
examples: 'Enter number of hours (24), absolute time (17:30) or timestamp (2013-11-22 14:00).'
publish_to_category:
title: "Schedule Publishing"
temp_open:
title: "Open Temporarily"
auto_reopen:
@ -1496,7 +1499,7 @@ en:
status_update_notice:
auto_open: "This topic will automatically open %{timeLeft}."
auto_close: "This topic will automatically close %{timeLeft}."
auto_open_based_on_last_post: "This topic will open %{duration} after the last reply."
auto_publish_to_category: "This topic will be published to <a href=%{categoryUrl}>#%{categoryName}</a> %{timeLeft}."
auto_close_based_on_last_post: "This topic will close %{duration} after the last reply."
auto_close_title: 'Auto-Close Settings'
auto_close_immediate:

View File

@ -0,0 +1,5 @@
class AddCategoryIdToTopicStatusUpdates < ActiveRecord::Migration
def change
add_column :topic_status_updates, :category_id, :integer
end
end

View File

@ -260,6 +260,9 @@ describe PostCreator do
end
describe "topic's auto close" do
before do
SiteSetting.queue_jobs = true
end
it "doesn't update topic's auto close when it's not based on last post" do
Timecop.freeze do
@ -275,8 +278,6 @@ describe PostCreator do
end
it "updates topic's auto close date when it's based on last post" do
SiteSetting.queue_jobs = true
Timecop.freeze do
topic = Fabricate(:topic,
topic_status_updates: [Fabricate(:topic_status_update,

View File

@ -76,6 +76,31 @@ RSpec.describe "Managing a topic's status update", type: :request do
expect(json['closed']).to eq(topic.closed)
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}/status_update.json",
time: 24,
status_type: TopicStatusUpdate.types[3],
category_id: topic.category_id
expect(response).to be_success
topic_status_update = TopicStatusUpdate.last
expect(topic_status_update.topic).to eq(topic)
expect(topic_status_update.execute_at)
.to be_within(1.second).of(24.hours.from_now)
expect(topic_status_update.status_type)
.to eq(TopicStatusUpdate.types[:publish_to_category])
json = JSON.parse(response.body)
expect(json['category_id']).to eq(topic.category_id)
end
end
describe 'invalid status type' do
it 'should raise the right error' do
expect do

View File

@ -0,0 +1,52 @@
require 'rails_helper'
RSpec.describe Jobs::PublishTopicToCategory do
let(:category) { Fabricate(:category) }
let(:another_category) { Fabricate(:category) }
let(:topic) do
Fabricate(:topic, category: category, topic_status_updates: [
Fabricate(:topic_status_update,
status_type: TopicStatusUpdate.types[:publish_to_category],
category_id: another_category.id
)
])
end
before do
SiteSetting.queue_jobs = true
end
describe 'when topic_status_update_id is invalid' do
it 'should raise the right error' do
expect { described_class.new.execute(topic_status_update_id: -1) }
.to raise_error(Discourse::InvalidParameters)
end
end
describe 'when topic has been deleted' do
it 'should not publish the topic to the new category' do
Timecop.travel(1.hour.ago) { topic }
topic.trash!
described_class.new.execute(topic_status_update_id: topic.topic_status_update.id)
topic.reload
expect(topic.category).to eq(category)
expect(topic.created_at).to be_within(1.second).of(Time.zone.now - 1.hour)
end
end
it 'should publish the topic to the new category correctly' do
Timecop.travel(1.hour.ago) { topic }
described_class.new.execute(topic_status_update_id: topic.topic_status_update.id)
topic.reload
expect(topic.category).to eq(another_category)
%w{created_at bumped_at updated_at last_posted_at}.each do |attribute|
expect(topic.public_send(attribute)).to be_within(1.second).of(Time.zone.now)
end
end
end

View File

@ -1265,7 +1265,7 @@ describe Topic do
expect(topic.reload.closed).to eq(false)
Timecop.freeze(3.hours.from_now) do
Timecop.travel(3.hours.from_now) do
TopicStatusUpdate.ensure_consistency!
expect(topic.reload.closed).to eq(true)
end