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:
Erick Guan
2017-08-02 18:24:19 +02:00
committed by Sam
parent 3de45ce0cd
commit 468a8fcd20
28 changed files with 1373 additions and 364 deletions

View 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

View 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

View File

@ -10,7 +10,7 @@ class SiteSettings::LocalProcessProvider
@settings[current_site] ||= {}
end
def initialize()
def initialize
@settings = {}
self.current_site = "test"
end

View 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

View 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

View File

@ -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, {}