diff --git a/app/assets/javascripts/discourse/app/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/app/components/edit-category-settings.hbs
index 647ce9c3fd..2a36e0531d 100644
--- a/app/assets/javascripts/discourse/app/components/edit-category-settings.hbs
+++ b/app/assets/javascripts/discourse/app/components/edit-category-settings.hbs
@@ -193,6 +193,18 @@
@min="0"
/>
+
+
+
+
+
diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js
index 8b3d3d7d96..839fa6e7d2 100644
--- a/app/assets/javascripts/discourse/app/models/category.js
+++ b/app/assets/javascripts/discourse/app/models/category.js
@@ -219,6 +219,7 @@ const Category = RestModel.extend({
uploaded_logo_dark_id: this.get("uploaded_logo_dark.id"),
uploaded_background_id: this.get("uploaded_background.id"),
allow_badges: this.allow_badges,
+ category_setting_attributes: this.category_setting,
custom_fields: this.custom_fields,
topic_template: this.topic_template,
form_template_ids: this.form_template_ids,
diff --git a/app/assets/javascripts/discourse/app/routes/new-category.js b/app/assets/javascripts/discourse/app/routes/new-category.js
index 2fb6237719..7dbead0fad 100644
--- a/app/assets/javascripts/discourse/app/routes/new-category.js
+++ b/app/assets/javascripts/discourse/app/routes/new-category.js
@@ -31,6 +31,7 @@ export default DiscourseRoute.extend({
allow_badges: true,
topic_featured_link_allowed: true,
custom_fields: {},
+ category_setting: {},
search_priority: SEARCH_PRIORITIES.normal,
required_tag_groups: [],
form_template_ids: [],
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index 7c450962ca..0bf524f965 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -258,7 +258,8 @@ class CategoriesController < ApplicationController
def find_by_slug
params.require(:category_slug)
- @category = Category.find_by_slug_path(params[:category_slug].split("/"))
+ @category =
+ Category.includes(:category_setting).find_by_slug_path(params[:category_slug].split("/"))
raise Discourse::NotFound unless @category.present?
@@ -405,6 +406,7 @@ class CategoriesController < ApplicationController
:read_only_banner,
:default_list_filter,
:reviewable_by_group_id,
+ category_setting_attributes: %i[auto_bump_cooldown_days],
custom_fields: [custom_field_params],
permissions: [*p.try(:keys)],
allowed_tags: [],
diff --git a/app/models/category.rb b/app/models/category.rb
index c568107da4..32397ee6aa 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -48,8 +48,12 @@ class Category < ActiveRecord::Base
has_one :category_setting, dependent: :destroy
+ delegate :auto_bump_cooldown_days, to: :category_setting, allow_nil: true
+
has_and_belongs_to_many :web_hooks
+ accepts_nested_attributes_for :category_setting, update_only: true
+
validates :user_id, presence: true
validates :name,
@@ -96,6 +100,7 @@ class Category < ActiveRecord::Base
before_save :apply_permissions
before_save :downcase_email
before_save :downcase_name
+ before_save :ensure_category_setting
after_save :publish_discourse_stylesheet
after_save :publish_category
@@ -682,7 +687,7 @@ class Category < ActiveRecord::Base
.exclude_scheduled_bump_topics
.where(category_id: self.id)
.where("id <> ?", self.topic_id)
- .where("bumped_at < ?", 1.day.ago)
+ .where("bumped_at < ?", (self.auto_bump_cooldown_days || 1).days.ago)
.where("pinned_at IS NULL AND NOT closed AND NOT archived")
.order("bumped_at ASC")
.limit(1)
@@ -1040,6 +1045,10 @@ class Category < ActiveRecord::Base
private
+ def ensure_category_setting
+ self.build_category_setting if self.category_setting.blank?
+ end
+
def should_update_reviewables?
SiteSetting.enable_category_group_moderation? && saved_change_to_reviewable_by_group_id?
end
diff --git a/app/models/category_setting.rb b/app/models/category_setting.rb
index 3295564ec5..4506d634af 100644
--- a/app/models/category_setting.rb
+++ b/app/models/category_setting.rb
@@ -9,19 +9,27 @@ class CategorySetting < ActiveRecord::Base
greater_than_or_equal_to: 0,
allow_nil: true,
}
+
+ validates :auto_bump_cooldown_days,
+ numericality: {
+ only_integer: true,
+ greater_than_or_equal_to: 0,
+ allow_nil: true,
+ }
end
# == Schema Information
#
# Table name: category_settings
#
-# id :bigint not null, primary key
-# category_id :bigint not null
-# require_topic_approval :boolean
-# require_reply_approval :boolean
-# num_auto_bump_daily :integer
-# created_at :datetime not null
-# updated_at :datetime not null
+# id :bigint not null, primary key
+# category_id :bigint not null
+# require_topic_approval :boolean
+# require_reply_approval :boolean
+# num_auto_bump_daily :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# auto_bump_cooldown_days :integer default(1)
# Indexes
#
# index_category_settings_on_category_id (category_id) UNIQUE
diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb
index d2bfbb1612..08b92ac6a5 100644
--- a/app/serializers/category_serializer.rb
+++ b/app/serializers/category_serializer.rb
@@ -1,6 +1,13 @@
# frozen_string_literal: true
class CategorySerializer < SiteCategorySerializer
+ class CategorySettingSerializer < ApplicationSerializer
+ attributes :auto_bump_cooldown_days,
+ :num_auto_bump_daily,
+ :require_reply_approval,
+ :require_topic_approval
+ end
+
attributes :read_restricted,
:available_groups,
:auto_close_hours,
@@ -22,6 +29,8 @@ class CategorySerializer < SiteCategorySerializer
:reviewable_by_group_name,
:default_slow_mode_seconds
+ has_one :category_setting, serializer: CategorySettingSerializer, embed: :objects
+
def reviewable_by_group_name
object.reviewable_by_group.name
end
@@ -30,6 +39,10 @@ class CategorySerializer < SiteCategorySerializer
SiteSetting.enable_category_group_moderation? && object.reviewable_by_group_id.present?
end
+ def include_category_setting?
+ object.association(:category_setting).loaded?
+ end
+
def group_permissions
@group_permissions ||=
begin
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index ec74c481de..a55cc7b98c 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3740,6 +3740,7 @@ en:
default_slow_mode: 'Enable "Slow Mode" for new topics in this category.'
parent: "Parent Category"
num_auto_bump_daily: "Number of open topics to automatically bump daily:"
+ auto_bump_cooldown_days: "Minimum days before bumping the same topic again:"
navigate_to_first_post_after_read: "Navigate to first post after topics are read"
notifications:
title: "change notification level for this category"
diff --git a/db/migrate/20230301071240_add_auto_bump_cooldown_days_to_category_settings.rb b/db/migrate/20230301071240_add_auto_bump_cooldown_days_to_category_settings.rb
new file mode 100644
index 0000000000..0119f4dd8c
--- /dev/null
+++ b/db/migrate/20230301071240_add_auto_bump_cooldown_days_to_category_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddAutoBumpCooldownDaysToCategorySettings < ActiveRecord::Migration[7.0]
+ def change
+ add_column :category_settings, :auto_bump_cooldown_days, :integer, default: 1
+ end
+end
diff --git a/db/migrate/20230308042434_backfill_auto_bump_cooldown_days_category_setting.rb b/db/migrate/20230308042434_backfill_auto_bump_cooldown_days_category_setting.rb
new file mode 100644
index 0000000000..f37c9117e2
--- /dev/null
+++ b/db/migrate/20230308042434_backfill_auto_bump_cooldown_days_category_setting.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BackfillAutoBumpCooldownDaysCategorySetting < ActiveRecord::Migration[7.0]
+ def up
+ execute(<<~SQL)
+ INSERT INTO
+ category_settings(
+ category_id,
+ auto_bump_cooldown_days,
+ created_at,
+ updated_at
+ )
+ SELECT
+ id,
+ 1,
+ NOW(),
+ NOW()
+ FROM categories
+ ON CONFLICT (category_id)
+ DO
+ UPDATE SET
+ auto_bump_cooldown_days = 1,
+ updated_at = NOW();
+ SQL
+ end
+end
diff --git a/spec/models/category_setting_spec.rb b/spec/models/category_setting_spec.rb
index fbcd643133..c9bf1160a7 100644
--- a/spec/models/category_setting_spec.rb
+++ b/spec/models/category_setting_spec.rb
@@ -9,4 +9,11 @@ RSpec.describe CategorySetting do
.is_greater_than_or_equal_to(0)
.allow_nil
end
+
+ it do
+ is_expected.to validate_numericality_of(:auto_bump_cooldown_days)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ .allow_nil
+ end
end
diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb
index 73c501d153..3157f3e4db 100644
--- a/spec/models/category_spec.rb
+++ b/spec/models/category_spec.rb
@@ -39,6 +39,10 @@ RSpec.describe Category do
describe "Associations" do
it { is_expected.to have_one(:category_setting).dependent(:destroy) }
+ it "automatically creates a category setting" do
+ expect { Fabricate(:category) }.to change { CategorySetting.count }.by(1)
+ end
+
it "should delete associated sidebar_section_links when category is destroyed" do
category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
category_sidebar_section_link_2 =
@@ -965,6 +969,40 @@ RSpec.describe Category do
expect(Category.auto_bump_topic!).to eq(false)
end
+ it "should not auto-bump the same topic within the cooldown" do
+ freeze_time
+ category =
+ Fabricate(
+ :category_with_definition,
+ num_auto_bump_daily: 2,
+ created_at: 1.minute.ago,
+ category_setting_attributes: {
+ auto_bump_cooldown_days: 1,
+ },
+ )
+ category.clear_auto_bump_cache!
+
+ post1 = create_post(category: category, created_at: 15.seconds.ago)
+
+ # no limits on post creation or category creation please
+ RateLimiter.enable
+
+ time = freeze_time 1.month.from_now
+
+ expect(category.auto_bump_topic!).to eq(true)
+ expect(Topic.where(bumped_at: time).count).to eq(1)
+
+ time = freeze_time 13.hours.from_now
+
+ expect(category.auto_bump_topic!).to eq(false)
+ expect(Topic.where(bumped_at: time).count).to eq(0)
+
+ time = freeze_time 13.hours.from_now
+
+ expect(category.auto_bump_topic!).to eq(true)
+ expect(Topic.where(bumped_at: time).count).to eq(1)
+ end
+
it "should not automatically bump topics with a bump scheduled" do
freeze_time
category = Fabricate(:category_with_definition, created_at: 1.second.ago)
diff --git a/spec/requests/api/schemas/json/category_create_response.json b/spec/requests/api/schemas/json/category_create_response.json
index 95b119111e..a669e84aa7 100644
--- a/spec/requests/api/schemas/json/category_create_response.json
+++ b/spec/requests/api/schemas/json/category_create_response.json
@@ -160,6 +160,12 @@
]
}
},
+ "category_setting": {
+ "auto_bump_cooldown_days": 1,
+ "num_auto_bump_daily": null,
+ "require_reply_approval": null,
+ "require_topic_approval": null
+ },
"read_only_banner": {
"type": [
"string",
diff --git a/spec/requests/api/schemas/json/category_update_response.json b/spec/requests/api/schemas/json/category_update_response.json
index 2570b4192c..93cf445d60 100644
--- a/spec/requests/api/schemas/json/category_update_response.json
+++ b/spec/requests/api/schemas/json/category_update_response.json
@@ -163,6 +163,12 @@
]
}
},
+ "category_setting": {
+ "auto_bump_cooldown_days": 1,
+ "num_auto_bump_daily": null,
+ "require_reply_approval": null,
+ "require_topic_approval": null
+ },
"read_only_banner": {
"type": [
"string",