mirror of
https://github.com/discourse/discourse.git
synced 2025-06-02 04:08:41 +08:00
FEATURE: Site settings defaults per locale
This change-set allows setting different defaults for different locales. It also: - Adds extensive testing around site setting validation - raises deprecation error if site setting has the default property based on env - relocated site settings for dev and tests in the initializer - deprecated client_setting in the site setting's loading process - ensure it raises when a enum site setting being set - default_locale is promoted to `required` category. - fixes incorrect default setting and validation - fixes ensure type check for site settings - creates a benchmark for site setting - sets reasonable defaults for Chinese
This commit is contained in:
126
lib/site_settings/defaults_provider.rb
Normal file
126
lib/site_settings/defaults_provider.rb
Normal file
@ -0,0 +1,126 @@
|
||||
module SiteSettings; end
|
||||
|
||||
# A cache for providing default value based on site locale
|
||||
class SiteSettings::DefaultsProvider
|
||||
include Enumerable
|
||||
|
||||
CONSUMED_OPTS = %i[default locale_default].freeze
|
||||
DEFAULT_LOCALE_KEY = :default_locale
|
||||
DEFAULT_LOCALE = 'en'.freeze
|
||||
DEFAULT_CATEGORY = 'required'.freeze
|
||||
|
||||
def initialize(site_setting)
|
||||
@site_setting = site_setting
|
||||
@site_setting.refresh_settings << DEFAULT_LOCALE_KEY
|
||||
|
||||
@cached = {}
|
||||
@defaults = {}
|
||||
@defaults[DEFAULT_LOCALE.to_sym] = {}
|
||||
@site_locale = nil
|
||||
refresh_site_locale!
|
||||
end
|
||||
|
||||
def load_setting(name_arg, value, opts = {})
|
||||
name = name_arg.to_sym
|
||||
@defaults[DEFAULT_LOCALE.to_sym][name] = value
|
||||
|
||||
if (locale_default = opts[:locale_default])
|
||||
locale_default.each do |locale, v|
|
||||
locale = locale.to_sym
|
||||
@defaults[locale] ||= {}
|
||||
@defaults[locale][name] = v
|
||||
end
|
||||
end
|
||||
refresh_cache!
|
||||
end
|
||||
|
||||
def db_all
|
||||
@site_setting.provider.all.delete_if { |s| s.name.to_sym == DEFAULT_LOCALE_KEY }
|
||||
end
|
||||
|
||||
def all
|
||||
@cached
|
||||
end
|
||||
|
||||
def get(name)
|
||||
@cached[name.to_sym]
|
||||
end
|
||||
|
||||
# Used to override site settings in dev/test env
|
||||
def set_regardless_of_locale(name, value)
|
||||
name = name.to_sym
|
||||
if @site_setting.has_setting?(name)
|
||||
@defaults.each { |_, hash| hash.delete(name) }
|
||||
@defaults[DEFAULT_LOCALE.to_sym][name] = value
|
||||
value, type = @site_setting.type_supervisor.to_db_value(name, value)
|
||||
@cached[name] = @site_setting.type_supervisor.to_rb_value(name, value, type)
|
||||
else
|
||||
raise ArgumentError.new("No setting named '#{name}' exists")
|
||||
end
|
||||
end
|
||||
|
||||
alias [] get
|
||||
|
||||
attr_reader :site_locale
|
||||
|
||||
def site_locale=(val)
|
||||
val = val.to_s
|
||||
raise Discourse::InvalidParameters.new(:value) unless LocaleSiteSetting.valid_value?(val)
|
||||
|
||||
if val != @site_locale
|
||||
@site_setting.provider.save(DEFAULT_LOCALE_KEY, val, SiteSetting.types[:string])
|
||||
refresh_site_locale!
|
||||
@site_setting.refresh!
|
||||
Discourse.request_refresh!
|
||||
end
|
||||
|
||||
@site_locale
|
||||
end
|
||||
|
||||
def each
|
||||
@cached.each { |k, v| yield k.to_sym, v }
|
||||
end
|
||||
|
||||
def locale_setting_hash
|
||||
{
|
||||
setting: DEFAULT_LOCALE_KEY,
|
||||
default: DEFAULT_LOCALE,
|
||||
category: DEFAULT_CATEGORY,
|
||||
description: @site_setting.description(DEFAULT_LOCALE_KEY),
|
||||
type: SiteSetting.types[SiteSetting.types[:enum]],
|
||||
preview: nil,
|
||||
value: @site_locale,
|
||||
valid_values: LocaleSiteSetting.values,
|
||||
translate_names: LocaleSiteSetting.translate_names?
|
||||
}
|
||||
end
|
||||
|
||||
def refresh_site_locale!
|
||||
if GlobalSetting.respond_to?(DEFAULT_LOCALE_KEY) &&
|
||||
(global_val = GlobalSetting.send(DEFAULT_LOCALE_KEY)) &&
|
||||
!global_val.blank?
|
||||
@site_locale = global_val
|
||||
elsif (db_val = @site_setting.provider.find(DEFAULT_LOCALE_KEY))
|
||||
@site_locale = db_val.value.to_s
|
||||
else
|
||||
@site_locale = DEFAULT_LOCALE
|
||||
end
|
||||
refresh_cache!
|
||||
@site_locale
|
||||
end
|
||||
|
||||
def has_setting?(name)
|
||||
has_key?(name.to_sym) || has_key?("#{name.to_s}?".to_sym)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def has_key?(key)
|
||||
@cached.key?(key) || key == DEFAULT_LOCALE_KEY
|
||||
end
|
||||
|
||||
def refresh_cache!
|
||||
@cached = @defaults[DEFAULT_LOCALE.to_sym].merge(@defaults.fetch(@site_locale.to_sym, {}))
|
||||
end
|
||||
|
||||
end
|
26
lib/site_settings/deprecated_settings.rb
Normal file
26
lib/site_settings/deprecated_settings.rb
Normal file
@ -0,0 +1,26 @@
|
||||
module SiteSettings; end
|
||||
|
||||
module SiteSettings::DeprecatedSettings
|
||||
DEPRECATED_SETTINGS = [
|
||||
%w[use_https force_https 1.7]
|
||||
]
|
||||
|
||||
def setup_deprecated_methods
|
||||
DEPRECATED_SETTINGS.each do |old_setting, new_setting, version|
|
||||
define_singleton_method old_setting do
|
||||
logger.warn("`SiteSetting.#{old_setting}` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}` instead")
|
||||
self.public_send new_setting
|
||||
end
|
||||
|
||||
define_singleton_method "#{old_setting}?" do
|
||||
logger.warn("`SiteSetting.#{old_setting}?` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}?` instead")
|
||||
self.public_send "#{new_setting}?"
|
||||
end
|
||||
|
||||
define_singleton_method "#{old_setting}=" do |val|
|
||||
logger.warn("`SiteSetting.#{old_setting}=` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}=` instead")
|
||||
self.public_send "#{new_setting}=", val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -10,7 +10,7 @@ class SiteSettings::LocalProcessProvider
|
||||
@settings[current_site] ||= {}
|
||||
end
|
||||
|
||||
def initialize()
|
||||
def initialize
|
||||
@settings = {}
|
||||
self.current_site = "test"
|
||||
end
|
||||
|
210
lib/site_settings/type_supervisor.rb
Normal file
210
lib/site_settings/type_supervisor.rb
Normal file
@ -0,0 +1,210 @@
|
||||
require_dependency 'site_settings/validations'
|
||||
require_dependency 'enum'
|
||||
|
||||
module SiteSettings; end
|
||||
|
||||
class SiteSettings::TypeSupervisor
|
||||
include SiteSettings::Validations
|
||||
|
||||
CONSUMED_OPTS = %i[enum choices type validator min max regex hidden regex_error].freeze
|
||||
VALIDATOR_OPTS = %i[min max regex hidden regex_error].freeze
|
||||
|
||||
# For plugins, so they can tell if a feature is supported
|
||||
SUPPORTED_TYPES = %i[email username list enum].freeze
|
||||
|
||||
def self.types
|
||||
@types ||= Enum.new(string: 1,
|
||||
time: 2,
|
||||
integer: 3,
|
||||
float: 4,
|
||||
bool: 5,
|
||||
null: 6,
|
||||
enum: 7,
|
||||
list: 8,
|
||||
url_list: 9,
|
||||
host_list: 10,
|
||||
category_list: 11,
|
||||
value_list: 12,
|
||||
regex: 13,
|
||||
email: 14,
|
||||
username: 15)
|
||||
end
|
||||
|
||||
def self.parse_value_type(val)
|
||||
case val
|
||||
when NilClass
|
||||
self.types[:null]
|
||||
when String
|
||||
self.types[:string]
|
||||
when Integer
|
||||
self.types[:integer]
|
||||
when Float
|
||||
self.types[:float]
|
||||
when TrueClass, FalseClass
|
||||
self.types[:bool]
|
||||
else
|
||||
raise ArgumentError.new :val
|
||||
end
|
||||
end
|
||||
|
||||
def self.supported_types
|
||||
SUPPORTED_TYPES
|
||||
end
|
||||
|
||||
def initialize(defaults_provider)
|
||||
@defaults_provider = defaults_provider
|
||||
@enums = {}
|
||||
@static_types = {}
|
||||
@choices = {}
|
||||
@validators = {}
|
||||
@types = {}
|
||||
end
|
||||
|
||||
def load_setting(name_arg, opts = {})
|
||||
name = name_arg.to_sym
|
||||
|
||||
if (enum = opts[:enum])
|
||||
@enums[name] = enum.is_a?(String) ? enum.constantize : enum
|
||||
opts[:type] ||= :enum
|
||||
end
|
||||
|
||||
if (new_choices = opts[:choices])
|
||||
new_choices = eval(new_choices) if new_choices.is_a?(String)
|
||||
|
||||
if @choices.has_key?(name)
|
||||
@choices[name].concat(new_choices)
|
||||
else
|
||||
@choices[name] = new_choices
|
||||
end
|
||||
end
|
||||
|
||||
if (type = opts[:type])
|
||||
@static_types[name] = type.to_sym
|
||||
end
|
||||
@types[name] = get_data_type(name, @defaults_provider[name])
|
||||
|
||||
opts[:validator] = opts[:validator].try(:constantize)
|
||||
if (validator_type = (opts[:validator] || validator_for(@types[name])))
|
||||
@validators[name] = { class: validator_type, opts: opts.slice(*VALIDATOR_OPTS) }
|
||||
end
|
||||
end
|
||||
|
||||
def to_rb_value(name, value, override_type = nil)
|
||||
name = name.to_sym
|
||||
type = @types[name] = (override_type || @types[name] || get_data_type(name, value))
|
||||
|
||||
case type
|
||||
when self.class.types[:float]
|
||||
value.to_f
|
||||
when self.class.types[:integer]
|
||||
value.to_i
|
||||
when self.class.types[:bool]
|
||||
value == true || value == 't' || value == 'true'
|
||||
when self.class.types[:null]
|
||||
nil
|
||||
when self.class.types[:enum]
|
||||
@defaults_provider[name].is_a?(Integer) ? value.to_i : value.to_s
|
||||
when self.class.types[:string]
|
||||
value.to_s
|
||||
else
|
||||
return value if self.class.types[type]
|
||||
# Otherwise it's a type error
|
||||
raise ArgumentError.new :type
|
||||
end
|
||||
end
|
||||
|
||||
def to_db_value(name, value)
|
||||
val, type = normalize_input(name, value)
|
||||
validate_value(name, type, val)
|
||||
[val, type]
|
||||
end
|
||||
|
||||
def type_hash(name)
|
||||
name = name.to_sym
|
||||
type = self.class.types[@types[name]]
|
||||
|
||||
result = { type: type.to_s }
|
||||
|
||||
if type == :enum
|
||||
if (klass = enum_class(name))
|
||||
result.merge!(valid_values: klass.values, translate_names: klass.translate_names?)
|
||||
else
|
||||
result.merge!(valid_values: @choices[name].map { |c| { name: c, value: c } }, translate_names: false)
|
||||
end
|
||||
end
|
||||
|
||||
result[:choices] = @choices[name] if @choices.has_key? name
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_input(name, val)
|
||||
name = name.to_sym
|
||||
type = @types[name] || self.class.parse_value_type(val)
|
||||
|
||||
if type == self.class.types[:bool]
|
||||
val = (val == true || val == 't' || val == 'true') ? 't' : 'f'
|
||||
elsif type == self.class.types[:integer] && !val.is_a?(Integer)
|
||||
val = val.to_i
|
||||
elsif type == self.class.types[:null] && val != ''
|
||||
type = get_data_type(name, val)
|
||||
elsif type == self.class.types[:enum]
|
||||
val = @defaults_provider[name].is_a?(Integer) ? val.to_i : val.to_s
|
||||
end
|
||||
|
||||
[val, type]
|
||||
end
|
||||
|
||||
def validate_value(name, type, val)
|
||||
if type == self.class.types[:enum]
|
||||
if enum_class(name)
|
||||
raise Discourse::InvalidParameters.new(:value) unless enum_class(name).valid_value?(val)
|
||||
else
|
||||
raise Discourse::InvalidParameters.new(:value) unless @choices[name].include?(val)
|
||||
end
|
||||
end
|
||||
|
||||
if (v = @validators[name])
|
||||
validator = v[:class].new(v[:opts])
|
||||
unless validator.valid_value?(val)
|
||||
raise Discourse::InvalidParameters, "#{name.to_s}: #{validator.error_message}"
|
||||
end
|
||||
end
|
||||
|
||||
validate_method = "validate_#{name}"
|
||||
if self.respond_to? validate_method
|
||||
send(validate_method, val)
|
||||
end
|
||||
end
|
||||
|
||||
def get_data_type(name, val)
|
||||
# Some types are just for validations like email.
|
||||
# Only consider it valid if includes in `types`
|
||||
if (static_type = @static_types[name.to_sym])
|
||||
return self.class.types[static_type] if self.class.types.keys.include?(static_type)
|
||||
end
|
||||
|
||||
self.class.parse_value_type(val)
|
||||
end
|
||||
|
||||
def enum_class(name)
|
||||
@enums[name]
|
||||
end
|
||||
|
||||
def validator_for(type_name)
|
||||
case type_name
|
||||
when self.class.types[:email]
|
||||
EmailSettingValidator
|
||||
when self.class.types[:username]
|
||||
UsernameSettingValidator
|
||||
when self.class.types[:integer]
|
||||
IntegerSettingValidator
|
||||
when self.class.types[:regex]
|
||||
RegexSettingValidator
|
||||
when self.class.types[:string], self.class.types[:list], self.class.types[:enum]
|
||||
StringSettingValidator
|
||||
else nil
|
||||
end
|
||||
end
|
||||
end
|
66
lib/site_settings/validations.rb
Normal file
66
lib/site_settings/validations.rb
Normal file
@ -0,0 +1,66 @@
|
||||
module SiteSettings; end
|
||||
|
||||
module SiteSettings::Validations
|
||||
def validate_error(key)
|
||||
raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}"))
|
||||
end
|
||||
|
||||
def validate_min_username_length(new_val)
|
||||
validate_error :min_username_length_range if new_val > SiteSetting.max_username_length
|
||||
validate_error :min_username_length_exists if User.where('length(username) < ?', new_val).exists?
|
||||
end
|
||||
|
||||
def validate_max_username_length(new_val)
|
||||
validate_error :min_username_length_range if new_val < SiteSetting.min_username_length
|
||||
validate_error :max_username_length_exists if User.where('length(username) > ?', new_val).exists?
|
||||
end
|
||||
|
||||
def validate_default_categories(new_val, default_categories_selected)
|
||||
validate_error :default_categories_already_selected if (new_val.split("|").to_set & default_categories_selected).size > 0
|
||||
end
|
||||
|
||||
def validate_default_categories_watching(new_val)
|
||||
default_categories_selected = [
|
||||
SiteSetting.default_categories_tracking.split("|"),
|
||||
SiteSetting.default_categories_muted.split("|"),
|
||||
SiteSetting.default_categories_watching_first_post.split("|")
|
||||
].flatten.to_set
|
||||
|
||||
validate_default_categories(new_val, default_categories_selected)
|
||||
end
|
||||
|
||||
def validate_default_categories_tracking(new_val)
|
||||
default_categories_selected = [
|
||||
SiteSetting.default_categories_watching.split("|"),
|
||||
SiteSetting.default_categories_muted.split("|"),
|
||||
SiteSetting.default_categories_watching_first_post.split("|")
|
||||
].flatten.to_set
|
||||
|
||||
validate_default_categories(new_val, default_categories_selected)
|
||||
end
|
||||
|
||||
def validate_default_categories_muted(new_val)
|
||||
default_categories_selected = [
|
||||
SiteSetting.default_categories_watching.split("|"),
|
||||
SiteSetting.default_categories_tracking.split("|"),
|
||||
SiteSetting.default_categories_watching_first_post.split("|")
|
||||
].flatten.to_set
|
||||
|
||||
validate_default_categories(new_val, default_categories_selected)
|
||||
end
|
||||
|
||||
def validate_default_categories_watching_first_post(new_val)
|
||||
default_categories_selected = [
|
||||
SiteSetting.default_categories_watching.split("|"),
|
||||
SiteSetting.default_categories_tracking.split("|"),
|
||||
SiteSetting.default_categories_muted.split("|")
|
||||
].flatten.to_set
|
||||
|
||||
validate_default_categories(new_val, default_categories_selected)
|
||||
end
|
||||
|
||||
def validate_enable_s3_uploads(new_val)
|
||||
validate_error :s3_upload_bucket_is_required if new_val == "t" && SiteSetting.s3_upload_bucket.blank?
|
||||
end
|
||||
|
||||
end
|
@ -1,32 +1,26 @@
|
||||
module SiteSettings; end
|
||||
|
||||
class SiteSettings::YamlLoader
|
||||
|
||||
def initialize(file)
|
||||
@file = file
|
||||
end
|
||||
|
||||
def env_val(value)
|
||||
if value.is_a?(Hash)
|
||||
value.has_key?(Rails.env) ? value[Rails.env] : value['default']
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def load
|
||||
yaml = YAML.load_file(@file)
|
||||
yaml.each_key do |category|
|
||||
yaml[category].each do |setting_name, hash|
|
||||
if hash.is_a?(Hash)
|
||||
# Get default value for the site setting:
|
||||
value = env_val(hash.delete('default'))
|
||||
|
||||
if hash.key?('hidden')
|
||||
hash['hidden'] = env_val(hash.delete('hidden'))
|
||||
value = hash.delete('default')
|
||||
if value.is_a?(Hash)
|
||||
raise Discourse::Deprecation, "Site setting per env is no longer supported. Error setting: #{setting_name}"
|
||||
end
|
||||
|
||||
yield category, setting_name, value, hash.symbolize_keys!
|
||||
if hash['hidden']&.is_a?(Hash)
|
||||
raise Discourse::Deprecation, "Hidden site setting per env is no longer supported. Error setting: #{setting_name}"
|
||||
end
|
||||
|
||||
yield category, setting_name, value, hash.deep_symbolize_keys!
|
||||
else
|
||||
# Simplest case. site_setting_name: 'default value'
|
||||
yield category, setting_name, hash, {}
|
||||
|
Reference in New Issue
Block a user