mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
Add tabs to category create/edit modal. Categories can have a default auto-close setting that applies to all new topics created in the category. Add rspec-given and write some integration tests. Tests for topic auto-close with category default
This commit is contained in:
parent
737e16f7e0
commit
f3282e33a3
22 changed files with 298 additions and 98 deletions
1
Gemfile
1
Gemfile
|
@ -108,6 +108,7 @@ group :test, :development do
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
gem 'terminal-notifier-guard', require: false
|
gem 'terminal-notifier-guard', require: false
|
||||||
gem 'timecop'
|
gem 'timecop'
|
||||||
|
gem 'rspec-given'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
|
|
@ -371,6 +371,9 @@ GEM
|
||||||
rspec-core (2.13.1)
|
rspec-core (2.13.1)
|
||||||
rspec-expectations (2.13.0)
|
rspec-expectations (2.13.0)
|
||||||
diff-lcs (>= 1.1.3, < 2.0)
|
diff-lcs (>= 1.1.3, < 2.0)
|
||||||
|
rspec-given (2.4.1)
|
||||||
|
rspec (>= 2.11)
|
||||||
|
sorcerer (>= 0.3.7)
|
||||||
rspec-mocks (2.13.1)
|
rspec-mocks (2.13.1)
|
||||||
rspec-rails (2.13.0)
|
rspec-rails (2.13.0)
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 3.0)
|
||||||
|
@ -416,6 +419,7 @@ GEM
|
||||||
temple (~> 0.6.3)
|
temple (~> 0.6.3)
|
||||||
tilt (~> 1.3.3)
|
tilt (~> 1.3.3)
|
||||||
slop (3.4.4)
|
slop (3.4.4)
|
||||||
|
sorcerer (0.3.10)
|
||||||
spork (0.9.2)
|
spork (0.9.2)
|
||||||
temple (0.6.4)
|
temple (0.6.4)
|
||||||
terminal-notifier-guard (1.5.3)
|
terminal-notifier-guard (1.5.3)
|
||||||
|
@ -511,6 +515,7 @@ DEPENDENCIES
|
||||||
redis-rails
|
redis-rails
|
||||||
rest-client
|
rest-client
|
||||||
rinku
|
rinku
|
||||||
|
rspec-given
|
||||||
rspec-rails
|
rspec-rails
|
||||||
sanitize
|
sanitize
|
||||||
sass
|
sass
|
||||||
|
|
|
@ -45,7 +45,8 @@ Discourse.Category = Discourse.Model.extend({
|
||||||
text_color: this.get('text_color'),
|
text_color: this.get('text_color'),
|
||||||
hotness: this.get('hotness'),
|
hotness: this.get('hotness'),
|
||||||
secure: this.get('secure'),
|
secure: this.get('secure'),
|
||||||
group_names: this.get('groups').join(",")
|
group_names: this.get('groups').join(","),
|
||||||
|
auto_close_days: this.get('auto_close_days')
|
||||||
},
|
},
|
||||||
type: this.get('id') ? 'PUT' : 'POST'
|
type: this.get('id') ? 'PUT' : 'POST'
|
||||||
});
|
});
|
||||||
|
|
|
@ -538,13 +538,7 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
var reply = this.get('reply') || "";
|
var reply = this.get('reply') || "";
|
||||||
while (Discourse.BBCode.QUOTE_REGEXP.test(reply)) { reply = reply.replace(Discourse.BBCode.QUOTE_REGEXP, ""); }
|
while (Discourse.BBCode.QUOTE_REGEXP.test(reply)) { reply = reply.replace(Discourse.BBCode.QUOTE_REGEXP, ""); }
|
||||||
return reply.replace(/\s+/img, " ").trim().length;
|
return reply.replace(/\s+/img, " ").trim().length;
|
||||||
}.property('reply'),
|
}.property('reply')
|
||||||
|
|
||||||
autoCloseChanged: function() {
|
|
||||||
if( this.get('auto_close_days') && this.get('auto_close_days').length > 0 ) {
|
|
||||||
this.set('auto_close_days', this.get('auto_close_days').replace(/[^\d]/g, '') )
|
|
||||||
}
|
|
||||||
}.observes('auto_close_days')
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="auto-close-fields">
|
||||||
|
<i class="icon icon-time"></i>
|
||||||
|
{{view.label}}
|
||||||
|
{{view Discourse.TextField valueBinding="view.autoCloseDays" maxlength="3"}}
|
||||||
|
{{i18n composer.auto_close_units}}
|
||||||
|
</div>
|
|
@ -45,12 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-options-form">
|
<div class="admin-options-form">
|
||||||
<div class="auto-close-fields">
|
{{view Discourse.AutoCloseFormView autoCloseDaysBinding="content.auto_close_days"}}
|
||||||
<i class="icon icon-time"></i>
|
|
||||||
{{i18n composer.auto_close_label}}
|
|
||||||
{{view Discourse.TextField valueBinding="content.auto_close_days" maxlength="5"}}
|
|
||||||
{{i18n composer.auto_close_units}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form>
|
<form>
|
||||||
<div class="auto-close-fields">
|
{{view Discourse.AutoCloseFormView autoCloseDaysBinding="view.auto_close_days"}}
|
||||||
<i class="icon icon-time"></i>
|
|
||||||
{{i18n composer.auto_close_label}}
|
|
||||||
{{view Discourse.TextField valueBinding="view.auto_close_days" maxlength="5"}}
|
|
||||||
{{i18n composer.auto_close_units}}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
|
@ -1,28 +1,66 @@
|
||||||
{{#with view.category}}
|
{{#with view.category}}
|
||||||
|
|
||||||
<div {{bindAttr class="view.loading:invisible"}}>
|
<div {{bindAttr class="view.loading:invisible"}}>
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
<li {{bindAttr class="view.generalSelected:active"}}>
|
||||||
|
<a href="#" {{action selectGeneral target="view"}}>{{i18n category.general}}</a>
|
||||||
|
</li>
|
||||||
|
{{#unless is_uncategorized}}
|
||||||
|
<li {{bindAttr class="view.securitySelected:active"}}>
|
||||||
|
<a href="#" {{action selectSecurity target="view"}}>{{i18n category.security}}</a>
|
||||||
|
</li>
|
||||||
|
<li {{bindAttr class="view.settingsSelected:active"}}>
|
||||||
|
<a href="#" {{action selectSettings target="view"}}>{{i18n category.settings}}</a>
|
||||||
|
</li>
|
||||||
|
{{/unless}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form>
|
<div {{bindAttr class=":modal-tab :general-tab view.generalSelected::invisible"}}>
|
||||||
<section class='field'>
|
<form>
|
||||||
<label>{{i18n category.name}}</label>
|
|
||||||
{{view Discourse.TextField valueBinding="name" placeholderKey="category.name_placeholder" maxlength="50"}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{#unless is_uncategorized}}
|
|
||||||
<section class='field'>
|
<section class='field'>
|
||||||
<label>{{i18n category.description}}</label>
|
<label>{{i18n category.name}}</label>
|
||||||
|
{{view Discourse.TextField valueBinding="name" placeholderKey="category.name_placeholder" maxlength="50"}}
|
||||||
{{#if description}}
|
|
||||||
{{description}}
|
|
||||||
{{else}}
|
|
||||||
{{i18n category.no_description}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if topic_url}}
|
|
||||||
<br/>
|
|
||||||
<button class="btn btn-small" {{action showCategoryTopic target="view"}}>{{i18n category.change_in_category_topic}}</button>
|
|
||||||
{{/if}}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{#unless is_uncategorized}}
|
||||||
|
<section class='field'>
|
||||||
|
<label>{{i18n category.description}}</label>
|
||||||
|
|
||||||
|
{{#if description}}
|
||||||
|
{{description}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n category.no_description}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if topic_url}}
|
||||||
|
<br/>
|
||||||
|
<button class="btn btn-small" {{action showCategoryTopic target="view"}}>{{i18n category.change_in_category_topic}}</button>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
<section class='field'>
|
||||||
|
<label>{{i18n category.badge_colors}}</label>
|
||||||
|
<div class="category-color-editor">
|
||||||
|
<span class='badge-category' {{bindAttr style="view.colorStyle"}}>{{view.categoryName}}</span>
|
||||||
|
|
||||||
|
<div class='input-prepend input-append' style="margin-top: 10px;">
|
||||||
|
<span class='color-title'>{{i18n category.background_color}}:</span>
|
||||||
|
<span class='add-on'>#</span>{{view Discourse.TextField valueBinding="color" placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||||
|
{{view Discourse.ColorsView colorsBinding="view.backgroundColors" usedColorsBinding="view.usedBackgroundColors" valueBinding="color"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='input-prepend input-append'>
|
||||||
|
<span class='color-title'>{{i18n category.foreground_color}}:</span>
|
||||||
|
<span class='add-on'>#</span>{{view Discourse.TextField valueBinding="text_color" placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||||
|
{{view Discourse.ColorsView colorsBinding="view.foregroundColors" valueBinding="text_color"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{#unless is_uncategorized}}
|
||||||
|
<div {{bindAttr class=":modal-tab :options-tab view.securitySelected::invisible"}}>
|
||||||
<section class='field'>
|
<section class='field'>
|
||||||
<label>{{i18n category.security}}</label>
|
<label>{{i18n category.security}}</label>
|
||||||
<label>
|
<label>
|
||||||
|
@ -40,35 +78,18 @@
|
||||||
<button {{action addGroup target="view"}}>{{i18n category.add_group}}</button>
|
<button {{action addGroup target="view"}}>{{i18n category.add_group}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
<div {{bindAttr class=":modal-tab :options-tab view.settingsSelected::invisible"}}>
|
||||||
|
<section class='field'>
|
||||||
|
{{view Discourse.AutoCloseFormView autoCloseDaysBinding="auto_close_days" labelKey="category.auto_close_label"}}
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Sam, disabled for now
|
|
||||||
<section class='field'>
|
<section class='field'>
|
||||||
<label>{{i18n category.hotness}}</label>
|
<label>{{i18n category.hotness}}</label>
|
||||||
{{view Discourse.HotnessView hotnessBinding="hotness"}}
|
{{view Discourse.HotnessView hotnessBinding="hotness"}}
|
||||||
</section>
|
</section>
|
||||||
-->
|
</div>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
<section class='field'>
|
|
||||||
<label>{{i18n category.badge_colors}}</label>
|
|
||||||
<div class="category-color-editor">
|
|
||||||
<span class='badge-category' {{bindAttr style="view.colorStyle"}}>{{view.categoryName}}</span>
|
|
||||||
|
|
||||||
<div class='input-prepend input-append' style="margin-top: 10px;">
|
|
||||||
<span class='color-title'>{{i18n category.background_color}}:</span>
|
|
||||||
<span class='add-on'>#</span>{{view Discourse.TextField valueBinding="color" placeholderKey="category.color_placeholder" maxlength="6"}}
|
|
||||||
{{view Discourse.ColorsView colorsBinding="view.backgroundColors" usedColorsBinding="view.usedBackgroundColors" valueBinding="color"}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='input-prepend input-append'>
|
|
||||||
<span class='color-title'>{{i18n category.foreground_color}}:</span>
|
|
||||||
<span class='add-on'>#</span>{{view Discourse.TextField valueBinding="text_color" placeholderKey="category.color_placeholder" maxlength="6"}}
|
|
||||||
{{view Discourse.ColorsView colorsBinding="view.foregroundColors" valueBinding="text_color"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class='btn btn-primary' {{bindAttr disabled="view.disabled"}} {{action saveCategory target="view"}}>{{view.buttonTitle}}</button>
|
<button class='btn btn-primary' {{bindAttr disabled="view.disabled"}} {{action saveCategory target="view"}}>{{view.buttonTitle}}</button>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
This view renders the form to set or change a topic or category's auto-close setting.
|
||||||
|
|
||||||
|
@class AutoCloseFormView
|
||||||
|
@extends Ember.View
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AutoCloseFormView = Ember.View.extend({
|
||||||
|
templateName: 'auto_close_form',
|
||||||
|
|
||||||
|
label: function() {
|
||||||
|
return Em.String.i18n( this.get('labelKey') || 'composer.auto_close_label' );
|
||||||
|
}.property('labelKey'),
|
||||||
|
|
||||||
|
autoCloseChanged: function() {
|
||||||
|
if( this.get('autoCloseDays') && this.get('autoCloseDays').length > 0 ) {
|
||||||
|
this.set('autoCloseDays', this.get('autoCloseDays').replace(/[^\d]/g, '') )
|
||||||
|
}
|
||||||
|
}.observes('autoCloseDays')
|
||||||
|
});
|
|
@ -8,11 +8,32 @@
|
||||||
**/
|
**/
|
||||||
Discourse.EditCategoryView = Discourse.ModalBodyView.extend({
|
Discourse.EditCategoryView = Discourse.ModalBodyView.extend({
|
||||||
templateName: 'modal/edit_category',
|
templateName: 'modal/edit_category',
|
||||||
appControllerBinding: 'Discourse.appController',
|
generalSelected: Ember.computed.equal('selectedTab', 'general'),
|
||||||
|
securitySelected: Ember.computed.equal('selectedTab', 'security'),
|
||||||
// black & white only for foreground colors
|
settingsSelected: Ember.computed.equal('selectedTab', 'settings'),
|
||||||
foregroundColors: ['FFFFFF', '000000'],
|
foregroundColors: ['FFFFFF', '000000'],
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
this._super();
|
||||||
|
this.set('selectedTab', 'general');
|
||||||
|
},
|
||||||
|
|
||||||
|
modalClass: function() {
|
||||||
|
return "edit-category-modal " + (this.present('category.description') ? 'full' : 'small');
|
||||||
|
}.property(),
|
||||||
|
|
||||||
|
selectGeneral: function() {
|
||||||
|
this.set('selectedTab', 'general');
|
||||||
|
},
|
||||||
|
|
||||||
|
selectSecurity: function() {
|
||||||
|
this.set('selectedTab', 'security');
|
||||||
|
},
|
||||||
|
|
||||||
|
selectSettings: function() {
|
||||||
|
this.set('selectedTab', 'settings');
|
||||||
|
},
|
||||||
|
|
||||||
disabled: function() {
|
disabled: function() {
|
||||||
if (this.get('saving') || this.get('deleting')) return true;
|
if (this.get('saving') || this.get('deleting')) return true;
|
||||||
if (!this.get('category.name')) return true;
|
if (!this.get('category.name')) return true;
|
||||||
|
|
|
@ -192,3 +192,17 @@
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
width: 95% !important;
|
width: 95% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-category-modal {
|
||||||
|
.modal-body {
|
||||||
|
position: relative;
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
&.small .modal-body {
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-tab {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ class CategoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def category_param_keys
|
def category_param_keys
|
||||||
[required_param_keys, :hotness, :secure, :group_names].flatten!
|
[required_param_keys, :hotness, :secure, :group_names, :auto_close_days].flatten!
|
||||||
end
|
end
|
||||||
|
|
||||||
def category_params
|
def category_params
|
||||||
|
|
|
@ -95,6 +95,10 @@ class Topic < ActiveRecord::Base
|
||||||
before_create do
|
before_create do
|
||||||
self.bumped_at ||= Time.now
|
self.bumped_at ||= Time.now
|
||||||
self.last_post_user_id ||= user_id
|
self.last_post_user_id ||= user_id
|
||||||
|
if !@ignore_category_auto_close and self.category and self.category.auto_close_days and self.auto_close_at.nil?
|
||||||
|
self.auto_close_at = self.category.auto_close_days.days.from_now
|
||||||
|
self.auto_close_user = (self.user.staff? ? self.user : Discourse.system_user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
after_create do
|
after_create do
|
||||||
|
@ -112,6 +116,7 @@ class Topic < ActiveRecord::Base
|
||||||
before_save do
|
before_save do
|
||||||
if (auto_close_at_changed? and !auto_close_at_was.nil?) or (auto_close_user_id_changed? and auto_close_at)
|
if (auto_close_at_changed? and !auto_close_at_was.nil?) or (auto_close_user_id_changed? and auto_close_at)
|
||||||
Jobs.cancel_scheduled_job(:close_topic, {topic_id: id})
|
Jobs.cancel_scheduled_job(:close_topic, {topic_id: id})
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -736,6 +741,7 @@ class Topic < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def auto_close_days=(num_days)
|
def auto_close_days=(num_days)
|
||||||
|
@ignore_category_auto_close = true
|
||||||
self.auto_close_at = (num_days and num_days.to_i > 0.0 ? num_days.to_i.days.from_now : nil)
|
self.auto_close_at = (num_days and num_days.to_i > 0.0 ? num_days.to_i.days.from_now : nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class CategorySerializer < BasicCategorySerializer
|
class CategorySerializer < BasicCategorySerializer
|
||||||
|
|
||||||
attributes :secure, :groups, :available_groups
|
attributes :secure, :groups, :available_groups, :auto_close_days
|
||||||
|
|
||||||
def groups
|
def groups
|
||||||
@groups ||= object.groups.order("name").all.map(&:name)
|
@groups ||= object.groups.order("name").all.map(&:name)
|
||||||
|
|
|
@ -775,6 +775,8 @@ en:
|
||||||
edit_long: "Edit Category"
|
edit_long: "Edit Category"
|
||||||
edit_uncategorized: "Edit Uncategorized"
|
edit_uncategorized: "Edit Uncategorized"
|
||||||
view: 'View Topics in Category'
|
view: 'View Topics in Category'
|
||||||
|
general: 'General'
|
||||||
|
settings: 'Settings'
|
||||||
delete: 'Delete Category'
|
delete: 'Delete Category'
|
||||||
create: 'Create Category'
|
create: 'Create Category'
|
||||||
save: 'Save Category'
|
save: 'Save Category'
|
||||||
|
@ -800,6 +802,7 @@ en:
|
||||||
add_group: "Add Group"
|
add_group: "Add Group"
|
||||||
security: "Security"
|
security: "Security"
|
||||||
allowed_groups: "Allowed Groups:"
|
allowed_groups: "Allowed Groups:"
|
||||||
|
auto_close_label: "Auto-close topics after:"
|
||||||
|
|
||||||
|
|
||||||
flagging:
|
flagging:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddAutoCloseDaysToCategories < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :categories, :auto_close_days, :float
|
||||||
|
end
|
||||||
|
end
|
23
lib/jobs.rb
23
lib/jobs.rb
|
@ -103,15 +103,19 @@ module Jobs
|
||||||
enqueue_in( [(datetime - Time.zone.now).to_i, 0].max, job_name, opts )
|
enqueue_in( [(datetime - Time.zone.now).to_i, 0].max, job_name, opts )
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: should take job_name like enqueue methods
|
|
||||||
def self.cancel_scheduled_job(job_name, params={})
|
def self.cancel_scheduled_job(job_name, params={})
|
||||||
|
jobs = scheduled_for(job_name, params)
|
||||||
|
return false if jobs.empty?
|
||||||
|
jobs.each { |job| job.delete }
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.scheduled_for(job_name, params={})
|
||||||
job_class = "Jobs::#{job_name.to_s.camelcase}"
|
job_class = "Jobs::#{job_name.to_s.camelcase}"
|
||||||
matched = true
|
Sidekiq::ScheduledSet.new.select do |scheduled_job|
|
||||||
Sidekiq::ScheduledSet.new.each do |scheduled_job|
|
|
||||||
if scheduled_job.klass == 'Sidekiq::Extensions::DelayedClass'
|
if scheduled_job.klass == 'Sidekiq::Extensions::DelayedClass'
|
||||||
job_args = YAML.load(scheduled_job.args[0])
|
job_args = YAML.load(scheduled_job.args[0])
|
||||||
if job_args[0] == job_class
|
if job_args[0].to_s == job_class and job_args[2] and job_args[2][0]
|
||||||
next unless job_args[2] and job_args[2][0]
|
|
||||||
matched = true
|
matched = true
|
||||||
params.each do |key, value|
|
params.each do |key, value|
|
||||||
unless job_args[2][0][key] == value
|
unless job_args[2][0][key] == value
|
||||||
|
@ -119,13 +123,14 @@ module Jobs
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
next unless matched
|
matched
|
||||||
|
else
|
||||||
|
false
|
||||||
end
|
end
|
||||||
scheduled_job.delete
|
else
|
||||||
break
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
matched
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ module Jobs
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
topic = Topic.find(args[:topic_id])
|
topic = Topic.find(args[:topic_id])
|
||||||
if topic.auto_close_at
|
if topic and topic.auto_close_at and !topic.closed? and !topic.deleted_at
|
||||||
closer = User.find(args[:user_id])
|
closer = User.find(args[:user_id])
|
||||||
if Guardian.new(closer).can_moderate?(topic)
|
if Guardian.new(closer).can_moderate?(topic)
|
||||||
topic.update_status('autoclosed', true, closer)
|
topic.update_status('autoclosed', true, closer)
|
||||||
|
|
|
@ -13,23 +13,34 @@ describe Jobs::CloseTopic do
|
||||||
Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
|
Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does nothing if the topic is not set to auto-close' do
|
shared_examples_for "cases when CloseTopic does nothing" do
|
||||||
topic = Fabricate.build(:topic, auto_close_at: nil, user: admin)
|
it 'does nothing to the topic' do
|
||||||
topic.expects(:update_status).never
|
topic.expects(:update_status).never
|
||||||
Topic.stubs(:find).returns(topic)
|
Topic.stubs(:find).returns(topic)
|
||||||
User.stubs(:find).returns(admin)
|
User.stubs(:find).returns(admin)
|
||||||
Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
|
Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does nothing if the user is not authorized to close the topic' do
|
context 'when topic is not set to auto-close' do
|
||||||
topic = Fabricate.build(:topic, auto_close_at: Time.zone.now, user: admin)
|
subject(:topic) { Fabricate.build(:topic, auto_close_at: nil, user: admin) }
|
||||||
topic.expects(:update_status).never
|
it_behaves_like 'cases when CloseTopic does nothing'
|
||||||
Topic.stubs(:find).returns(topic)
|
|
||||||
User.stubs(:find).returns(admin)
|
|
||||||
Guardian.any_instance.stubs(:can_moderate?).returns(false)
|
|
||||||
Jobs::CloseTopic.new.execute( topic_id: 123, user_id: 234 )
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does nothing if the topic is already closed'
|
context 'when user is not authorized to close topics' do
|
||||||
|
subject(:topic) { Fabricate.build(:topic, auto_close_at: 2.days.from_now, user: admin) }
|
||||||
|
before { Guardian.any_instance.stubs(:can_moderate?).returns(false) }
|
||||||
|
it_behaves_like 'cases when CloseTopic does nothing'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the topic is already closed' do
|
||||||
|
subject(:topic) { Fabricate.build(:topic, auto_close_at: 2.days.from_now, user: admin, closed: true) }
|
||||||
|
it_behaves_like 'cases when CloseTopic does nothing'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the topic has been deleted' do
|
||||||
|
subject(:topic) { Fabricate.build(:deleted_topic, auto_close_at: 2.days.from_now, user: admin) }
|
||||||
|
it_behaves_like 'cases when CloseTopic does nothing'
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
0
spec/integration/.gitkeep
Normal file
0
spec/integration/.gitkeep
Normal file
85
spec/integration/topic_auto_close_spec.rb
Normal file
85
spec/integration/topic_auto_close_spec.rb
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'sidekiq/testing'
|
||||||
|
|
||||||
|
describe Topic do
|
||||||
|
|
||||||
|
def scheduled_jobs_for(job_name, params={})
|
||||||
|
Sidekiq::Extensions::DelayedClass.jobs.select do |job|
|
||||||
|
job_args = YAML.load(job['args'][0])
|
||||||
|
if job_args[0].to_s == "Jobs::#{job_name.to_s.camelcase}" and job_args[2] and job_args[2][0]
|
||||||
|
matched = true
|
||||||
|
params.each do |key, value|
|
||||||
|
unless job_args[2][0][key] == value
|
||||||
|
matched = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
matched
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
before { SiteSetting.stubs(:queue_jobs).returns(true) }
|
||||||
|
|
||||||
|
context 'creating a topic without auto-close' do
|
||||||
|
Given(:topic) { Fabricate(:topic, category: category) }
|
||||||
|
|
||||||
|
context 'uncategorized' do
|
||||||
|
Given(:category) { nil }
|
||||||
|
Then { topic.auto_close_at.should be_nil }
|
||||||
|
And { scheduled_jobs_for(:close_topic).should be_empty }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'category without default auto-close' do
|
||||||
|
Given(:category) { Fabricate(:category, auto_close_days: nil) }
|
||||||
|
Then { topic.auto_close_at.should be_nil }
|
||||||
|
And { scheduled_jobs_for(:close_topic).should be_empty }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'jobs may be queued' do
|
||||||
|
before do
|
||||||
|
Timecop.freeze(Time.zone.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Timecop.return
|
||||||
|
Sidekiq::Extensions::DelayedClass.jobs.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'category has a default auto-close' do
|
||||||
|
Given(:category) { Fabricate(:category, auto_close_days: 2.0) }
|
||||||
|
Then { topic.auto_close_at.should == 2.days.from_now }
|
||||||
|
And { scheduled_jobs_for(:close_topic, {topic_id: topic.id}).should have(1).job }
|
||||||
|
And { scheduled_jobs_for(:close_topic, {topic_id: category.topic.id}).should be_empty }
|
||||||
|
|
||||||
|
context 'topic was created by staff user' do
|
||||||
|
Given(:admin) { Fabricate(:admin) }
|
||||||
|
Given(:staff_topic) { Fabricate(:topic, user: admin, category: category) }
|
||||||
|
Then { scheduled_jobs_for(:close_topic, {topic_id: staff_topic.id, user_id: admin.id}).should have(1).job }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'topic was created by a non-staff user' do
|
||||||
|
Given!(:system_user) { Fabricate(:admin) }
|
||||||
|
Given { Discourse.stubs(:system_user).returns(system_user) }
|
||||||
|
Given(:regular_user) { Fabricate(:user) }
|
||||||
|
Given(:regular_user_topic) { Fabricate(:topic, user: regular_user, category: category) }
|
||||||
|
Then { scheduled_jobs_for(:close_topic, {topic_id: regular_user_topic.id, user_id: system_user.id}).should have(1).job }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'auto_close_days of topic was set to 0' do
|
||||||
|
Given(:dont_close_topic) { Fabricate(:topic, auto_close_days: 0, category: category) }
|
||||||
|
Then { scheduled_jobs_for(:close_topic).should be_empty }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'two topics in the category' do
|
||||||
|
Given!(:other_topic) { Fabricate(:topic, category: category) }
|
||||||
|
When { topic } # create the second topic
|
||||||
|
Then { scheduled_jobs_for(:close_topic).should have(2).jobs }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -985,11 +985,6 @@ describe Topic do
|
||||||
|
|
||||||
describe 'auto-close' do
|
describe 'auto-close' do
|
||||||
context 'a new topic' do
|
context 'a new topic' do
|
||||||
it 'when auto_close_at is not present, it does not queue a job to close the topic' do
|
|
||||||
Jobs.expects(:enqueue_at).never
|
|
||||||
Fabricate(:topic)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'auto_close_at is set' do
|
context 'auto_close_at is set' do
|
||||||
it 'queues a job to close the topic' do
|
it 'queues a job to close the topic' do
|
||||||
Timecop.freeze(Time.zone.now) do
|
Timecop.freeze(Time.zone.now) do
|
||||||
|
@ -1014,6 +1009,13 @@ describe Topic do
|
||||||
end
|
end
|
||||||
Fabricate(:topic, auto_close_at: 7.days.from_now, auto_close_user: topic_closer, user: topic_creator)
|
Fabricate(:topic, auto_close_at: 7.days.from_now, auto_close_user: topic_closer, user: topic_creator)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "ignores the category's default auto-close" do
|
||||||
|
Timecop.freeze(Time.zone.now) do
|
||||||
|
Jobs.expects(:enqueue_at).with(7.days.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
|
||||||
|
Fabricate(:topic, auto_close_at: 7.days.from_now, user: Fabricate(:admin), category: Fabricate(:category, auto_close_days: 2))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1090,6 +1092,15 @@ describe Topic do
|
||||||
topic.save.should be_true
|
topic.save.should be_true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "ignores the category's default auto-close" do
|
||||||
|
Timecop.freeze(Time.zone.now) do
|
||||||
|
topic = Fabricate(:topic, category: Fabricate(:category, auto_close_days: 14))
|
||||||
|
Jobs.expects(:enqueue_at).with(12.hours.from_now, :close_topic, has_entries(topic_id: topic.id, user_id: topic.user_id))
|
||||||
|
topic.auto_close_at = 12.hours.from_now
|
||||||
|
topic.save.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue