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:
commit
ad44d2a400
|
@ -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) {
|
||||
|
|
|
@ -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'>× ${category.get('topic_count')}</span>`;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({}));
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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'}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddCategoryIdToTopicStatusUpdates < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :topic_status_updates, :category_id, :integer
|
||||
end
|
||||
end
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue