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,252 @@
|
|
1
|
+
#
|
2
|
+
# Helper methods for the chosen setting model
|
3
|
+
# They are in this module to leave the end developer some room for
|
4
|
+
# his own methods in the setting model for his own methods.
|
5
|
+
#
|
6
|
+
|
7
|
+
module SettingAccessors::SettingScaffold
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.validates_with SettingAccessors::Validator
|
12
|
+
base.serialize :value
|
13
|
+
|
14
|
+
base.validates :name,
|
15
|
+
:uniqueness => {:scope => [:assignable_type, :assignable_id]},
|
16
|
+
:presence => true
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
#
|
21
|
+
# Searches for a setting in the database and returns its value
|
22
|
+
#
|
23
|
+
# @param [String, Symbol] name
|
24
|
+
# The setting's name
|
25
|
+
#
|
26
|
+
# @param [ActiveRecord::Base] assignable
|
27
|
+
# If given, the setting searched has to be assigned to the given record
|
28
|
+
# If not given, a global setting is searched
|
29
|
+
#
|
30
|
+
# @return [Object, NilClass]
|
31
|
+
# If a setting is found, **its value** is returned.
|
32
|
+
# If not, +nil+ is returned.
|
33
|
+
#
|
34
|
+
def [](name, assignable = nil)
|
35
|
+
self.setting_record(name, assignable).try(:value)
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :get, :[]
|
39
|
+
|
40
|
+
#
|
41
|
+
# Tries to look the setting up using #get, if no existing setting is found,
|
42
|
+
# the setting's default value is returned.
|
43
|
+
#
|
44
|
+
def get_or_default(name, assignable = nil)
|
45
|
+
if (val = self[name, assignable]).nil?
|
46
|
+
self.new(:name => name, :assignable => assignable).default_value
|
47
|
+
else
|
48
|
+
val
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Creates or updates the setting with the given name
|
54
|
+
#
|
55
|
+
# @param [String, Symbol] name
|
56
|
+
# The setting's name
|
57
|
+
#
|
58
|
+
# @param [Object] value
|
59
|
+
# The new setting value
|
60
|
+
#
|
61
|
+
# @param [ActiveRecord::Base] assignable
|
62
|
+
# The optional record this setting belongs to. If not
|
63
|
+
# given, the setting is global.
|
64
|
+
#
|
65
|
+
# @param [Boolean] return_value
|
66
|
+
# If set to +true+, only the setting's value is returned
|
67
|
+
#
|
68
|
+
# @return [Object, Setting]
|
69
|
+
# Depending on +return_value+ either the newly created Setting record
|
70
|
+
# or the newly assigned value.
|
71
|
+
# This is due to the fact that Setting.my_setting = 'something' should
|
72
|
+
# show the same behaviour as other attribute assigns in the system while
|
73
|
+
# you might still want to get validation errors on custom settings.
|
74
|
+
#
|
75
|
+
# Please note that - if +return_value+ is set to +true+,
|
76
|
+
# #save! is used instead of #save to ensure that validation
|
77
|
+
# errors are noticed by the programmer / user.
|
78
|
+
# As this is usually only the case when coming from method_missing,
|
79
|
+
# it should not happen anyways
|
80
|
+
#
|
81
|
+
# @toto: Bless the rains down in Africa!
|
82
|
+
#
|
83
|
+
def create_or_update(name, value, assignable = nil, return_value = false)
|
84
|
+
setting = self.setting_record(name, assignable)
|
85
|
+
setting ||= self.new(:name => name, :assignable => assignable)
|
86
|
+
setting.set_value(value)
|
87
|
+
|
88
|
+
if return_value
|
89
|
+
setting.save!
|
90
|
+
setting.value
|
91
|
+
else
|
92
|
+
setting.save
|
93
|
+
setting
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Shortcut for #create_or_update
|
99
|
+
#
|
100
|
+
# @param [String, Symbol] name
|
101
|
+
# The setting's name
|
102
|
+
#
|
103
|
+
# The second argument is an optional assignable
|
104
|
+
#
|
105
|
+
def []=(name, *args)
|
106
|
+
assignable = args.size > 1 ? args.first : nil
|
107
|
+
self.create_or_update(name, args.last, assignable, true)
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Creates a new setting for the given name and assignable,
|
112
|
+
# using the setting's default value stored in the config file
|
113
|
+
#
|
114
|
+
# If the setting already exists, its value will not be overridden
|
115
|
+
#
|
116
|
+
# @param [String] name
|
117
|
+
# The setting's name
|
118
|
+
#
|
119
|
+
# @param [ActiveRecord::Base] assignable
|
120
|
+
# An optional assignable
|
121
|
+
#
|
122
|
+
# @example Create a global default setting 'meaning_of_life'
|
123
|
+
# Setting.create_default_setting(:meaning_of_life)
|
124
|
+
#
|
125
|
+
# @example Create a default setting for all users in the system
|
126
|
+
# User.all.each { |u| Setting.create_default_setting(:some_setting, u) }
|
127
|
+
#
|
128
|
+
def create_default_setting(name, assignable = nil)
|
129
|
+
self.create_or_update(name, self.get_or_default(name, assignable), assignable)
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# @return [Object] the default value for the given setting
|
134
|
+
#
|
135
|
+
def get_default_value(name, assignable = nil)
|
136
|
+
self.new(:name => name, :assignable => assignable).default_value
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Looks up a setting record for the given name and assignable
|
141
|
+
# Unlike the other methods here, this one actually returns a Setting object
|
142
|
+
# instead of its value.
|
143
|
+
#
|
144
|
+
# @return [Setting, NilClass] The found setting or nil if not existing
|
145
|
+
#
|
146
|
+
def setting_record(name, assignable = nil)
|
147
|
+
self.find_by(:name => name.to_s, :assignable => assignable)
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
# Tests, if the given value would be valid for the given
|
152
|
+
# setting name. This is done in this class method due to
|
153
|
+
# the process of setting creation through assigned records
|
154
|
+
# which does not allow going the "normal" way of testing whether
|
155
|
+
# a setting was saved correctly or not.
|
156
|
+
#
|
157
|
+
# @return [Array<String>] The validation errors for the setting's value
|
158
|
+
#
|
159
|
+
def validation_errors(name, value, assignable = nil)
|
160
|
+
s = self.new(:name => name, :value => value, :assignable => assignable)
|
161
|
+
s.valid?
|
162
|
+
s.errors.get(:value) || []
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# @return [String] the localized setting name
|
168
|
+
# they are stored in config/locales/settings.LOCALE.yml
|
169
|
+
#
|
170
|
+
def localized_name
|
171
|
+
i18n_lookup(:name)
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# @return [String] the localized setting description
|
176
|
+
# see #localized_name
|
177
|
+
#
|
178
|
+
def localized_description
|
179
|
+
i18n_lookup(:description)
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Performs an I18n lookup in the settings locale.
|
184
|
+
# Class based settings are store in 'settings.CLASS.NAME', globally defined settings
|
185
|
+
# in 'settings.global.NAME'
|
186
|
+
#
|
187
|
+
def i18n_lookup(key, options = {})
|
188
|
+
options[:scope] = [:settings, :global, self.name]
|
189
|
+
options[:scope] = [:settings, self.assignable.class.to_s.underscore, self.name] unless SettingAccessors::Internal.globally_defined_setting?(self.name)
|
190
|
+
I18n.t(key, options)
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# @return [Object] the default value for the current setting
|
195
|
+
#
|
196
|
+
def default_value
|
197
|
+
data['default']
|
198
|
+
end
|
199
|
+
|
200
|
+
#
|
201
|
+
# @return [String] the setting's type as specified in settings.yml
|
202
|
+
# If the setting wasn't specified, a polymorphic type is assumed
|
203
|
+
#
|
204
|
+
def value_type
|
205
|
+
data['type'] || 'polymorphic'
|
206
|
+
end
|
207
|
+
|
208
|
+
#
|
209
|
+
# @return [Object] the setting's value before it was type casted using the defined rule in settings.yml
|
210
|
+
# See #value_before_type_cast for ActiveRecord attributes
|
211
|
+
#
|
212
|
+
# We can't use the name #value_before_type_cast here as it would
|
213
|
+
# shadow ActiveRecord's default one - which might still be needed.
|
214
|
+
#
|
215
|
+
def original_value
|
216
|
+
@original_value || self.value
|
217
|
+
end
|
218
|
+
|
219
|
+
#
|
220
|
+
# Sets the setting's value to the given one
|
221
|
+
# Performs automatic type casts
|
222
|
+
#
|
223
|
+
def set_value(new_value)
|
224
|
+
@original_value = new_value
|
225
|
+
self.value = converter.convert(new_value)
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
def converter
|
231
|
+
@converter ||= SettingAccessors::Internal.converter(value_type)
|
232
|
+
end
|
233
|
+
|
234
|
+
def value_required?
|
235
|
+
!!validations['required']
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
# Accessor to the validations part of the setting's data
|
240
|
+
#
|
241
|
+
def validations
|
242
|
+
data['validates'] || {}
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# @see {SettingAccessors::Internal#setting_data} for more information
|
247
|
+
#
|
248
|
+
def data
|
249
|
+
SettingAccessors::Internal.setting_data(name, assignable)
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
class SettingAccessors::Validator < ActiveModel::Validator
|
2
|
+
|
3
|
+
def validate(record)
|
4
|
+
record.send(:validations).each do |key, requirement|
|
5
|
+
if key.to_s == 'custom'
|
6
|
+
Array(requirement).each do |validation|
|
7
|
+
run_custom_validation(record, validation)
|
8
|
+
end
|
9
|
+
elsif built_in_validation?(key)
|
10
|
+
send("validate_#{key}", record, requirement)
|
11
|
+
else
|
12
|
+
raise ArgumentError.new("The invalid validation '#{key}' was given in model '#{defining_model(record).to_s}'")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def defining_model(record)
|
20
|
+
if SettingAccessors::Internal.globally_defined_setting?(record.name) || !record.assignable
|
21
|
+
SettingAccessors.setting_class
|
22
|
+
else
|
23
|
+
record.assignable.class
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Runs a custom validation method
|
29
|
+
# The method may either be a Proc or an instance method in +record+.+class+
|
30
|
+
#
|
31
|
+
def run_custom_validation(record, proc)
|
32
|
+
case proc
|
33
|
+
when Proc
|
34
|
+
proc.call(record)
|
35
|
+
when Symbol
|
36
|
+
if defining_model(record).respond_to?(proc)
|
37
|
+
defining_model(record).send(proc)
|
38
|
+
else
|
39
|
+
raise ArgumentError.new "The method '#{proc}' was set up as validation method in model '#{defining_model(record).name}', but doesn't exist."
|
40
|
+
end
|
41
|
+
else
|
42
|
+
raise ArgumentError.new "An invalid validations method was given ('#{proc}')"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# @return [TrueClass, FalseClass] +true+ if the given validation
|
48
|
+
# is a built-in one.
|
49
|
+
#
|
50
|
+
def built_in_validation?(validation_name)
|
51
|
+
private_methods.include?("validate_#{validation_name}".to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Validates that the setting's value is given
|
56
|
+
# accepts :allow_blank and :allow_nil as options
|
57
|
+
#
|
58
|
+
def validate_presence(record, requirement)
|
59
|
+
return true unless requirement
|
60
|
+
|
61
|
+
if requirement.is_a?(Hash)
|
62
|
+
if requirement['allow_blank'] && !record.value.nil? ||
|
63
|
+
requirement['allow_nil'] && record.value.nil? ||
|
64
|
+
record.value.present?
|
65
|
+
true
|
66
|
+
else
|
67
|
+
add_error record, :blank
|
68
|
+
false
|
69
|
+
end
|
70
|
+
else
|
71
|
+
add_error_if record.value.nil? || record.value == '', record, :blank
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Validates numericality of the setting's value based on the options given
|
77
|
+
# in settings.yml
|
78
|
+
#
|
79
|
+
def validate_numericality(record, options)
|
80
|
+
#Test if the value is Numeric in any way (float or int)
|
81
|
+
add_error_if(!parse_value_as_numeric(record.value), record, :not_a_number) && return
|
82
|
+
|
83
|
+
#If the validation was set to check for integer values, do that as well
|
84
|
+
add_error_if(options['only_integer'] && !parse_value_as_fixnum(record.value), record, :not_an_integer)
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Validates whether the given value is a valid boolean
|
89
|
+
#
|
90
|
+
def validate_boolean(record, requirement)
|
91
|
+
add_error_if(requirement && parse_value_as_boolean(record.value).nil?, record, :not_a_boolean)
|
92
|
+
end
|
93
|
+
|
94
|
+
#----------------------------------------------------------------
|
95
|
+
# Helper Methods
|
96
|
+
#----------------------------------------------------------------
|
97
|
+
|
98
|
+
def add_error(record, validation, options = {})
|
99
|
+
record.errors.add :value, validation, options
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_error_if(cond, record, validation, options = {})
|
103
|
+
add_error(record, validation, options) if cond
|
104
|
+
cond
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Borrowed from Rails' numericality validator
|
109
|
+
# @return [Float, NilClass] the given String value as a float or nil
|
110
|
+
# if the value was not a valid Numeric
|
111
|
+
#
|
112
|
+
def parse_value_as_numeric(raw_value)
|
113
|
+
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
|
114
|
+
rescue ArgumentError, TypeError
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# Borrowed from Rails' numericality validator
|
120
|
+
#
|
121
|
+
# @return [Fixnum, NilClass] the given String value as an int or nil
|
122
|
+
#
|
123
|
+
def parse_value_as_fixnum(raw_value)
|
124
|
+
raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Tries to parse the given value as a boolean value
|
129
|
+
#
|
130
|
+
# @return [TrueClass, FalseClass, NilClass]
|
131
|
+
#
|
132
|
+
def parse_value_as_boolean(raw_value)
|
133
|
+
case raw_value
|
134
|
+
when TrueClass, FalseClass
|
135
|
+
raw_value
|
136
|
+
when String
|
137
|
+
return true if %w[true 1].include?(raw_value.downcase)
|
138
|
+
return false if %w[false 0].include?(raw_value.downcase)
|
139
|
+
nil
|
140
|
+
else
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'setting_accessors/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'setting_accessors'
|
8
|
+
spec.version = SettingAccessors::VERSION
|
9
|
+
spec.authors = ["Stefan Exner"]
|
10
|
+
spec.email = ["stex@sterex.de"]
|
11
|
+
spec.summary = %q{A global key-value-store and virtual model columns}
|
12
|
+
spec.description = %q{Adds a global key-value-store to Rails applications and allows adding typed columns
|
13
|
+
to model classes without having to change the database layout.}
|
14
|
+
spec.homepage = 'https://www.github.com/stex/setting_accessors'
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.4"
|
24
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
25
|
+
spec.add_development_dependency 'shoulda', '~> 3.5'
|
26
|
+
spec.add_development_dependency 'minitest', '~> 5.5'
|
27
|
+
spec.add_development_dependency 'byebug', '~> 3.5'
|
28
|
+
|
29
|
+
spec.add_dependency 'rails', '~> 4'
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
== README
|
2
|
+
|
3
|
+
This README would normally document whatever steps are necessary to get the
|
4
|
+
application up and running.
|
5
|
+
|
6
|
+
Things you may want to cover:
|
7
|
+
|
8
|
+
* Ruby version
|
9
|
+
|
10
|
+
* System dependencies
|
11
|
+
|
12
|
+
* Configuration
|
13
|
+
|
14
|
+
* Database creation
|
15
|
+
|
16
|
+
* Database initialization
|
17
|
+
|
18
|
+
* How to run the test suite
|
19
|
+
|
20
|
+
* Services (job queues, cache servers, search engines, etc.)
|
21
|
+
|
22
|
+
* Deployment instructions
|
23
|
+
|
24
|
+
* ...
|
25
|
+
|
26
|
+
|
27
|
+
Please feel free to use a different markup language if you do not plan to run
|
28
|
+
<tt>rake doc:app</tt>.
|