docbrown/app/models/settings/base.rb
Joshua Wehner 6a48b486ca
Onboarding: custom newsletter opt-in settings (#20114)
* Settings can process markdown into html

* Update Settings with new onboarding settings

* Async render onboarding newsletter step

* Tweak onboarding design

* Fix broken spec

* Better rendered component test

* Tweaks to match design

* Try to tweak design

* Try having a default state

* Tweak placeholder content

* Better await componentDidMount

* Continue to tweak the design

* ContentRenderer#process should always return Result

* Try more clarity in the partial

* Rubocop
2023-09-26 06:34:19 -04:00

179 lines
5 KiB
Ruby

# This class was adapted from the rails-settings-cached gem.
# See: https://github.com/huacnlee/rails-settings-cached
#
# It has changed in several significant ways:
# * renamed DSL method from "field" to "setting"
# * refactored request caching to allow for more than one settings model
# * removed features like cache prefixes and readonly fields
# * changed code in accordance with Rubocop and our internal practices
module Settings
class ProtectedKeyError < ArgumentError; end
class Base < ApplicationRecord
self.abstract_class = true
PROTECTED_KEYS = %w[var value].freeze
SEPARATOR_REGEXP = /[\n,;]+/
after_commit :clear_cache, on: %i[create update destroy]
class << self
def clear_cache
RequestStore.delete(cache_key)
Rails.cache.delete(cache_key)
end
def setting(key, default: nil, type: :string, separator: nil, validates: nil)
define_setting(
key,
default: default,
type: type,
separator: separator,
validates: validates,
)
end
def get_setting(key)
@defined_settings.detect { |setting| setting[:key] == key.to_s } || {}
end
def get_default(key)
get_setting(key)[:default]
end
def keys
@defined_settings.pluck(:key)
end
def to_h
keys.to_h { |k| [k.to_sym, public_send(k)] }
end
private
def cache_key
@cache_key ||= name.underscore
end
def define_setting(key, default: nil, type: :string, separator: nil, validates: nil)
key = key.to_s
raise(ProtectedKeyError, "Can't use '#{key}' as setting name") if key.in?(PROTECTED_KEYS)
@defined_settings ||= []
@defined_settings << {
key: key,
default: default,
type: type || :string
}
# Getter
define_singleton_method(key) do
result = __send__(:value_of, key)
if result.nil? # we don't want to accidentally do this for "false"
result ||= default.is_a?(Proc) ? default.call : default
end
read_as_type = type == :markdown ? :string : type
result = __send__(:convert_string_to_value_type, read_as_type, result, separator: separator)
result
end
# Setter
define_singleton_method("#{key}=") do |value|
var_name = key
record = find_by(var: var_name) || new(var: var_name)
if type == :markdown
processed = __send__(:convert_string_to_value_type, type, value)
record.value = value
record.save!
__send__(:"#{key}_processed_html=", processed)
else
value = __send__(:convert_string_to_value_type, type, value, separator: separator)
record.value = value
record.save!
end
value
end
# Validation
if validates
validates[:if] = proc { |item| item.var.to_s == key }
__send__(:validates, key, **validates)
define_method(:read_attribute_for_validation) { |_key| self.value }
end
return unless type == :boolean
# Predicate method for booleans
define_singleton_method("#{key}?") { __send__(key) }
end
def convert_string_to_value_type(type, value, separator: nil)
return value unless value.class.in?([String, Integer, Float, BigDecimal])
case type
when :boolean
value.in?(["true", "1", 1, true])
when :array
value.split(separator || SEPARATOR_REGEXP).compact_blank.map(&:strip)
when :hash
value = begin
YAML.safe_load(value).to_h
rescue StandardError
{}
end
value.deep_stringify_keys!
ActiveSupport::HashWithIndifferentAccess.new(value)
when :integer
value.to_i
when :float
value.to_f
when :big_decimal
value.to_d
when :markdown
ContentRenderer.new(value).process.processed_html
else
value
end
end
def value_of(var_name)
unless table_exists?
# Fallback to default value if table was not ready (before migrate)
Rails.logger.warn("'#{table_name}' does not exist, '#{name}.#{var_name}' will return the default value.")
return
end
all_settings[var_name]
end
def all_settings
RequestStore[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 1.week) do
unscoped.select(:var, :value).each_with_object({}) do |record, result|
result[record.var] = record.value
end.with_indifferent_access
end
end
end
# get the setting's value, YAML decoded
def value
YAML.unsafe_load(self[:value]) if self[:value].present?
end
# set the settings's value, YAML encoded
def value=(new_value)
self[:value] = new_value.to_yaml
end
def clear_cache
self.class.clear_cache
end
end
end