setting_accessors 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +254 -0
- data/Rakefile +2 -0
- data/lib/generators/setting_accessors/install_generator.rb +38 -0
- data/lib/generators/setting_accessors/templates/migration.rb.erb +12 -0
- data/lib/generators/setting_accessors/templates/model.rb.erb +47 -0
- data/lib/generators/setting_accessors/templates/settings.yml +24 -0
- data/lib/setting_accessors.rb +26 -0
- data/lib/setting_accessors/accessor.rb +148 -0
- data/lib/setting_accessors/converter.rb +67 -0
- data/lib/setting_accessors/integration.rb +79 -0
- data/lib/setting_accessors/integration_validator.rb +15 -0
- data/lib/setting_accessors/internal.rb +108 -0
- data/lib/setting_accessors/setting_scaffold.rb +252 -0
- data/lib/setting_accessors/validator.rb +144 -0
- data/lib/setting_accessors/version.rb +3 -0
- data/lib/tasks/setting_accessors_tasks.rake +4 -0
- data/setting_accessors.gemspec +30 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/mailers/.keep +0 -0
- data/test/dummy/app/models/.keep +0 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/setting.rb +59 -0
- data/test/dummy/app/models/user.rb +15 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +25 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +83 -0
- data/test/dummy/config/environments/test.rb +34 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/setting_accessors.rb +1 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config/settings.yml +23 -0
- data/test/dummy/db/migrate/20150102112106_create_users.rb +9 -0
- data/test/dummy/db/migrate/20150102115329_create_settings.rb +12 -0
- data/test/dummy/db/schema.rb +32 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/test/models/setting_test.rb +131 -0
- data/test/dummy/test/models/user_test.rb +5 -0
- data/test/generators/install_generator_test.rb +15 -0
- data/test/setting_accessors_test.rb +4 -0
- data/test/test_helper.rb +23 -0
- metadata +270 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'setting_accessors/version'
|
2
|
+
require 'setting_accessors/accessor'
|
3
|
+
require 'setting_accessors/converter'
|
4
|
+
require 'setting_accessors/integration'
|
5
|
+
require 'setting_accessors/integration_validator'
|
6
|
+
require 'setting_accessors/internal'
|
7
|
+
require 'setting_accessors/setting_scaffold'
|
8
|
+
require 'setting_accessors/validator'
|
9
|
+
|
10
|
+
ActiveRecord::Base.class_eval do
|
11
|
+
include SettingAccessors::Integration
|
12
|
+
end
|
13
|
+
|
14
|
+
module SettingAccessors
|
15
|
+
def self.setting_class
|
16
|
+
self.setting_class_name.constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.setting_class=(klass)
|
20
|
+
@@setting_class = klass.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.setting_class_name
|
24
|
+
(@@setting_class ||= 'Setting').camelize
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
#
|
2
|
+
# Helper class to make accessing record specific settings easier
|
3
|
+
#
|
4
|
+
|
5
|
+
class SettingAccessors::Accessor
|
6
|
+
|
7
|
+
def initialize(record)
|
8
|
+
@record = record
|
9
|
+
@temp_settings = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Gets a setting's value
|
14
|
+
#
|
15
|
+
def [](key)
|
16
|
+
@temp_settings[key.to_sym] || SettingAccessors.setting_class.get(key, @record)
|
17
|
+
end
|
18
|
+
|
19
|
+
def has_key?(key)
|
20
|
+
@temp_settings.has_key?(key.to_sym)
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Writes a setting's value
|
25
|
+
#
|
26
|
+
def []=(key, val)
|
27
|
+
set_value_before_type_cast(key, val)
|
28
|
+
@temp_settings[key.to_sym] = SettingAccessors::Internal.converter(value_type(key)).convert(val)
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Tries to find a setting for this record.
|
33
|
+
# If none is found, will return the default setting value
|
34
|
+
# specified in the setting config file.
|
35
|
+
#
|
36
|
+
def get_or_default(key)
|
37
|
+
self[key] || SettingAccessors.setting_class.get_or_default(key, @record)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Tries to find a setting for this record first.
|
42
|
+
# If none is found, tries to find a global setting with the same name
|
43
|
+
#
|
44
|
+
def get_or_global(key)
|
45
|
+
self[key] || SettingAccessors.setting_class.get(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Tries to find a setting for this record first,
|
50
|
+
# if none is found, it will return the given value instead.
|
51
|
+
#
|
52
|
+
def get_or_value(key, value)
|
53
|
+
self.has_key?(key) ? self[key] : value
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_with_fallback(key, fallback = nil)
|
57
|
+
return self[key] if fallback.nil?
|
58
|
+
|
59
|
+
case fallback.to_s
|
60
|
+
when 'default' then get_or_default(key)
|
61
|
+
when 'global' then get_or_global(key)
|
62
|
+
else get_or_value(key, fallback)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# @return [String] the setting's value type in the +@record+'s context
|
68
|
+
#
|
69
|
+
def value_type(key)
|
70
|
+
SettingAccessors::Internal.setting_value_type(key, @record)
|
71
|
+
end
|
72
|
+
|
73
|
+
#----------------------------------------------------------------
|
74
|
+
# ActiveRecord Helper Methods Emulation
|
75
|
+
#----------------------------------------------------------------
|
76
|
+
|
77
|
+
def value_was(key, fallback = nil)
|
78
|
+
return SettingAccessors.setting_class.get(key, @record) if fallback.nil?
|
79
|
+
|
80
|
+
case fallback.to_s
|
81
|
+
when 'default' then SettingAccessors.setting_class.get_or_default(key, @record)
|
82
|
+
when 'global' then SettingAccessors.setting_class.get(key)
|
83
|
+
else fallback
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def value_changed?(key)
|
88
|
+
self[key] != value_was(key)
|
89
|
+
end
|
90
|
+
|
91
|
+
def value_before_type_cast(key)
|
92
|
+
SettingAccessors::Internal.lookup_nested_hash(@values_before_type_casts, key.to_s) || self[key]
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
#
|
98
|
+
# Keeps a record of the originally set value for a setting before it was
|
99
|
+
# automatically converted.
|
100
|
+
#
|
101
|
+
def set_value_before_type_cast(key, value)
|
102
|
+
@values_before_type_casts ||= {}
|
103
|
+
@values_before_type_casts[key.to_s] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Validates the new setting values.
|
108
|
+
# If there is an accessor for the setting, the errors will be
|
109
|
+
# directly forwarded to it, otherwise to :base
|
110
|
+
#
|
111
|
+
# Please do not call this method directly, use the IntegrationValidator
|
112
|
+
# class instead, e.g.
|
113
|
+
#
|
114
|
+
# validates_with SettingAccessors::IntegrationValidator
|
115
|
+
#
|
116
|
+
def validate!
|
117
|
+
@temp_settings.each do |key, value|
|
118
|
+
validation_errors = SettingAccessors.setting_class.validation_errors(key, value, @record)
|
119
|
+
validation_errors.each do |message|
|
120
|
+
if @record.respond_to?("#{key}=")
|
121
|
+
@record.errors.add(key, message)
|
122
|
+
else
|
123
|
+
@record.errors.add :base, :invalid_setting, :name => key, :message => message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Saves the new setting values into the database
|
131
|
+
# Please note that there is no check if the values changed their
|
132
|
+
# in the meantime.
|
133
|
+
#
|
134
|
+
# Also, this method expects that the settings were validated
|
135
|
+
# before using #validate! and will therefore not perform
|
136
|
+
# validations itself.
|
137
|
+
#
|
138
|
+
def persist!
|
139
|
+
@temp_settings.each do |key, value|
|
140
|
+
Setting.create_or_update(key, value, @record)
|
141
|
+
end
|
142
|
+
flush!
|
143
|
+
end
|
144
|
+
|
145
|
+
def flush!
|
146
|
+
@temp_settings = {}
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#
|
2
|
+
# This class hopefully will hopefully one day mimic ActiveRecord's
|
3
|
+
# attribute assigning methods, meaning that a conversion to the column type
|
4
|
+
# is done as soon as a new value is assigned by the programmer.
|
5
|
+
#
|
6
|
+
# If the value cannot be parsed in the required type, +nil+ is assigned.
|
7
|
+
# Please make sure that you specify the correct validations in settings.yml
|
8
|
+
# to avoid this.
|
9
|
+
#
|
10
|
+
# Currently supported types:
|
11
|
+
# - Fixnum
|
12
|
+
# - String
|
13
|
+
# - Boolean
|
14
|
+
#
|
15
|
+
# If the type is 'polymorphic', it is not converted at all.
|
16
|
+
#
|
17
|
+
class SettingAccessors::Converter
|
18
|
+
|
19
|
+
def initialize(value_type)
|
20
|
+
@value_type = value_type
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Converts the setting's value to the correct type
|
25
|
+
#
|
26
|
+
def convert(new_value)
|
27
|
+
#If the value is set to be polymorphic, we don't have to convert anything.
|
28
|
+
return new_value if @value_type == 'polymorphic'
|
29
|
+
|
30
|
+
parse_method = :"parse_#{@value_type}"
|
31
|
+
|
32
|
+
if private_methods.include?(parse_method)
|
33
|
+
send(parse_method, new_value)
|
34
|
+
else
|
35
|
+
Rails.logger.warn("Invalid Setting type: #{@value_type}")
|
36
|
+
new_value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def parse_boolean(value)
|
43
|
+
case value
|
44
|
+
when TrueClass, FalseClass
|
45
|
+
value
|
46
|
+
when String
|
47
|
+
return true if %w[true 1].include?(value.downcase)
|
48
|
+
return false if %w[false 0].include?(value.downcase)
|
49
|
+
nil
|
50
|
+
when Fixnum
|
51
|
+
return true if value == 1
|
52
|
+
return false if value.zero?
|
53
|
+
nil
|
54
|
+
else
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_integer(value)
|
60
|
+
value.to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_string(value)
|
64
|
+
value.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module SettingAccessors::Integration
|
2
|
+
def self.included(base)
|
3
|
+
base.validates_with SettingAccessors::IntegrationValidator
|
4
|
+
|
5
|
+
#After the main record was saved, we can save its settings.
|
6
|
+
#This is necessary as the record might have been a new record
|
7
|
+
#without an ID yet
|
8
|
+
base.after_save do
|
9
|
+
settings.send(:persist!)
|
10
|
+
end
|
11
|
+
|
12
|
+
base.extend ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
#
|
18
|
+
# Generates a new accessor (=getter and setter) for the given setting
|
19
|
+
#
|
20
|
+
# @param [String, Symbol] setting_name
|
21
|
+
# The setting's name
|
22
|
+
#
|
23
|
+
# @param [Hash] options
|
24
|
+
# Options to customize the behaviour of the generated accessor
|
25
|
+
#
|
26
|
+
# @option options [Symbol, Object] :fallback (nil)
|
27
|
+
# If set to +:default+, the getter will return the setting's default
|
28
|
+
# value if no own value was specified for this record
|
29
|
+
#
|
30
|
+
# If set to +:global+, the getter will try to find a global
|
31
|
+
# setting if no record specific setting was found
|
32
|
+
#
|
33
|
+
# If set to another value, this value is used by default
|
34
|
+
#
|
35
|
+
# If not set at all or to +nil+, the getter will only search for a record specific
|
36
|
+
# setting and return +nil+ if none was specified previously.
|
37
|
+
#
|
38
|
+
def setting_accessor(setting_name, options = {})
|
39
|
+
fallback = options.delete(:fallback)
|
40
|
+
|
41
|
+
SettingAccessors::Internal.set_class_setting(self, setting_name, options)
|
42
|
+
|
43
|
+
#Create a virtual column in the models column hash.
|
44
|
+
#This is currently not absolutely necessary, but will become important once
|
45
|
+
#Time etc. are supported. Otherwise, Rails won't be able to e.g. automatically
|
46
|
+
#create multi-param fields in forms.
|
47
|
+
self.columns_hash[setting_name.to_s] = OpenStruct.new(type: SettingAccessors::Internal.setting_value_type(setting_name, self.new).to_sym)
|
48
|
+
|
49
|
+
#Getter
|
50
|
+
define_method(setting_name) do
|
51
|
+
settings.get_with_fallback(setting_name, fallback)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Setter
|
55
|
+
define_method("#{setting_name}=") do |new_value|
|
56
|
+
settings[setting_name] = new_value
|
57
|
+
end
|
58
|
+
|
59
|
+
#NAME_was
|
60
|
+
define_method("#{setting_name}_was") do
|
61
|
+
settings.value_was(setting_name, fallback)
|
62
|
+
end
|
63
|
+
|
64
|
+
#NAME_before_type_cast
|
65
|
+
define_method("#{setting_name}_before_type_cast") do
|
66
|
+
settings.value_before_type_cast(setting_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
#NAME_changed?
|
70
|
+
define_method("#{setting_name}_changed?") do
|
71
|
+
settings.value_changed?(setting_name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def settings
|
77
|
+
@settings_accessor ||= SettingAccessors::Accessor.new(self)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#
|
2
|
+
# This class handles model validations for assigned records, e.g.
|
3
|
+
# if the settings are accessed using the Accessor class in this module.
|
4
|
+
# Only the new temp values are validated using the setting config.
|
5
|
+
#
|
6
|
+
# The main work is still done in the Accessor class, so we don't have
|
7
|
+
# to access its instance variables here, this class acts as a wrapper
|
8
|
+
# for Rails' validation chain
|
9
|
+
#
|
10
|
+
|
11
|
+
class SettingAccessors::IntegrationValidator < ActiveModel::Validator
|
12
|
+
def validate(record)
|
13
|
+
record.settings.send(:validate!)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
#
|
2
|
+
# This module contains class methods used internally.
|
3
|
+
#
|
4
|
+
module SettingAccessors
|
5
|
+
module Internal
|
6
|
+
|
7
|
+
def self.ensure_nested_hash!(hash, *keys)
|
8
|
+
h = hash
|
9
|
+
keys.each do |key|
|
10
|
+
h[key] ||= {}
|
11
|
+
h = h[key]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.lookup_nested_hash(hash, *keys)
|
16
|
+
return nil if hash.nil?
|
17
|
+
|
18
|
+
h = hash
|
19
|
+
keys.each do |key|
|
20
|
+
return nil if h[key].nil?
|
21
|
+
h = h[key]
|
22
|
+
end
|
23
|
+
h
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Loads information about all settings from YAML file
|
28
|
+
# These are cached in the class so they don't have to be reloaded
|
29
|
+
# every time.
|
30
|
+
#
|
31
|
+
# Note: For development / test, this is flushed every time
|
32
|
+
#
|
33
|
+
def self.global_config
|
34
|
+
if Rails.env.test? || Rails.env.development?
|
35
|
+
YAML.load(File.open(Rails.root.join('config/settings.yml'))).deep_stringify_keys
|
36
|
+
else
|
37
|
+
@@config ||= YAML.load(File.open(Rails.root.join('config/settings.yml'))).deep_stringify_keys
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# @return [TrueClass, FalseClass] +true+ if the setting is defined in config/settings.yml
|
43
|
+
#
|
44
|
+
def self.globally_defined_setting?(setting_name)
|
45
|
+
self.global_config[setting_name.to_s].present?
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Sets a class-specific setting
|
50
|
+
# For global settings, this is done in config/settings.yml
|
51
|
+
# Please do not call this method yourself, it is done automatically
|
52
|
+
# by using setting_accessor in your model class
|
53
|
+
#
|
54
|
+
def self.set_class_setting(klass, setting_name, options = {})
|
55
|
+
@@class_settings ||= {}
|
56
|
+
|
57
|
+
#If there are no options given, the setting *has* to be defined globally.
|
58
|
+
if options.empty? && !self.globally_defined_setting?(setting_name)
|
59
|
+
raise ArgumentError.new "The setting '#{setting_name}' in model '#{klass.to_s}' is neither globally defined nor did it receive options"
|
60
|
+
|
61
|
+
#A setting which is already globally defined, may not be re-defined on class base
|
62
|
+
elsif self.globally_defined_setting?(setting_name) && options.any?
|
63
|
+
raise ArgumentError.new("The setting #{setting_name} is already defined in config/settings.yml and may not be redefined in #{klass}")
|
64
|
+
|
65
|
+
#If the setting is defined on class base, we have to store its options
|
66
|
+
elsif options.any? && !self.globally_defined_setting?(setting_name)
|
67
|
+
self.ensure_nested_hash!(@@class_settings, klass.to_s)
|
68
|
+
@@class_settings[klass.to_s][setting_name.to_s] = options.deep_stringify_keys
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
#
|
74
|
+
# @return [Hash] configuration data regarding this setting
|
75
|
+
#
|
76
|
+
# - If it's a globally defined setting, the value is taken from config/settings.yml
|
77
|
+
# - If it's a setting defined in a setting_accessor call, the information is taken from this call
|
78
|
+
# - Otherwise, an empty hash is returned
|
79
|
+
#
|
80
|
+
def self.setting_data(setting_name, assignable = nil)
|
81
|
+
(assignable && self.get_class_setting(assignable.class, setting_name)) ||
|
82
|
+
self.global_config[setting_name.to_s] ||
|
83
|
+
{}
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# @return [String] the given setting's value type
|
88
|
+
#
|
89
|
+
def self.setting_value_type(*args)
|
90
|
+
self.setting_data(*args)['type'] || 'polymorphic'
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# @return [SettingAccessors::Converter] A value converter for the given type
|
95
|
+
#
|
96
|
+
def self.converter(value_type)
|
97
|
+
@@converters ||= {}
|
98
|
+
@@converters[value_type.to_sym] ||= SettingAccessors::Converter.new(value_type)
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# @return [Hash, NilClass] Information about a class specific setting or +nil+ if it wasn't set before
|
103
|
+
#
|
104
|
+
def self.get_class_setting(klass, setting_name)
|
105
|
+
self.lookup_nested_hash(@@class_settings, klass.to_s, setting_name.to_s)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|