tunable 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 +1 -0
- data/Gemfile +4 -0
- data/README.md +115 -0
- data/Rakefile +8 -0
- data/generators/tunable_migration/templates/migration.rb +23 -0
- data/generators/tunable_migration/tunable_migration_generator.rb +7 -0
- data/lib/tunable/core_ext.rb +41 -0
- data/lib/tunable/emoji_fix.rb +5 -0
- data/lib/tunable/hasher.rb +30 -0
- data/lib/tunable/model.rb +260 -0
- data/lib/tunable/normalizer.rb +34 -0
- data/lib/tunable/setting.rb +59 -0
- data/lib/tunable/version.rb +5 -0
- data/lib/tunable.rb +6 -0
- data/spec/schema.rb +17 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/tunable/defaults_spec.rb +143 -0
- data/spec/tunable/normalization_spec.rb +344 -0
- data/spec/tunable/querying_spec.rb +259 -0
- data/spec/tunable/setters_spec.rb +63 -0
- data/tunable.gemspec +27 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8f46c7acc701e5042514bd5e5b38aaf6e7d55f36
|
4
|
+
data.tar.gz: d64dacc173796faffd043002b8984df92f2f744a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f3b123409df4270709860c7f0f02ae43e738450905ef44494664a01ef54c583a5897ca723e8fc120e849c465b1a26fdd5d2060c369551fa59e2a1e479ae497f
|
7
|
+
data.tar.gz: 8c89d8944a34e84aa30c79ac91504aa120c31d6788bbe01f2ecabc61ea98f01a2c0098fae763458df75529a8f09153868d90eb1fea2f6c9b9fabe8f76b8ba1b3
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*~
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
Tunable
|
2
|
+
=======
|
3
|
+
|
4
|
+
Pluggable settings for your AR models.
|
5
|
+
|
6
|
+
The code
|
7
|
+
--------
|
8
|
+
|
9
|
+
Let's set up notification settings for our Users.
|
10
|
+
|
11
|
+
``` rb
|
12
|
+
class User < ActiveRecord::Base
|
13
|
+
include Tunable::Model
|
14
|
+
|
15
|
+
has_settings :notify => :activity, :new_messages, :weekly_report
|
16
|
+
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
Now we can do:
|
21
|
+
|
22
|
+
``` rb
|
23
|
+
user = User.create(:name => 'John Lennon')
|
24
|
+
user.get_setting(:notify, :activity) # => nil
|
25
|
+
|
26
|
+
user.settings = { notify: { activity: true } }
|
27
|
+
user.save
|
28
|
+
|
29
|
+
user.get_setting(:notify, :activity) # => true
|
30
|
+
```
|
31
|
+
|
32
|
+
Tunable also lets you set defaults for your settings. Here's how.
|
33
|
+
|
34
|
+
|
35
|
+
``` rb
|
36
|
+
class User < ActiveRecord::Base
|
37
|
+
include Tunable::Model
|
38
|
+
|
39
|
+
has_settings :notify => {
|
40
|
+
activity: { default: false },
|
41
|
+
new_messages: { default: true },
|
42
|
+
weekly_report: { default: true }
|
43
|
+
}
|
44
|
+
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
Now we can do:
|
49
|
+
|
50
|
+
``` rb
|
51
|
+
user = User.create(:name => 'John Lennon')
|
52
|
+
user.get_setting(:notify, :new_messages) # => true (default value)
|
53
|
+
|
54
|
+
user.settings = { notify: { new_messages: false } }
|
55
|
+
user.save
|
56
|
+
|
57
|
+
user.get_setting(:notify, :new_messages) # => false
|
58
|
+
```
|
59
|
+
|
60
|
+
Tunable also provides a `main_settings` helper that sets up main level settings.
|
61
|
+
|
62
|
+
``` rb
|
63
|
+
class User < ActiveRecord::Base
|
64
|
+
include Tunable::Model
|
65
|
+
|
66
|
+
# in this case we're not setting a default value for the :no_cookies setting
|
67
|
+
main_settings :no_cookies, :language => { :default => 'en' }
|
68
|
+
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
Now let's see what happens.
|
73
|
+
|
74
|
+
|
75
|
+
``` rb
|
76
|
+
user = User.create(:name => 'Paul MacCartney')
|
77
|
+
user.no_cookies # => nil
|
78
|
+
user.language # => 'en'
|
79
|
+
|
80
|
+
user.language = 'es'
|
81
|
+
user.no_cookies = true
|
82
|
+
user.save
|
83
|
+
|
84
|
+
user.no_cookies # => true
|
85
|
+
user.language # => 'es'
|
86
|
+
```
|
87
|
+
|
88
|
+
You can also set a lambda to return the default setting for a model.
|
89
|
+
|
90
|
+
|
91
|
+
```
|
92
|
+
class User < ActiveRecord::Base
|
93
|
+
|
94
|
+
main_settings :layout_color => {
|
95
|
+
:default => lambda { |user| user.is_admin? ? 'black' : 'blue' }
|
96
|
+
}
|
97
|
+
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
Then:
|
102
|
+
|
103
|
+
``` rb
|
104
|
+
user = User.create(:name => 'Ringo Starr', :admin => false)
|
105
|
+
user.layout_color # => 'blue'
|
106
|
+
user.admin = true
|
107
|
+
user.layout_color # => 'black'
|
108
|
+
```
|
109
|
+
|
110
|
+
That's pretty much it.
|
111
|
+
|
112
|
+
Boring stuff
|
113
|
+
------------
|
114
|
+
|
115
|
+
Copyright (c) Fork Ltd. (http://forkhq.com), released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
class TunableMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :settings do |t|
|
4
|
+
t.column :context, :string
|
5
|
+
t.column :key, :string
|
6
|
+
t.column :value, :string
|
7
|
+
|
8
|
+
t.column :settable_id, :integer
|
9
|
+
t.column :settable_type, :string
|
10
|
+
t.column :created_at, :datetime
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :settings, [:settable_id, :settable_type]
|
14
|
+
add_index :settings, [:settable_id, :context, :key], :unique => true
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.down
|
18
|
+
remove_index :settings, :name => "index_settings_on_settable_id_and_settable_type"
|
19
|
+
remove_index :settings, :name => "index_settings_on_settable_id_and_context_and_key"
|
20
|
+
|
21
|
+
drop_table :settings
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'tunable/emoji_fix'
|
2
|
+
|
3
|
+
module Tunable
|
4
|
+
|
5
|
+
module ActiveRecordExtensions
|
6
|
+
|
7
|
+
def import(columns, values, options = {})
|
8
|
+
if columns.length != values[0].length
|
9
|
+
raise ArgumentError "Column and row lengths must match!"
|
10
|
+
end
|
11
|
+
|
12
|
+
columns_array = columns.map { |column| connection.quote_column_name(column) }
|
13
|
+
|
14
|
+
values_array = values.map do |arr|
|
15
|
+
row_values = []
|
16
|
+
arr.each_with_index do |val, i|
|
17
|
+
row_values << connection.quote(val)
|
18
|
+
end
|
19
|
+
row_values.join(',')
|
20
|
+
end
|
21
|
+
|
22
|
+
values_array = values_array.map{ |str| str.gsub(EmojiFix::REGEX, " ") }
|
23
|
+
insert_method = options[:method] || 'INSERT'
|
24
|
+
sql = "#{insert_method} INTO `#{self.table_name}` (#{columns_array.join(',')}) VALUES "
|
25
|
+
|
26
|
+
# sqlite3 does not support multiple insert/replace,
|
27
|
+
# so we need to generate a transaction with separate queries
|
28
|
+
if ActiveRecord::Base.connection.adapter_name.downcase == 'sqlite'
|
29
|
+
values_array.each do |vals|
|
30
|
+
row_sql = "#{sql}(#{vals})"
|
31
|
+
connection.execute(row_sql)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
sql += "(#{values_array.join('),(')})"
|
35
|
+
connection.execute(sql)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
module EmojiFix
|
2
|
+
|
3
|
+
REGEX = /[\u{203C}\u{2049}\u{20E3}\u{2122}\u{2139}\u{2194}-\u{2199}\u{21A9}-\u{21AA}\u{231A}-\u{231B}\u{23E9}-\u{23EC}\u{23F0}\u{23F3}\u{24C2}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2600}-\u{2601}\u{260E}\u{2611}\u{2614}-\u{2615}\u{261D}\u{263A}\u{2648}-\u{2653}\u{2660}\u{2663}\u{2665}-\u{2666}\u{2668}\u{267B}\u{267F}\u{2693}\u{26A0}-\u{26A1}\u{26AA}-\u{26AB}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26CE}\u{26D4}\u{26EA}\u{26F2}-\u{26F3}\u{26F5}\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270C}\u{270F}\u{2712}\u{2714}\u{2716}\u{2728}\u{2733}-\u{2734}\u{2744}\u{2747}\u{274C}\u{274E}\u{2753}-\u{2755}\u{2757}\u{2764}\u{2795}-\u{2797}\u{27A1}\u{27B0}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}\u{1F004}\u{1F0CF}\u{1F170}-\u{1F171}\u{1F17E}-\u{1F17F}\u{1F18E}\u{1F191}-\u{1F19A}\u{1F1E7}-\u{1F1EC}\u{1F1EE}-\u{1F1F0}\u{1F1F3}\u{1F1F5}\u{1F1F7}-\u{1F1FA}\u{1F201}-\u{1F202}\u{1F21A}\u{1F22F}\u{1F232}-\u{1F23A}\u{1F250}-\u{1F251}\u{1F300}-\u{1F320}\u{1F330}-\u{1F335}\u{1F337}-\u{1F37C}\u{1F380}-\u{1F393}\u{1F3A0}-\u{1F3C4}\u{1F3C6}-\u{1F3CA}\u{1F3E0}-\u{1F3F0}\u{1F400}-\u{1F43E}\u{1F440}\u{1F442}-\u{1F4F7}\u{1F4F9}-\u{1F4FC}\u{1F500}-\u{1F507}\u{1F509}-\u{1F53D}\u{1F550}-\u{1F567}\u{1F5FB}-\u{1F640}\u{1F645}-\u{1F64F}\u{1F680}-\u{1F68A}]/
|
4
|
+
|
5
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Tunable
|
2
|
+
|
3
|
+
module Hasher
|
4
|
+
|
5
|
+
def self.flatten(hash, primary_key, secondary_key = nil)
|
6
|
+
if secondary_key.nil?
|
7
|
+
hashify_using(hash, primary_key)
|
8
|
+
else
|
9
|
+
double_hashify_using(hash, primary_key, secondary_key)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def self.hashify_using(hash, key)
|
16
|
+
return {} if hash.empty?
|
17
|
+
Hash[*hash.collect { |v| [v.send(key).to_sym, v.normalized_value] }.flatten]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.double_hashify_using(hash, primary_key, secondary_key)
|
21
|
+
return {} if hash.empty?
|
22
|
+
c = {}
|
23
|
+
hash.collect { |e| c[e.send(primary_key).to_sym] = {} }
|
24
|
+
hash.collect { |e| c[e.send(primary_key).to_sym][e.send(secondary_key).to_sym] = e.normalized_value }
|
25
|
+
c
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'tunable/setting'
|
3
|
+
require 'tunable/hasher'
|
4
|
+
|
5
|
+
module Tunable
|
6
|
+
|
7
|
+
COLUMNS = [:context, :key, :value, :settable_id, :settable_type]
|
8
|
+
|
9
|
+
module Model
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
# class variables for main settings and defaults
|
14
|
+
class_variable_set('@@main_settings', [])
|
15
|
+
class_variable_set('@@default_settings', {})
|
16
|
+
|
17
|
+
# declare relationship
|
18
|
+
has_many :settings, :class_name => "Tunable::Setting", :as => :settable, :dependent => :delete_all
|
19
|
+
|
20
|
+
# and make sure settings are saved after any changes
|
21
|
+
after_save :save_new_settings
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
def default_settings(context)
|
27
|
+
class_variable_get('@@default_settings')[context.to_sym] || {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def main_settings_list
|
31
|
+
class_variable_get('@@main_settings')
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_default_setting(key, field, value)
|
35
|
+
hash = class_variable_get('@@default_settings')
|
36
|
+
hash[key.to_sym] = {} unless hash[key.to_sym]
|
37
|
+
hash[key.to_sym][field.to_sym] = value
|
38
|
+
|
39
|
+
class_variable_set('@@default_settings', hash)
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_settings(contexts)
|
43
|
+
|
44
|
+
contexts.each do |name, options|
|
45
|
+
|
46
|
+
if options.is_a?(Array)
|
47
|
+
res = {}
|
48
|
+
options.each { |el| res[el.keys.first] = el.values.first }
|
49
|
+
else
|
50
|
+
res = options
|
51
|
+
end
|
52
|
+
|
53
|
+
res.each do |field, opts|
|
54
|
+
if opts.is_a?(Hash)
|
55
|
+
default = opts[:default]
|
56
|
+
else
|
57
|
+
default = opts
|
58
|
+
end
|
59
|
+
|
60
|
+
set_default_setting(name, field, default) unless default.nil?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
def main_settings(*options)
|
67
|
+
|
68
|
+
if options[0].is_a?(Hash)
|
69
|
+
fields = options[0]
|
70
|
+
else # no defaults
|
71
|
+
fields = {}
|
72
|
+
options.each { |key| fields[key] = {} }
|
73
|
+
end
|
74
|
+
|
75
|
+
fields.each do |field, opts|
|
76
|
+
|
77
|
+
if opts.is_a?(Hash)
|
78
|
+
strict = opts[:strict]
|
79
|
+
default = opts[:default]
|
80
|
+
else
|
81
|
+
default = opts
|
82
|
+
strict = false
|
83
|
+
end
|
84
|
+
|
85
|
+
main_settings_list.push(field)
|
86
|
+
set_default_setting(:main, field, default) unless default.nil?
|
87
|
+
|
88
|
+
define_method field do
|
89
|
+
if instance_variable_defined?("@setting_main_#{field}")
|
90
|
+
# the instance var is already normalized to 1/0 when called by the setter
|
91
|
+
Tunable.getter_value(instance_variable_get("@setting_main_#{field}"))
|
92
|
+
else
|
93
|
+
current = main_settings[field.to_sym]
|
94
|
+
default_value = default.is_a?(Proc) ? default.call(self) : default
|
95
|
+
current.nil? ? default_value : current
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
define_method "#{field}?" do
|
100
|
+
main_setting_on?(field.to_sym)
|
101
|
+
end
|
102
|
+
|
103
|
+
define_method "#{field}_changed?" do
|
104
|
+
changes[field.to_sym].present?
|
105
|
+
end
|
106
|
+
|
107
|
+
define_method "#{field}=" do |raw_value|
|
108
|
+
|
109
|
+
if strict && !default.nil? && !Tunable.matching_type(raw_value, default)
|
110
|
+
raise "Invalid value: #{raw_value}. Expected #{default.class}, got #{raw_value.class}"
|
111
|
+
end
|
112
|
+
|
113
|
+
value = Tunable.normalize_value(raw_value)
|
114
|
+
current = Tunable.normalize_value(send("#{field}"))
|
115
|
+
# debug "Setting #{field} to #{value} (#{value.class}), current: #{current} (#{current.class})"
|
116
|
+
|
117
|
+
if value === current
|
118
|
+
send('changed_attributes').delete(field) # in case we had set if before
|
119
|
+
return
|
120
|
+
end
|
121
|
+
|
122
|
+
instance_variable_set("@setting_main_#{field}", value)
|
123
|
+
|
124
|
+
if value.nil?
|
125
|
+
main_settings.delete(field.to_sym)
|
126
|
+
queue_setting_for_deletion(:main, field)
|
127
|
+
else
|
128
|
+
main_settings[field.to_sym] = value
|
129
|
+
queue_setting_for_update(:main, field, value)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end # main_settings
|
136
|
+
|
137
|
+
end # ClassMethods
|
138
|
+
|
139
|
+
# instance methods below
|
140
|
+
|
141
|
+
def settings=(hash)
|
142
|
+
puts hash.inspect
|
143
|
+
Tunable::Setting.store_many(hash, self)
|
144
|
+
end
|
145
|
+
|
146
|
+
def settings_hash
|
147
|
+
if modified_settings.any?
|
148
|
+
puts "Settings have been changed. Hash will be incomplete."
|
149
|
+
end
|
150
|
+
|
151
|
+
@object_hashed_settings ||= Hasher.flatten(settings.reload, :context, :key)
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_setting(context, key)
|
155
|
+
val = settings_context(context)[key]
|
156
|
+
|
157
|
+
# if value is nil or no default is set, stop here
|
158
|
+
return val if !val.nil? or self.class.default_settings(context)[key.to_sym].nil?
|
159
|
+
|
160
|
+
self.class.default_settings(context)[key.to_sym]
|
161
|
+
end
|
162
|
+
|
163
|
+
def get_main_setting(key)
|
164
|
+
get_setting(:main, key)
|
165
|
+
end
|
166
|
+
|
167
|
+
def settings_context(context)
|
168
|
+
settings_hash[context.to_sym] || {}
|
169
|
+
end
|
170
|
+
|
171
|
+
def main_settings
|
172
|
+
settings_context(:main)
|
173
|
+
end
|
174
|
+
|
175
|
+
def clear_instance_settings
|
176
|
+
@object_hashed_settings = nil
|
177
|
+
@settings = nil # so settings get reloaded from DB
|
178
|
+
end
|
179
|
+
|
180
|
+
def setting_off?(context, key)
|
181
|
+
get_setting(context, key) == false
|
182
|
+
end
|
183
|
+
|
184
|
+
def setting_on?(context, key)
|
185
|
+
get_setting(context, key) == true
|
186
|
+
end
|
187
|
+
|
188
|
+
def queue_setting_for_update(context, key, val)
|
189
|
+
if self.class.main_settings_list.include?(key.to_sym)
|
190
|
+
send('changed_attributes')[key.to_sym] = val
|
191
|
+
end
|
192
|
+
(modified_settings[context.to_sym] ||= {})[key.to_sym] = val
|
193
|
+
end
|
194
|
+
|
195
|
+
def queue_setting_for_deletion(context, key)
|
196
|
+
if self.class.main_settings_list.include?(key)
|
197
|
+
send('changed_attributes')[key.to_sym] = nil
|
198
|
+
end
|
199
|
+
(deleted_settings[context.to_sym] ||= []) << key.to_sym
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def modified_settings
|
205
|
+
@modified_settings ||= {}
|
206
|
+
end
|
207
|
+
|
208
|
+
def deleted_settings
|
209
|
+
@deleted_settings ||= {}
|
210
|
+
end
|
211
|
+
|
212
|
+
def delete_setting(context, key)
|
213
|
+
Tunable::Setting.delete_all(
|
214
|
+
:context => context.to_s,
|
215
|
+
:key => key.to_s,
|
216
|
+
:settable_type => self.class.model_name.to_s,
|
217
|
+
:settable_id => self.id
|
218
|
+
)
|
219
|
+
end
|
220
|
+
|
221
|
+
def save_new_settings
|
222
|
+
|
223
|
+
if modified_settings.any?
|
224
|
+
# debug "Saving new settings: #{modified_settings.inspect}"
|
225
|
+
new_settings = []
|
226
|
+
|
227
|
+
modified_settings.each do |context, fields|
|
228
|
+
fields.each do |key, value|
|
229
|
+
# even though we do normalize on the setters, not all settings are
|
230
|
+
# main settings, so we need to make sure we normalize here again
|
231
|
+
normalized_value = Tunable.normalize_value(value)
|
232
|
+
|
233
|
+
new_settings << [context.to_s, key.to_s, normalized_value, self.id, self.class.model_name.to_s]
|
234
|
+
# remove_instance_variable("@setting_main_#{key}") if context == :main
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
Tunable::Setting.import(Tunable::COLUMNS, new_settings, { :method => 'REPLACE' }) # from lib/core_ext
|
239
|
+
end
|
240
|
+
|
241
|
+
if deleted_settings.any?
|
242
|
+
# puts deleted_settings.inspect
|
243
|
+
deleted_settings.each do |context, fields|
|
244
|
+
fields.each do |key|
|
245
|
+
delete_setting(context.to_s, key.to_s)
|
246
|
+
# remove_instance_variable("@setting_main_#{key}") if context == :main
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
self.clear_instance_settings
|
252
|
+
@modified_settings = {}
|
253
|
+
@deleted_settings = {}
|
254
|
+
|
255
|
+
true # make sure the other callbacks are triggered
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tunable
|
2
|
+
|
3
|
+
module Normalizer
|
4
|
+
|
5
|
+
TRUTHIES = ['true', 't', 'on', 'yes', 'y', '1'].freeze
|
6
|
+
FALSIES = ['false', 'f', 'off', 'no', 'n', '0'].freeze
|
7
|
+
|
8
|
+
def normalize_value(val)
|
9
|
+
return 1 if TRUTHIES.include?(val.to_s)
|
10
|
+
return 0 if FALSIES.include?(val.to_s)
|
11
|
+
return if val.blank? # false.blank? returns true so this needs to go after the 0 line
|
12
|
+
val
|
13
|
+
end
|
14
|
+
|
15
|
+
def getter_value(normalized)
|
16
|
+
return normalized === 1 ? true : normalized === 0 ? false : normalized
|
17
|
+
end
|
18
|
+
|
19
|
+
# Called from Setting#normalized_value and DeviceActions#toggle_action
|
20
|
+
def normalize_and_get(val)
|
21
|
+
getter_value(normalize_value(val))
|
22
|
+
end
|
23
|
+
|
24
|
+
def matching_type(a, b)
|
25
|
+
if [true, false].include?(a)
|
26
|
+
return [true, false].include?(b)
|
27
|
+
else
|
28
|
+
a.class == b.class
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'tunable/core_ext'
|
2
|
+
|
3
|
+
module Tunable
|
4
|
+
|
5
|
+
class Setting < ActiveRecord::Base
|
6
|
+
extend ActiveRecordExtensions
|
7
|
+
|
8
|
+
belongs_to :settable, :polymorphic => true
|
9
|
+
|
10
|
+
# scope :for_context, lambda { |context|
|
11
|
+
# return if context.blank?
|
12
|
+
# where(:context => context)
|
13
|
+
# }
|
14
|
+
|
15
|
+
# scope :main, lambda { where(:context => 'main') }
|
16
|
+
|
17
|
+
# scope :get, lambda { |key|
|
18
|
+
# return if key.blank?
|
19
|
+
# where(:key => key)
|
20
|
+
# }
|
21
|
+
|
22
|
+
# this method regenerates all settings when updating a device.
|
23
|
+
# we first remove all the settings (we dont get params from disabled modules)
|
24
|
+
def self.store_many(hash, object)
|
25
|
+
wipe_all(object) and return if hash.blank?
|
26
|
+
|
27
|
+
hash.each do |context, fields|
|
28
|
+
fields.each do |key, val|
|
29
|
+
if val.blank? && object.settings_context(context.to_sym)[key.to_sym].present?
|
30
|
+
|
31
|
+
# setting was present and now deleted
|
32
|
+
object.queue_setting_for_deletion(context, key)
|
33
|
+
|
34
|
+
elsif val != object.settings_context(context.to_sym)[key.to_sym]
|
35
|
+
|
36
|
+
# settings different from previous, so update
|
37
|
+
object.queue_setting_for_update(context, key, val)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.wipe_all(object)
|
44
|
+
count = object.settings.where("`context` != 'main'").delete_all
|
45
|
+
# debug "#{count} deleted settings."
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def normalized_value
|
50
|
+
Tunable.normalize_and_get(self[:value])
|
51
|
+
end
|
52
|
+
|
53
|
+
def value=(val)
|
54
|
+
self[:value] = Tunable.normalize_value(val)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/lib/tunable.rb
ADDED
data/spec/schema.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
ActiveRecord::Schema.define :version => 0 do
|
2
|
+
create_table "settings", :force => true do |t|
|
3
|
+
t.integer "settable_id", :limit => 11
|
4
|
+
t.string "settable_type"
|
5
|
+
t.string "context"
|
6
|
+
t.string "key"
|
7
|
+
t.string "value"
|
8
|
+
t.datetime "created_at"
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index "settings", ["settable_id", "context", "key"], name: "index_settings_on_settable_id_and_context_and_key", unique: true, using: :btree
|
12
|
+
add_index "settings", ["settable_id", "settable_type"], name: "index_settings_on_settable_id_and_settable_type", using: :btree
|
13
|
+
|
14
|
+
create_table :tunable_models, :force => true do |t|
|
15
|
+
t.column :name, :string
|
16
|
+
end
|
17
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
"adapter" => "sqlite3", "database" => ':memory:'
|
7
|
+
)
|
8
|
+
|
9
|
+
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
10
|
+
|
11
|
+
this_path = File.dirname(__FILE__)
|
12
|
+
load File.join(this_path, '/schema.rb')
|
13
|
+
require File.join(this_path, '..', 'lib', 'tunable.rb')
|
14
|
+
|
15
|
+
class TunableModel < ActiveRecord::Base
|
16
|
+
include Tunable::Model
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_main_settings!
|
20
|
+
|
21
|
+
TunableModel.main_settings \
|
22
|
+
:boolean_setting,
|
23
|
+
:number_setting,
|
24
|
+
:empty_setting,
|
25
|
+
:on_off_setting,
|
26
|
+
:y_n_setting,
|
27
|
+
:other_setting
|
28
|
+
|
29
|
+
=begin
|
30
|
+
TunableModel.main_settings ({
|
31
|
+
:boolean_setting => { :default => true },
|
32
|
+
:number_setting => { :default => false },
|
33
|
+
:empty_setting => { },
|
34
|
+
:on_off_setting => { },
|
35
|
+
:y_n_setting => { :default => 'y', :strict => false },
|
36
|
+
:other_setting => { }
|
37
|
+
})
|
38
|
+
=end
|
39
|
+
|
40
|
+
end
|