From a90ad52dffb2e1dfa41db9d259454dbbe99ff64f Mon Sep 17 00:00:00 2001
From: Ted Johansson <ted@discourse.org>
Date: Mon, 13 Feb 2023 12:37:59 +0800
Subject: [PATCH] DEV: Add dedicated category settings model - Part 1 (#20211)

This is the first in a multi-part change to move the custom fields to a new table. It includes:

- Adding a new CategorySetting model and corresponding table.
- Populating it with data from the category_custom_fields table.
---
 app/models/category.rb                        |  2 +
 app/models/category_setting.rb                | 28 +++++++++++
 ...20230207093514_create_category_settings.rb | 17 +++++++
 ...230208020404_populate_category_settings.rb | 46 +++++++++++++++++++
 spec/models/category_setting_spec.rb          | 12 +++++
 spec/models/category_spec.rb                  |  2 +
 6 files changed, 107 insertions(+)
 create mode 100644 app/models/category_setting.rb
 create mode 100644 db/migrate/20230207093514_create_category_settings.rb
 create mode 100644 db/migrate/20230208020404_populate_category_settings.rb
 create mode 100644 spec/models/category_setting_spec.rb

diff --git a/app/models/category.rb b/app/models/category.rb
index 00e3bc3aeeb..39a2c70bf4f 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -46,6 +46,8 @@ class Category < ActiveRecord::Base
   has_many :topic_timers, dependent: :destroy
   has_many :upload_references, as: :target, dependent: :destroy
 
+  has_one :category_setting, dependent: :destroy
+
   has_and_belongs_to_many :web_hooks
 
   validates :user_id, presence: true
diff --git a/app/models/category_setting.rb b/app/models/category_setting.rb
new file mode 100644
index 00000000000..3295564ec5a
--- /dev/null
+++ b/app/models/category_setting.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class CategorySetting < ActiveRecord::Base
+  belongs_to :category
+
+  validates :num_auto_bump_daily,
+            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
+# Indexes
+#
+#  index_category_settings_on_category_id  (category_id) UNIQUE
+#
diff --git a/db/migrate/20230207093514_create_category_settings.rb b/db/migrate/20230207093514_create_category_settings.rb
new file mode 100644
index 00000000000..2a256a37049
--- /dev/null
+++ b/db/migrate/20230207093514_create_category_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateCategorySettings < ActiveRecord::Migration[7.0]
+  def change
+    # Moving the custom fields in core into a dedicated table for
+    # better type casting, validations, etc.
+    create_table :category_settings do |t|
+      t.references :category, null: false, index: { unique: true }
+
+      t.boolean :require_topic_approval, null: true
+      t.boolean :require_reply_approval, null: true
+      t.integer :num_auto_bump_daily, null: true
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20230208020404_populate_category_settings.rb b/db/migrate/20230208020404_populate_category_settings.rb
new file mode 100644
index 00000000000..bf3e0aabe93
--- /dev/null
+++ b/db/migrate/20230208020404_populate_category_settings.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class PopulateCategorySettings < ActiveRecord::Migration[7.0]
+  def up
+    execute(<<~SQL)
+      INSERT INTO
+        category_settings(
+          category_id,
+          require_topic_approval,
+          require_reply_approval,
+          num_auto_bump_daily,
+          created_at,
+          updated_at
+        )
+      SELECT
+        category_id,
+        MAX(
+          CASE WHEN (name = 'require_topic_approval')
+          THEN value ELSE NULL END
+        )::boolean AS require_topic_approval,
+        MAX(
+          CASE WHEN (name = 'require_reply_approval')
+          THEN value ELSE NULL END
+        )::boolean AS require_reply_approval,
+        MAX(
+          CASE WHEN (name = 'num_auto_bump_daily')
+          THEN value ELSE NULL END
+        )::integer AS num_auto_bump_daily,
+        NOW() AS created_at,
+        NOW() AS updated_at
+      FROM category_custom_fields
+      WHERE name IN (
+        'require_topic_approval',
+        'require_reply_approval',
+        'num_auto_bump_daily'
+      )
+      GROUP BY category_id;
+    SQL
+  end
+
+  def down
+    execute(<<~SQL)
+      TRUNCATE category_settings;
+    SQL
+  end
+end
diff --git a/spec/models/category_setting_spec.rb b/spec/models/category_setting_spec.rb
new file mode 100644
index 00000000000..fbcd6431336
--- /dev/null
+++ b/spec/models/category_setting_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.describe CategorySetting do
+  it { is_expected.to belong_to(:category) }
+
+  it do
+    is_expected.to validate_numericality_of(:num_auto_bump_daily)
+      .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 3c8a7ba2061..069f60023ab 100644
--- a/spec/models/category_spec.rb
+++ b/spec/models/category_spec.rb
@@ -37,6 +37,8 @@ RSpec.describe Category do
   end
 
   describe "Associations" do
+    it { is_expected.to have_one(:category_setting).dependent(:destroy) }
+
     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 =