discourse/lib/tasks/migrate_advanced_search_banner_to_welcome_banner.rake
Yuriy Kurant 0e380166fb
FIX: improves search banner migration scripts (#36192)
After running migration scripts on multiple sites the following
improvements were added:
* avoid conflicting `background_image` and `background_image_light`
settings migration
* run db query once for `migrate_all` task
* match theme url with and without `.git` suffix
* abort the migration if more than one theme is installed

and code related:
* improve logging for cleaner output
* remove logging duplicates
* rename methods and vars to be more self-explanatory
* simplify logic for a clearer execution
2025-11-24 17:37:53 +08:00

394 lines
12 KiB
Ruby

# frozen_string_literal: true
# rubocop:disable Lint/OrAssignmentToConstant
THEME_GIT_URL ||= "https://github.com/discourse/discourse-search-banner"
REQUIRED_TRANSLATION_KEYS ||= %w[search_banner.headline search_banner.subhead]
desc "Run all Advanced Search Banner migration tasks"
task "themes:advanced_search_banner:migrate_all" => :environment do
components = find_component_in_all_dbs(includes: %i[theme_settings theme_translation_overrides])
if components.any?
puts "\n1. Migrating settings..."
puts "------------------------"
components.each { |c| process_theme_component_settings(c) }
puts "\n2. Migrating translations..."
puts "----------------------------"
components.each { |c| process_theme_component_translations(c) }
puts "\n3. Excluding and disabling..."
puts "-----------------------------"
components.each { |c| process_theme_component(c) }
end
end
desc "Migrate settings from Advanced Search Banner to core welcome banner"
task "themes:advanced_search_banner:1_migrate_settings_to_welcome_banner" => :environment do
components = find_component_in_all_dbs(includes: [:theme_settings])
if components.any?
puts "\n1. Migrating settings..."
puts "------------------------"
components.each { |c| process_theme_component_settings(c) }
end
end
desc "Migrate translations from Advanced Search Banner to core welcome banner"
task "themes:advanced_search_banner:2_migrate_translations_to_welcome_banner" => :environment do
components = find_component_in_all_dbs(includes: [:theme_translation_overrides])
if components.any?
puts "\n2. Migrating translations..."
puts "----------------------------"
components.each { |c| process_theme_component_translations(c) }
end
end
desc "Exclude and disable Advanced Search Banner theme component"
task "themes:advanced_search_banner:3_exclude_and_disable" => :environment do
components = find_component_in_all_dbs
if components.any?
puts "\n3. Excluding and disabling..."
puts "-----------------------------"
components.each { |c| process_theme_component(c) }
end
end
# Common helper methods
def find_component_in_all_dbs(includes: [])
if ENV["RAILS_DB"].present?
db = validate_and_get_db(ENV["RAILS_DB"])
RailsMultisite::ConnectionManagement.establish_connection(db: db)
component = find_component_in_db(db, includes)
component ? [component] : []
else
components = []
RailsMultisite::ConnectionManagement.each_connection do |db|
component = find_component_in_db(db, includes)
components << component if component
end
components
end
end
def validate_and_get_db(db)
return db if RailsMultisite::ConnectionManagement.has_db?(db)
default_db = RailsMultisite::ConnectionManagement::DEFAULT
puts "\e[31m✗ Database \e[1;101m[#{db}]\e[0m \e[31mnot found\e[0m"
puts "Using default database instead: \e[1;104m[#{default_db}]\e[0m\n\n"
default_db
end
def find_component_in_db(db, additional_includes)
puts "Accessing database: \e[1;104m[#{db}]\e[0m"
puts " Searching for Advanced Search Banner theme component..."
includes = [{ parent_theme_relation: :parent_theme }] + Array(additional_includes)
themes =
RemoteTheme
.where(remote_url: [THEME_GIT_URL, "#{THEME_GIT_URL}.git"])
.includes(theme: includes)
.map(&:theme)
if themes.length > 1
puts " \e[33mMultiple (#{themes.length}) Advanced Search Banner theme components found:\e[0m"
themes.each { |theme| puts " - #{theme_identifier(theme)}" }
puts " \e[33mMake sure a single instance is installed before running migrations.\e[0m"
return nil
elsif themes.length == 1
puts " \e[1;34m✓ Found: #{theme_identifier(themes.first)}\e[0m"
else
puts " \e[33m✗ Not found.\e[0m"
end
themes.first
end
def theme_identifier(theme)
"\e[1m#{theme.name} (ID: #{theme.id})\e[0m"
end
def not_included_in_any_theme?(theme)
return false if theme.parent_theme_relation.exists?
puts " \e[33m#{theme_identifier(theme)} is not included in any of your themes. Skipping\e[0m"
true
end
# 1. Settings migration
unless defined?(SETTINGS_MAPPING)
SETTINGS_MAPPING = {
"show_on" => {
site_setting: "welcome_banner_page_visibility",
value_mapping: {
"top_menu" => "top_menu_pages",
"all" => "all_pages",
},
},
"plugin_outlet" => {
site_setting: "welcome_banner_location",
value_mapping: {
"above-main-container" => "above_topic_content",
"below-site-header" => "below_site_header",
},
},
"background_image" => {
site_setting: "welcome_banner_image",
value_mapping: nil,
},
"background_image_light" => {
site_setting: "welcome_banner_image",
value_mapping: nil,
},
}
end
def process_theme_component_settings(theme)
return if not_included_in_any_theme?(theme)
migration_errors = []
puts "\n Migrating settings for #{theme_identifier(theme)}..."
migrated_count = migrate_theme_settings_to_site_settings(theme.theme_settings, migration_errors)
if migrated_count == theme.theme_settings.size
puts " \e[1;32m✓ Migrated #{migrated_count} setting#{"s" if migrated_count != 1}\e[0m"
else
puts " \e[33mMigrated #{migrated_count} out of #{theme.theme_settings.size} setting#{"s" if theme.theme_settings.size != 1}\e[0m"
end
if migration_errors.any?
puts "\n\e[1;31mMigration completed with errors\e[0m"
else
puts "\n\e[1;34mMigration completed successfully!\e[0m"
end
end
def migrate_theme_settings_to_site_settings(theme_settings, errors)
migrated_count = 0
names = theme_settings.map(&:name).to_set
theme_settings.each do |ts|
mapping = SETTINGS_MAPPING[ts.name]
old_setting = "\e[0;31m#{ts.name}: #{ts.value}\e[0m"
unless mapping
puts " - #{old_setting}\e[33m: is not supported by core welcome banner. Skipping\e[0m"
next
end
if ts.name == "background_image" && names.include?("background_image_light")
puts " - #{old_setting}\e[33m: using 'background_image_light' instead. Skipping\e[0m"
next
end
site_setting_name = mapping[:site_setting]
if ts.value.blank?
puts " - \e[0;31m#{ts.name}\e[0m\e[33m: has no value. Skipping\e[0m"
next
end
if mapping[:value_mapping]
new_value = mapping[:value_mapping][ts.value] || ts.value
else
new_value = ts.value.to_i
end
begin
SiteSetting.set_and_log(
site_setting_name,
new_value,
Discourse.system_user,
"Migrated from the deprecated Advanced Search Banner",
)
arrow = "\e[0m=>\e[0m"
new_setting = "\e[0;32m#{site_setting_name}: #{new_value}\e[0m"
puts " - #{old_setting} #{arrow} #{new_setting}"
migrated_count += 1
rescue StandardError => e
errors << e
puts " \e[31m- failed to migrate '#{ts.name}': \e[1m#{e.message}\e[0m"
end
end
migrated_count
end
# 2. Translations migration
def process_theme_component_translations(theme)
return if not_included_in_any_theme?(theme)
puts "\n Migrating translation overrides for #{theme_identifier(theme)}..."
migrated_count = 0
if theme.theme_translation_overrides.any?
processed_keys_by_locale = Hash.new { |h, k| h[k] = Set.new }
theme.theme_translation_overrides.each do |override|
count =
migrate_translations(
locale: override.locale,
key: override.translation_key,
value: override.value,
)
migrated_count += count
processed_keys_by_locale[override.locale].add(override.translation_key)
end
shown = false
processed_keys_by_locale.each do |locale, processed_keys|
missing_keys = REQUIRED_TRANSLATION_KEYS - processed_keys.to_a
if missing_keys.any?
unless shown
puts " Migrating Advanced Search Banner's default translations..."
shown = true
end
missing_keys.each do |missing_key|
count = migrate_translations(locale: locale, key: missing_key)
migrated_count += count
end
end
end
else
puts " \e[33m✗ No translation overrides found\e[0m"
puts " Migrating Advanced Search Banner's default translations..."
REQUIRED_TRANSLATION_KEYS.each do |required_key|
count = migrate_translations(key: required_key)
migrated_count += count
end
end
puts " \e[1;32m✓ Migrated #{migrated_count} translation#{"s" if migrated_count != 1}\e[0m"
puts "\n\e[1;34mMigration completed successfully!\e[0m"
end
def migrate_translations(locale: "en", key:, value: nil)
default_translations = {
"js.welcome_banner.header.anonymous_members" => "Welcome to our community",
"js.welcome_banner.header.logged_in_members" => "Welcome to our community",
"js.welcome_banner.subheader.anonymous_members" =>
"We're happy to have you here. If you need help, please search before you post.",
"js.welcome_banner.subheader.logged_in_members" =>
"We're happy to have you here. If you need help, please search before you post.",
}
mapped_keys = map_translation_keys(key)
# Print the value once before processing all keys
first_key = mapped_keys.first
new_value = value || default_translations[first_key]
puts " \e[1;32m✓\e[0m \e[1;94m\"#{new_value}\"\e[0m"
mapped_keys.each do |new_key|
actual_value = value || default_translations[new_key]
TranslationOverride.upsert!(locale, new_key, actual_value)
old_text = "\e[0;31m#{key}\e[0m"
arrow = "\e[0m=>\e[0m"
new_text = "\e[0;32m#{locale}.#{new_key}\e[0m"
puts " - #{old_text} #{arrow} #{new_text}"
end
mapped_keys.count
end
def map_translation_keys(translation_key)
translations_mapping = {
"search_banner.headline" => %w[
js.welcome_banner.header.anonymous_members
js.welcome_banner.header.logged_in_members
],
"search_banner.subhead" => %w[
js.welcome_banner.subheader.anonymous_members
js.welcome_banner.subheader.logged_in_members
],
"search_banner.search_button_text" => ["js.welcome_banner.search_placeholder"],
}
translations_mapping[translation_key] || []
end
# 3. Exclude and disable
def process_theme_component(theme)
enable_welcome_banner(theme)
exclude_theme_component(theme)
disable_theme_component(theme)
puts "\n\e[1;34mTask completed successfully!\e[0m"
end
def enable_welcome_banner(theme)
puts "\n Executing enable core welcome banner step... (1/3)"
if !theme.enabled
puts " \e[33m#{theme_identifier(theme)} is disabled, thus no need to enable core welcome banner. Skipping\e[0m"
return
end
return unless theme.enabled
return if not_included_in_any_theme?(theme)
puts " Enabling \e[1mcore welcome banner\e[0m for..."
enabled_count = 0
theme.parent_theme_relation.each do |relation|
parent_theme = relation.parent_theme
site_setting =
ThemeSiteSetting.find_by(theme_id: parent_theme.id, name: "enable_welcome_banner")
next if site_setting.nil?
if site_setting.value == "f"
Themes::ThemeSiteSettingManager.call(
params: {
theme_id: parent_theme.id,
name: "enable_welcome_banner",
value: true,
},
guardian: Discourse.system_user.guardian,
)
puts " - #{parent_theme.name} (ID: #{parent_theme.id}) \e[32m- enabled\e[0m"
enabled_count += 1
else
puts " - #{parent_theme.name} (ID: #{parent_theme.id}) \e[33m- it was already enabled. Skipping\e[0m"
end
end
puts " \e[1;32m✓ Enabled for #{enabled_count} theme#{"s" unless enabled_count == 1}\e[0m"
end
def exclude_theme_component(theme)
puts "\n Executing exclude step... (2/3)"
return if not_included_in_any_theme?(theme)
parent_relations = theme.parent_theme_relation.to_a
total_relations = parent_relations.size
parent_names = parent_relations.map { |r| "#{r.parent_theme.name} (ID: #{r.parent_theme_id})" }
puts " Excluding #{theme_identifier(theme)} from:"
puts " - #{parent_names.join("\n - ")}"
theme.parent_theme_ids = []
theme.save!
puts " \e[1;32m✓ Excluded from #{total_relations} theme#{"s" if total_relations > 1}\e[0m"
end
def disable_theme_component(theme)
puts "\n Executing disable component step... (3/3)"
if !theme.enabled
puts " \e[33m#{theme_identifier(theme)} was already disabled. Skipping\e[0m"
return
end
puts " Disabling #{theme_identifier(theme)}..."
theme.update!(enabled: false)
StaffActionLogger.new(Discourse.system_user).log_theme_component_disabled(theme)
puts " \e[1;32m✓ Disabled\e[0m"
end