diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb
index c47582255..58f948cca 100644
--- a/app/controllers/admin/tags_controller.rb
+++ b/app/controllers/admin/tags_controller.rb
@@ -6,6 +6,7 @@ module Admin
id supported rules_markdown short_summary pretty_name bg_color_hex
text_color_hex user_id alias_for badge_id requires_approval
social_preview_template wiki_body_markdown submission_template
+ suggested
].freeze
before_action :set_default_options, only: %i[index]
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index f44c65b02..bbb7a741c 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -66,7 +66,7 @@ class TagsController < ApplicationController
def onboarding
skip_authorization
- @tags = Tag.where(name: Settings::General.suggested_tags)
+ @tags = Tags::SuggestedForOnboarding.call
.select(ONBOARDING_API_ATTRIBUTES)
render json: @tags
diff --git a/app/models/tag.rb b/app/models/tag.rb
index dcc60812f..ce97fa6cf 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -56,6 +56,7 @@ class Tag < ActsAsTaggableOn::Tag
before_save :calculate_hotness_score
before_save :mark_as_updated
+ after_save :update_suggested_tags, if: :saved_change_to_suggested?
after_commit :bust_cache
# @note Even though we have a data migration script (see further
@@ -81,6 +82,8 @@ class Tag < ActsAsTaggableOn::Tag
scope :eager_load_serialized_data, -> {}
scope :supported, -> { where(supported: true) }
+ scope :suggested_for_onboarding, -> { where(suggested: true) }
+
# possible social previews templates for articles with a particular tag
def self.social_preview_templates
Rails.root.join("app/views/social_previews/articles").children.map { |ch| File.basename(ch, ".html.erb") }
@@ -293,4 +296,8 @@ class Tag < ActsAsTaggableOn::Tag
def mark_as_updated
self.updated_at = Time.current # Acts-as-taggable didn't come with this by default
end
+
+ def update_suggested_tags
+ Settings::General.suggested_tags = Tag.suggested_for_onboarding.pluck(:name).join(",")
+ end
end
diff --git a/app/queries/tags/suggested_for_onboarding.rb b/app/queries/tags/suggested_for_onboarding.rb
new file mode 100644
index 000000000..0e5f386b1
--- /dev/null
+++ b/app/queries/tags/suggested_for_onboarding.rb
@@ -0,0 +1,33 @@
+module Tags
+ class SuggestedForOnboarding
+ MAX = 45
+
+ def self.call(...)
+ new(...).call
+ end
+
+ def initialize(...); end
+
+ def call
+ return suggested_tags if suggested_tags.count >= MAX
+
+ Tag
+ .where(suggested_for_onboarding_or_supported)
+ .order("suggested DESC, supported DESC, taggings_count DESC, name ASC")
+ .limit(MAX)
+ end
+
+ private
+
+ def suggested_tags
+ @suggested_tags ||= Tag.suggested_for_onboarding
+ end
+
+ def suggested_for_onboarding_or_supported
+ builder = Tag.arel_table
+ supported = builder[:supported].eq(true)
+ suggested = builder[:suggested].eq(true)
+ suggested.or(supported)
+ end
+ end
+end
diff --git a/app/services/settings/general/upsert.rb b/app/services/settings/general/upsert.rb
index d7a90c8a4..db63ddc69 100644
--- a/app/services/settings/general/upsert.rb
+++ b/app/services/settings/general/upsert.rb
@@ -34,8 +34,18 @@ module Settings
def self.create_tags_if_necessary(settings)
return unless (settings.keys & TAG_PARAMS).any?
- tags = Settings::General.suggested_tags + Settings::General.sidebar_tags
- Tag.find_or_create_all_with_like_by_name(tags)
+ create_sidebar_tags
+ create_suggested_tags
+ end
+
+ def self.create_sidebar_tags
+ Tag.find_or_create_all_with_like_by_name(Settings::General.sidebar_tags)
+ end
+
+ def self.create_suggested_tags
+ suggested = Tag.find_or_create_all_with_like_by_name(Settings::General.suggested_tags)
+ Tag.where(suggested: true).update_all(suggested: false)
+ Tag.where(id: suggested).update_all(suggested: true)
end
def self.upload_logo(image)
diff --git a/app/views/admin/tags/_form.html.erb b/app/views/admin/tags/_form.html.erb
index 518c9e59a..abec29c18 100644
--- a/app/views/admin/tags/_form.html.erb
+++ b/app/views/admin/tags/_form.html.erb
@@ -12,6 +12,13 @@
Allows the tag to be a searchable result when writing a post or a listing
+
+ <%= f.check_box :suggested, class: "crayons-checkbox" %>
+
+
<%= f.label :badge_id, class: "crayons-field__label" %>
<%= f.select(:badge_id, options_for_select(badges_for_options, tag.badge_id), { include_blank: true }, { class: "crayons-select" }) %>
diff --git a/db/migrate/20230517132219_add_suggested_to_tags.rb b/db/migrate/20230517132219_add_suggested_to_tags.rb
new file mode 100644
index 000000000..537b30289
--- /dev/null
+++ b/db/migrate/20230517132219_add_suggested_to_tags.rb
@@ -0,0 +1,5 @@
+class AddSuggestedToTags < ActiveRecord::Migration[7.0]
+ def change
+ add_column :tags, :suggested, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index da5ac80e6..ee900efa9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_03_30_145039) do
+ActiveRecord::Schema[7.0].define(version: 2023_05_17_132219) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@@ -1109,6 +1109,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_30_145039) do
t.string "social_image"
t.string "social_preview_template", default: "article"
t.text "submission_template"
+ t.boolean "suggested", default: false
t.boolean "supported", default: false
t.integer "taggings_count", default: 0
t.string "text_color_hex"
diff --git a/lib/data_update_scripts/20230517132257_populate_suggested_tags_from_settings.rb b/lib/data_update_scripts/20230517132257_populate_suggested_tags_from_settings.rb
new file mode 100644
index 000000000..ea771b0de
--- /dev/null
+++ b/lib/data_update_scripts/20230517132257_populate_suggested_tags_from_settings.rb
@@ -0,0 +1,20 @@
+module DataUpdateScripts
+ class PopulateSuggestedTagsFromSettings
+ cattr_accessor :suggested_tags
+
+ # We've added a new column on Tag (suggested) that needs to be populated
+ # from a list that has previously been stored in Settings::General.suggested_tags
+ # later this can go away
+ def run(suggested_tags: settings_suggested_tags)
+ return unless suggested_tags.any?
+
+ Tag.where(name: suggested_tags).update_all suggested: true
+ end
+
+ private
+
+ def settings_suggested_tags
+ suggested_tags || Settings::General.suggested_tags
+ end
+ end
+end
diff --git a/spec/lib/data_update_scripts/populate_suggested_tags_from_settings_spec.rb b/spec/lib/data_update_scripts/populate_suggested_tags_from_settings_spec.rb
new file mode 100644
index 000000000..80761f160
--- /dev/null
+++ b/spec/lib/data_update_scripts/populate_suggested_tags_from_settings_spec.rb
@@ -0,0 +1,29 @@
+require "rails_helper"
+require Rails.root.join(
+ "lib/data_update_scripts/20230517132257_populate_suggested_tags_from_settings.rb",
+)
+
+describe DataUpdateScripts::PopulateSuggestedTagsFromSettings do
+ let(:suggested_tags) { %w[some tags go here] }
+
+ before do
+ create(:tag, name: "some")
+ create(:tag, name: "tags")
+ create(:tag, name: "go")
+ create(:tag, name: "here")
+ create(:tag, name: "otherwise")
+ create(:tag, name: "nothing")
+ end
+
+ around do |example|
+ original = described_class.suggested_tags
+ described_class.suggested_tags = suggested_tags
+ example.run
+ described_class.suggested_tags = original
+ end
+
+ it "updates tags to use new boolean attribute (instead of settings)" do
+ described_class.new.run
+ expect(Tag.where(suggested: true).pluck(:name)).to include(*%w[some tags go here])
+ end
+end
diff --git a/spec/queries/tags/suggested_for_onboarding_spec.rb b/spec/queries/tags/suggested_for_onboarding_spec.rb
new file mode 100644
index 000000000..98e75b8c4
--- /dev/null
+++ b/spec/queries/tags/suggested_for_onboarding_spec.rb
@@ -0,0 +1,31 @@
+require "rails_helper"
+
+RSpec.describe Tags::SuggestedForOnboarding, type: :query do
+ subject(:result) { described_class.call }
+
+ let!(:suggested_tags) do
+ create(:tag, suggested: true)
+ create(:tag, suggested: true)
+ create(:tag, suggested: true)
+ Tag.where(suggested: true)
+ end
+
+ it "starts with Settings::General::SuggestedTags" do
+ expect(result).to match_array(suggested_tags)
+ end
+
+ context "when suggested tags aren't enough" do
+ let!(:supported) { create(:tag, supported: true) }
+
+ it "adds supported tags" do
+ expect(Tag.count < Tags::SuggestedForOnboarding::MAX).to be_truthy
+ expect(result).to include(*suggested_tags)
+ expect(result).to include(supported)
+ end
+
+ it "sorts suggested first, then supported" do
+ expect(suggested_tags).to include(result.first)
+ expect(result).to include(result.first)
+ end
+ end
+end
diff --git a/spec/requests/tags_spec.rb b/spec/requests/tags_spec.rb
index d9aab1a5d..df0cf66be 100644
--- a/spec/requests/tags_spec.rb
+++ b/spec/requests/tags_spec.rb
@@ -220,12 +220,14 @@ RSpec.describe "Tags", proper_status: true do
expect(response_tag["taggings_count"]).to eq(tag.taggings_count)
end
- it "returns only suggested tags" do
- not_suggested_tag = create(:tag, name: "definitelynotasuggestedtag")
+ it "returns suggested and supported tags" do
+ not_suggested_but_supported = create(:tag, name: "notsuggestedbutsupported", supported: true, suggested: false)
+ neither_suggested_nor_supported = create(:tag, name: "definitelynotasuggestedtag", supported: false)
get onboarding_tags_path, headers: headers
- expect(response.parsed_body.filter { |t| t["name"] == not_suggested_tag.name }).to be_empty
+ expect(response.parsed_body.filter { |t| t["name"] == not_suggested_but_supported.name }).not_to be_empty
+ expect(response.parsed_body.filter { |t| t["name"] == neither_suggested_nor_supported.name }).to be_empty
end
it "sets the correct edge caching surrogate key for all tags" do