dm_preferences 0.5.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.rdoc +86 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +224 -0
- data/Rakefile +36 -0
- data/app/models/preference.rb +65 -0
- data/dm_preferences.gemspec +21 -0
- data/lib/dm_preferences.rb +1 -0
- data/lib/generators/USAGE +5 -0
- data/lib/generators/preferences_generator.rb +17 -0
- data/lib/generators/templates/create_preferences.rb +12 -0
- data/lib/preferences.rb +634 -0
- data/lib/preferences/engine.rb +4 -0
- data/lib/preferences/preference_definition.rb +56 -0
- data/lib/preferences/version.rb +3 -0
- data/test/app_root/app/models/car.rb +2 -0
- data/test/app_root/app/models/employee.rb +2 -0
- data/test/app_root/app/models/manager.rb +3 -0
- data/test/app_root/app/models/user.rb +8 -0
- data/test/app_root/db/migrate/001_create_users.rb +11 -0
- data/test/app_root/db/migrate/002_create_cars.rb +11 -0
- data/test/app_root/db/migrate/003_create_employees.rb +12 -0
- data/test/app_root/db/migrate/004_migrate_preferences_to_version_1.rb +13 -0
- data/test/factory.rb +65 -0
- data/test/functional/preferences_test.rb +1387 -0
- data/test/test_helper.rb +26 -0
- data/test/unit/preference_definition_test.rb +237 -0
- data/test/unit/preference_test.rb +259 -0
- metadata +124 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'preferences/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "dm_preferences"
|
6
|
+
s.version = Preferences::VERSION
|
7
|
+
s.authors = ['Brett Walker', 'Aaron Pfeifer']
|
8
|
+
s.email = 'github@digitalmoksha.com'
|
9
|
+
s.description = "Adds support for easily creating custom preferences for ActiveRecord models"
|
10
|
+
s.summary = "Custom preferences for ActiveRecord models"
|
11
|
+
s.homepage = 'https://github.com/digitalmoksha/preferences'
|
12
|
+
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
16
|
+
s.rdoc_options = %w(--line-numbers --inline-source --title preferences --main README.rdoc)
|
17
|
+
s.extra_rdoc_files = %w(README.rdoc CHANGELOG.rdoc LICENSE)
|
18
|
+
|
19
|
+
s.add_development_dependency("rake")
|
20
|
+
s.add_development_dependency("plugin_test_helper", ">= 0.3.2")
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'preferences'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class PreferencesGenerator < Rails::Generators::Base
|
2
|
+
include Rails::Generators::Migration
|
3
|
+
|
4
|
+
source_root File.expand_path("../templates", __FILE__)
|
5
|
+
|
6
|
+
def self.next_migration_number(dirname)
|
7
|
+
if ActiveRecord::Base.timestamped_migrations
|
8
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
9
|
+
else
|
10
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template 'create_preferences.rb', "db/migrate/create_preferences.rb"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreatePreferences < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :preferences do |t|
|
4
|
+
t.string :name, :null => false
|
5
|
+
t.references :owner, :polymorphic => true, :null => false
|
6
|
+
t.references :group, :polymorphic => true
|
7
|
+
t.string :value
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
add_index :preferences, [:owner_id, :owner_type, :name, :group_id, :group_type], :unique => true, :name => 'index_preferences_on_owner_and_name_and_preference'
|
11
|
+
end
|
12
|
+
end
|
data/lib/preferences.rb
ADDED
@@ -0,0 +1,634 @@
|
|
1
|
+
require 'preferences/engine'
|
2
|
+
require 'preferences/preference_definition'
|
3
|
+
|
4
|
+
# Adds support for defining preferences on ActiveRecord models.
|
5
|
+
#
|
6
|
+
# == Saving preferences
|
7
|
+
#
|
8
|
+
# Preferences are not automatically saved when they are set. You must save
|
9
|
+
# the record that the preferences were set on.
|
10
|
+
#
|
11
|
+
# For example,
|
12
|
+
#
|
13
|
+
# class User < ActiveRecord::Base
|
14
|
+
# preference :notifications
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# u = User.new(:login => 'admin', :prefers_notifications => false)
|
18
|
+
# u.save!
|
19
|
+
#
|
20
|
+
# u = User.find_by_login('admin')
|
21
|
+
# u.attributes = {:prefers_notifications => true}
|
22
|
+
# u.save!
|
23
|
+
#
|
24
|
+
# == Validations
|
25
|
+
#
|
26
|
+
# Since the generated accessors for a preference allow the preference to be
|
27
|
+
# treated just like regular ActiveRecord attributes, they can also be
|
28
|
+
# validated against in the same way. For example,
|
29
|
+
#
|
30
|
+
# class User < ActiveRecord::Base
|
31
|
+
# preference :color, :string
|
32
|
+
#
|
33
|
+
# validates_presence_of :preferred_color
|
34
|
+
# validates_inclusion_of :preferred_color, :in => %w(red green blue)
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# u = User.new
|
38
|
+
# u.valid? # => false
|
39
|
+
# u.errors.on(:preferred_color) # => "can't be blank"
|
40
|
+
#
|
41
|
+
# u.preferred_color = 'white'
|
42
|
+
# u.valid? # => false
|
43
|
+
# u.errors.on(:preferred_color) # => "is not included in the list"
|
44
|
+
#
|
45
|
+
# u.preferred_color = 'red'
|
46
|
+
# u.valid? # => true
|
47
|
+
module Preferences
|
48
|
+
module MacroMethods
|
49
|
+
# Defines a new preference for all records in the model. By default,
|
50
|
+
# preferences are assumed to have a boolean data type, so all values will
|
51
|
+
# be typecasted to true/false based on ActiveRecord rules.
|
52
|
+
#
|
53
|
+
# Configuration options:
|
54
|
+
# * <tt>:default</tt> - The default value for the preference. Default is nil.
|
55
|
+
# * <tt>:group_defaults</tt> - Defines the default values to use for various
|
56
|
+
# groups. This should map group_name -> defaults. For ActiveRecord groups,
|
57
|
+
# use the class name.
|
58
|
+
#
|
59
|
+
# == Examples
|
60
|
+
#
|
61
|
+
# The example below shows the various ways to define a preference for a
|
62
|
+
# particular model.
|
63
|
+
#
|
64
|
+
# class User < ActiveRecord::Base
|
65
|
+
# preference :notifications, :default => false
|
66
|
+
# preference :color, :string, :default => 'red', :group_defaults => {:car => 'black'}
|
67
|
+
# preference :favorite_number, :integer
|
68
|
+
# preference :data, :any # Allows any data type to be stored
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# All preferences are also inherited by subclasses.
|
72
|
+
#
|
73
|
+
# == Associations
|
74
|
+
#
|
75
|
+
# After the first preference is defined, the following associations are
|
76
|
+
# created for the model:
|
77
|
+
# * +stored_preferences+ - A collection of all the custom preferences
|
78
|
+
# specified for a record. This will not include default preferences
|
79
|
+
# unless they have been explicitly set.
|
80
|
+
#
|
81
|
+
# == Named scopes
|
82
|
+
#
|
83
|
+
# In addition to the above associations, the following named scopes get
|
84
|
+
# generated for the model:
|
85
|
+
# * +with_preferences+ - Finds all records with a given set of preferences
|
86
|
+
# * +without_preferences+ - Finds all records without a given set of preferences
|
87
|
+
#
|
88
|
+
# In addition to utilizing preferences stored in the database, each of the
|
89
|
+
# above scopes also take into account the defaults that have been defined
|
90
|
+
# for each preference.
|
91
|
+
#
|
92
|
+
# Example:
|
93
|
+
#
|
94
|
+
# User.with_preferences(:notifications => true)
|
95
|
+
# User.with_preferences(:notifications => true, :color => 'blue')
|
96
|
+
#
|
97
|
+
# # Searching with group preferences
|
98
|
+
# car = Car.find(:first)
|
99
|
+
# User.with_preferences(car => {:color => 'blue'})
|
100
|
+
# User.with_preferences(:notifications => true, car => {:color => 'blue'})
|
101
|
+
#
|
102
|
+
# == Generated accessors
|
103
|
+
#
|
104
|
+
# In addition to calling <tt>prefers?</tt> and +preferred+ on a record,
|
105
|
+
# you can also use the shortcut accessor methods that are generated when a
|
106
|
+
# preference is defined. For example,
|
107
|
+
#
|
108
|
+
# class User < ActiveRecord::Base
|
109
|
+
# preference :notifications
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# ...generates the following methods:
|
113
|
+
# * <tt>prefers_notifications?</tt> - Whether a value has been specified, i.e. <tt>record.prefers?(:notifications)</tt>
|
114
|
+
# * <tt>prefers_notifications</tt> - The actual value stored, i.e. <tt>record.prefers(:notifications)</tt>
|
115
|
+
# * <tt>prefers_notifications=(value)</tt> - Sets a new value, i.e. <tt>record.write_preference(:notifications, value)</tt>
|
116
|
+
# * <tt>prefers_notifications_changed?</tt> - Whether the preference has unsaved changes
|
117
|
+
# * <tt>prefers_notifications_was</tt> - The last saved value for the preference
|
118
|
+
# * <tt>prefers_notifications_change</tt> - A list of [original_value, new_value] if the preference has changed
|
119
|
+
# * <tt>prefers_notifications_will_change!</tt> - Forces the preference to get updated
|
120
|
+
# * <tt>reset_prefers_notifications!</tt> - Reverts any unsaved changes to the preference
|
121
|
+
#
|
122
|
+
# ...and the equivalent +preferred+ methods:
|
123
|
+
# * <tt>preferred_notifications?</tt>
|
124
|
+
# * <tt>preferred_notifications</tt>
|
125
|
+
# * <tt>preferred_notifications=(value)</tt>
|
126
|
+
# * <tt>preferred_notifications_changed?</tt>
|
127
|
+
# * <tt>preferred_notifications_was</tt>
|
128
|
+
# * <tt>preferred_notifications_change</tt>
|
129
|
+
# * <tt>preferred_notifications_will_change!</tt>
|
130
|
+
# * <tt>reset_preferred_notifications!</tt>
|
131
|
+
#
|
132
|
+
# Notice that there are two tenses used depending on the context of the
|
133
|
+
# preference. Conventionally, <tt>prefers_notifications?</tt> is better
|
134
|
+
# for accessing boolean preferences, while +preferred_color+ is better for
|
135
|
+
# accessing non-boolean preferences.
|
136
|
+
#
|
137
|
+
# Example:
|
138
|
+
#
|
139
|
+
# user = User.find(:first)
|
140
|
+
# user.prefers_notifications? # => false
|
141
|
+
# user.prefers_notifications # => false
|
142
|
+
# user.preferred_color? # => true
|
143
|
+
# user.preferred_color # => 'red'
|
144
|
+
# user.preferred_color = 'blue' # => 'blue'
|
145
|
+
#
|
146
|
+
# user.prefers_notifications = true
|
147
|
+
#
|
148
|
+
# car = Car.find(:first)
|
149
|
+
# user.preferred_color = 'red', car # => 'red'
|
150
|
+
# user.preferred_color(car) # => 'red'
|
151
|
+
# user.preferred_color?(car) # => true
|
152
|
+
#
|
153
|
+
# user.save! # => true
|
154
|
+
def preference(name, *args)
|
155
|
+
unless included_modules.include?(InstanceMethods)
|
156
|
+
class_attribute :preference_definitions
|
157
|
+
self.preference_definitions = {}
|
158
|
+
|
159
|
+
has_many :stored_preferences, :as => :owner, :class_name => 'Preference', :dependent => :destroy
|
160
|
+
|
161
|
+
after_save :update_preferences
|
162
|
+
|
163
|
+
# Named scopes
|
164
|
+
scope :with_preferences, lambda {|preferences| build_preference_scope(preferences)}
|
165
|
+
scope :without_preferences, lambda {|preferences| build_preference_scope(preferences, true)}
|
166
|
+
|
167
|
+
extend Preferences::ClassMethods
|
168
|
+
include Preferences::InstanceMethods
|
169
|
+
end
|
170
|
+
|
171
|
+
# Create the definition
|
172
|
+
name = name.to_s
|
173
|
+
definition = PreferenceDefinition.new(name, *args)
|
174
|
+
self.preference_definitions[name] = definition
|
175
|
+
|
176
|
+
# Create short-hand accessor methods, making sure that the name
|
177
|
+
# is method-safe in terms of what characters are allowed
|
178
|
+
name = name.gsub(/[^A-Za-z0-9_-]/, '').underscore
|
179
|
+
|
180
|
+
# Query lookup
|
181
|
+
define_method("preferred_#{name}?") do |*group|
|
182
|
+
preferred?(name, group.first)
|
183
|
+
end
|
184
|
+
alias_method "prefers_#{name}?", "preferred_#{name}?"
|
185
|
+
|
186
|
+
# Reader
|
187
|
+
define_method("preferred_#{name}") do |*group|
|
188
|
+
preferred(name, group.first)
|
189
|
+
end
|
190
|
+
alias_method "prefers_#{name}", "preferred_#{name}"
|
191
|
+
|
192
|
+
# Writer
|
193
|
+
define_method("preferred_#{name}=") do |*args|
|
194
|
+
write_preference(*args.flatten.unshift(name))
|
195
|
+
end
|
196
|
+
alias_method "prefers_#{name}=", "preferred_#{name}="
|
197
|
+
|
198
|
+
# Changes
|
199
|
+
define_method("preferred_#{name}_changed?") do |*group|
|
200
|
+
preference_changed?(name, group.first)
|
201
|
+
end
|
202
|
+
alias_method "prefers_#{name}_changed?", "preferred_#{name}_changed?"
|
203
|
+
|
204
|
+
define_method("preferred_#{name}_was") do |*group|
|
205
|
+
preference_was(name, group.first)
|
206
|
+
end
|
207
|
+
alias_method "prefers_#{name}_was", "preferred_#{name}_was"
|
208
|
+
|
209
|
+
define_method("preferred_#{name}_change") do |*group|
|
210
|
+
preference_change(name, group.first)
|
211
|
+
end
|
212
|
+
alias_method "prefers_#{name}_change", "preferred_#{name}_change"
|
213
|
+
|
214
|
+
define_method("preferred_#{name}_will_change!") do |*group|
|
215
|
+
preference_will_change!(name, group.first)
|
216
|
+
end
|
217
|
+
alias_method "prefers_#{name}_will_change!", "preferred_#{name}_will_change!"
|
218
|
+
|
219
|
+
define_method("reset_preferred_#{name}!") do |*group|
|
220
|
+
reset_preference!(name, group.first)
|
221
|
+
end
|
222
|
+
alias_method "reset_prefers_#{name}!", "reset_preferred_#{name}!"
|
223
|
+
|
224
|
+
definition
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
module ClassMethods #:nodoc:
|
229
|
+
# Generates the scope for looking under records with a specific set of
|
230
|
+
# preferences associated with them.
|
231
|
+
#
|
232
|
+
# Note thate this is a bit more complicated than usual since the preference
|
233
|
+
# definitions aren't in the database for joins, defaults need to be accounted
|
234
|
+
# for, and querying for the the presence of multiple preferences requires
|
235
|
+
# multiple joins.
|
236
|
+
def build_preference_scope(preferences, inverse = false)
|
237
|
+
joins = []
|
238
|
+
statements = []
|
239
|
+
values = []
|
240
|
+
|
241
|
+
# Flatten the preferences for easier processing
|
242
|
+
preferences = preferences.inject({}) do |result, (group, value)|
|
243
|
+
if value.is_a?(Hash)
|
244
|
+
value.each {|preference, value| result[[group, preference]] = value}
|
245
|
+
else
|
246
|
+
result[[nil, group]] = value
|
247
|
+
end
|
248
|
+
result
|
249
|
+
end
|
250
|
+
|
251
|
+
preferences.each do |(group, preference), value|
|
252
|
+
group_id, group_type = Preference.split_group(group)
|
253
|
+
preference = preference.to_s
|
254
|
+
definition = preference_definitions[preference.to_s]
|
255
|
+
value = definition.type_cast(value)
|
256
|
+
is_default = definition.default_value(group_type) == value
|
257
|
+
|
258
|
+
table = "preferences_#{group_id}_#{group_type}_#{preference}"
|
259
|
+
|
260
|
+
# Since each preference is a different record, they need their own
|
261
|
+
# join so that the proper conditions can be set
|
262
|
+
joins << "LEFT JOIN preferences AS #{table} ON #{table}.owner_id = #{table_name}.#{primary_key} AND " + sanitize_sql(
|
263
|
+
"#{table}.owner_type" => base_class.name.to_s,
|
264
|
+
"#{table}.group_id" => group_id,
|
265
|
+
"#{table}.group_type" => group_type,
|
266
|
+
"#{table}.name" => preference
|
267
|
+
)
|
268
|
+
|
269
|
+
if inverse
|
270
|
+
statements << "#{table}.id IS NOT NULL AND #{table}.value " + (value.nil? ? ' IS NOT NULL' : ' != ?') + (!is_default ? " OR #{table}.id IS NULL" : '')
|
271
|
+
else
|
272
|
+
statements << "#{table}.id IS NOT NULL AND #{table}.value " + (value.nil? ? ' IS NULL' : ' = ?') + (is_default ? " OR #{table}.id IS NULL" : '')
|
273
|
+
end
|
274
|
+
values << value unless value.nil?
|
275
|
+
end
|
276
|
+
|
277
|
+
sql = statements.map! {|statement| "(#{statement})"} * ' AND '
|
278
|
+
self.joins(joins).where(values.unshift(sql))
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
module InstanceMethods
|
283
|
+
def self.included(base) #:nodoc:
|
284
|
+
base.class_eval do
|
285
|
+
alias_method :prefs, :preferences
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Finds all preferences, including defaults, for the current record. If
|
290
|
+
# looking up custom group preferences, then this will include all default
|
291
|
+
# preferences within that particular group as well.
|
292
|
+
#
|
293
|
+
# == Examples
|
294
|
+
#
|
295
|
+
# A user with no stored values:
|
296
|
+
#
|
297
|
+
# user = User.find(:first)
|
298
|
+
# user.preferences
|
299
|
+
# => {"language"=>"English", "color"=>nil}
|
300
|
+
#
|
301
|
+
# A user with stored values for a particular group:
|
302
|
+
#
|
303
|
+
# user.preferred_color = 'red', :cars
|
304
|
+
# user.preferences(:cars)
|
305
|
+
# => {"language=>"English", "color"=>"red"}
|
306
|
+
def preferences(group = nil)
|
307
|
+
preferences = preferences_group(group)
|
308
|
+
|
309
|
+
unless preferences_group_loaded?(group)
|
310
|
+
group_id, group_type = Preference.split_group(group)
|
311
|
+
find_preferences(:group_id => group_id, :group_type => group_type).each do |preference|
|
312
|
+
# fixed: ignore entries in database that are not present in the definition
|
313
|
+
preferences[preference.name] = preference.value unless (preferences.include?(preference.name) || !preference_definitions[preference.name])
|
314
|
+
end
|
315
|
+
|
316
|
+
# Add defaults
|
317
|
+
preference_definitions.each do |name, definition|
|
318
|
+
preferences[name] = definition.default_value(group_type) unless preferences.include?(name)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
preferences.inject({}) do |typed_preferences, (name, value)|
|
323
|
+
typed_preferences[name] = value.nil? ? value : preference_definitions[name].type_cast(value)
|
324
|
+
typed_preferences
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Queries whether or not a value is present for the given preference.
|
329
|
+
# This is dependent on how the value is type-casted.
|
330
|
+
#
|
331
|
+
# == Examples
|
332
|
+
#
|
333
|
+
# class User < ActiveRecord::Base
|
334
|
+
# preference :color, :string, :default => 'red'
|
335
|
+
# end
|
336
|
+
#
|
337
|
+
# user = User.create
|
338
|
+
# user.preferred(:color) # => "red"
|
339
|
+
# user.preferred?(:color) # => true
|
340
|
+
# user.preferred?(:color, 'cars') # => true
|
341
|
+
# user.preferred?(:color, Car.first) # => true
|
342
|
+
#
|
343
|
+
# user.write_preference(:color, nil)
|
344
|
+
# user.preferred(:color) # => nil
|
345
|
+
# user.preferred?(:color) # => false
|
346
|
+
def preferred?(name, group = nil)
|
347
|
+
name = name.to_s
|
348
|
+
assert_valid_preference(name)
|
349
|
+
|
350
|
+
value = preferred(name, group)
|
351
|
+
preference_definitions[name].query(value)
|
352
|
+
end
|
353
|
+
alias_method :prefers?, :preferred?
|
354
|
+
|
355
|
+
# Gets the actual value stored for the given preference, or the default
|
356
|
+
# value if nothing is present.
|
357
|
+
#
|
358
|
+
# == Examples
|
359
|
+
#
|
360
|
+
# class User < ActiveRecord::Base
|
361
|
+
# preference :color, :string, :default => 'red'
|
362
|
+
# end
|
363
|
+
#
|
364
|
+
# user = User.create
|
365
|
+
# user.preferred(:color) # => "red"
|
366
|
+
# user.preferred(:color, 'cars') # => "red"
|
367
|
+
# user.preferred(:color, Car.first) # => "red"
|
368
|
+
#
|
369
|
+
# user.write_preference(:color, 'blue')
|
370
|
+
# user.preferred(:color) # => "blue"
|
371
|
+
def preferred(name, group = nil)
|
372
|
+
name = name.to_s
|
373
|
+
assert_valid_preference(name)
|
374
|
+
|
375
|
+
if preferences_group(group).include?(name)
|
376
|
+
# Value for this group/name has been written, but not saved yet:
|
377
|
+
# grab from the pending values
|
378
|
+
value = preferences_group(group)[name]
|
379
|
+
else
|
380
|
+
# Grab the first preference; if it doesn't exist, use the default value
|
381
|
+
group_id, group_type = Preference.split_group(group)
|
382
|
+
preference = find_preferences(:name => name, :group_id => group_id, :group_type => group_type).first unless preferences_group_loaded?(group)
|
383
|
+
|
384
|
+
value = preference ? preference.value : preference_definitions[name].default_value(group_type)
|
385
|
+
preferences_group(group)[name] = value
|
386
|
+
end
|
387
|
+
|
388
|
+
definition = preference_definitions[name]
|
389
|
+
value = definition.type_cast(value) unless value.nil?
|
390
|
+
value
|
391
|
+
end
|
392
|
+
alias_method :prefers, :preferred
|
393
|
+
|
394
|
+
# Sets a new value for the given preference. The actual Preference record
|
395
|
+
# is *not* created until this record is saved. In this way, preferences
|
396
|
+
# act *exactly* the same as attributes. They can be written to and
|
397
|
+
# validated against, but won't actually be written to the database until
|
398
|
+
# the record is saved.
|
399
|
+
#
|
400
|
+
# == Examples
|
401
|
+
#
|
402
|
+
# user = User.find(:first)
|
403
|
+
# user.write_preference(:color, 'red') # => "red"
|
404
|
+
# user.save!
|
405
|
+
#
|
406
|
+
# user.write_preference(:color, 'blue', Car.first) # => "blue"
|
407
|
+
# user.save!
|
408
|
+
def write_preference(name, value, group = nil)
|
409
|
+
name = name.to_s
|
410
|
+
assert_valid_preference(name)
|
411
|
+
|
412
|
+
preferences_changed = preferences_changed_group(group)
|
413
|
+
if preferences_changed.include?(name)
|
414
|
+
old = preferences_changed[name]
|
415
|
+
preferences_changed.delete(name) unless preference_value_changed?(name, old, value)
|
416
|
+
else
|
417
|
+
old = clone_preference_value(name, group)
|
418
|
+
preferences_changed[name] = old if preference_value_changed?(name, old, value)
|
419
|
+
end
|
420
|
+
|
421
|
+
value = convert_number_column_value(value) if preference_definitions[name].number?
|
422
|
+
preferences_group(group)[name] = preference_definitions[name].type_cast(value)
|
423
|
+
|
424
|
+
value
|
425
|
+
end
|
426
|
+
|
427
|
+
# Whether any attributes have unsaved changes.
|
428
|
+
#
|
429
|
+
# == Examples
|
430
|
+
#
|
431
|
+
# user = User.find(:first)
|
432
|
+
# user.preferences_changed? # => false
|
433
|
+
# user.write_preference(:color, 'red')
|
434
|
+
# user.preferences_changed? # => true
|
435
|
+
# user.save
|
436
|
+
# user.preferences_changed? # => false
|
437
|
+
#
|
438
|
+
# # Groups
|
439
|
+
# user.preferences_changed?(:car) # => false
|
440
|
+
# user.write_preference(:color, 'red', :car)
|
441
|
+
# user.preferences_changed(:car) # => true
|
442
|
+
def preferences_changed?(group = nil)
|
443
|
+
!preferences_changed_group(group).empty?
|
444
|
+
end
|
445
|
+
|
446
|
+
# A list of the preferences that have unsaved changes.
|
447
|
+
#
|
448
|
+
# == Examples
|
449
|
+
#
|
450
|
+
# user = User.find(:first)
|
451
|
+
# user.preferences_changed # => []
|
452
|
+
# user.write_preference(:color, 'red')
|
453
|
+
# user.preferences_changed # => ["color"]
|
454
|
+
# user.save
|
455
|
+
# user.preferences_changed # => []
|
456
|
+
#
|
457
|
+
# # Groups
|
458
|
+
# user.preferences_changed(:car) # => []
|
459
|
+
# user.write_preference(:color, 'red', :car)
|
460
|
+
# user.preferences_changed(:car) # => ["color"]
|
461
|
+
def preferences_changed(group = nil)
|
462
|
+
preferences_changed_group(group).keys
|
463
|
+
end
|
464
|
+
|
465
|
+
# A map of the preferences that have changed in the current object.
|
466
|
+
#
|
467
|
+
# == Examples
|
468
|
+
#
|
469
|
+
# user = User.find(:first)
|
470
|
+
# user.preferred(:color) # => nil
|
471
|
+
# user.preference_changes # => {}
|
472
|
+
#
|
473
|
+
# user.write_preference(:color, 'red')
|
474
|
+
# user.preference_changes # => {"color" => [nil, "red"]}
|
475
|
+
# user.save
|
476
|
+
# user.preference_changes # => {}
|
477
|
+
#
|
478
|
+
# # Groups
|
479
|
+
# user.preferred(:color, :car) # => nil
|
480
|
+
# user.preference_changes(:car) # => {}
|
481
|
+
# user.write_preference(:color, 'red', :car)
|
482
|
+
# user.preference_changes(:car) # => {"color" => [nil, "red"]}
|
483
|
+
def preference_changes(group = nil)
|
484
|
+
preferences_changed(group).inject({}) do |changes, preference|
|
485
|
+
changes[preference] = preference_change(preference, group)
|
486
|
+
changes
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# Reloads the pereferences of this object as well as its attributes
|
491
|
+
def reload(*args) #:nodoc:
|
492
|
+
result = super
|
493
|
+
|
494
|
+
@preferences.clear if @preferences
|
495
|
+
@preferences_changed.clear if @preferences_changed
|
496
|
+
|
497
|
+
result
|
498
|
+
end
|
499
|
+
|
500
|
+
private
|
501
|
+
# Asserts that the given name is a valid preference in this model. If it
|
502
|
+
# is not, then an ArgumentError exception is raised.
|
503
|
+
def assert_valid_preference(name)
|
504
|
+
raise(ArgumentError, "Unknown preference: #{name}") unless preference_definitions.include?(name)
|
505
|
+
end
|
506
|
+
|
507
|
+
# Gets the set of preferences identified by the given group
|
508
|
+
def preferences_group(group)
|
509
|
+
@preferences ||= {}
|
510
|
+
@preferences[group.is_a?(Symbol) ? group.to_s : group] ||= {}
|
511
|
+
end
|
512
|
+
|
513
|
+
# Determines whether the given group of preferences has already been
|
514
|
+
# loaded from the database
|
515
|
+
def preferences_group_loaded?(group)
|
516
|
+
preference_definitions.length == preferences_group(group).length
|
517
|
+
end
|
518
|
+
|
519
|
+
# Generates a clone of the current value stored for the preference with
|
520
|
+
# the given name / group
|
521
|
+
def clone_preference_value(name, group)
|
522
|
+
value = preferred(name, group)
|
523
|
+
value.duplicable? ? value.clone : value
|
524
|
+
rescue TypeError, NoMethodError
|
525
|
+
value
|
526
|
+
end
|
527
|
+
|
528
|
+
# Keeps track of all preferences that have been changed so that they can
|
529
|
+
# be properly updated in the database. Maps group -> preference -> value.
|
530
|
+
def preferences_changed_group(group)
|
531
|
+
@preferences_changed ||= {}
|
532
|
+
@preferences_changed[group.is_a?(Symbol) ? group.to_s : group] ||= {}
|
533
|
+
end
|
534
|
+
|
535
|
+
# Determines whether a preference changed in the given group
|
536
|
+
def preference_changed?(name, group)
|
537
|
+
preferences_changed_group(group).include?(name)
|
538
|
+
end
|
539
|
+
|
540
|
+
# Builds an array of [original_value, new_value] for the given preference.
|
541
|
+
# If the perference did not change, this will return nil.
|
542
|
+
def preference_change(name, group)
|
543
|
+
[preferences_changed_group(group)[name], preferred(name, group)] if preference_changed?(name, group)
|
544
|
+
end
|
545
|
+
|
546
|
+
# Gets the last saved value for the given preference
|
547
|
+
def preference_was(name, group)
|
548
|
+
preference_changed?(name, group) ? preferences_changed_group(group)[name] : preferred(name, group)
|
549
|
+
end
|
550
|
+
|
551
|
+
# Forces the given preference to be saved regardless of whether the value
|
552
|
+
# is actually diferent
|
553
|
+
def preference_will_change!(name, group)
|
554
|
+
preferences_changed_group(group)[name] = clone_preference_value(name, group)
|
555
|
+
end
|
556
|
+
|
557
|
+
# Reverts any unsaved changes to the given preference
|
558
|
+
def reset_preference!(name, group)
|
559
|
+
write_preference(name, preferences_changed_group(group)[name], group) if preference_changed?(name, group)
|
560
|
+
end
|
561
|
+
|
562
|
+
# Determines whether the old value is different from the new value for the
|
563
|
+
# given preference. This will use the typecasted value to determine
|
564
|
+
# equality.
|
565
|
+
def preference_value_changed?(name, old, value)
|
566
|
+
definition = preference_definitions[name]
|
567
|
+
if definition.type == :integer && (old.nil? || old == 0)
|
568
|
+
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
569
|
+
# Hence we don't record it as a change if the value changes from nil to ''.
|
570
|
+
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
571
|
+
# be typecast back to 0 (''.to_i => 0)
|
572
|
+
value = nil if value.blank?
|
573
|
+
else
|
574
|
+
value = definition.type_cast(value)
|
575
|
+
end
|
576
|
+
|
577
|
+
old != value
|
578
|
+
end
|
579
|
+
|
580
|
+
# Updates any preferences that have been changed/added since the record
|
581
|
+
# was last saved
|
582
|
+
def update_preferences
|
583
|
+
if @preferences_changed
|
584
|
+
@preferences_changed.each do |group, preferences|
|
585
|
+
group_id, group_type = Preference.split_group(group)
|
586
|
+
|
587
|
+
preferences.keys.each do |name|
|
588
|
+
# Find an existing preference or build a new one
|
589
|
+
attributes = {:name => name, :group_id => group_id, :group_type => group_type}
|
590
|
+
unless (preference = find_preferences(attributes).first)
|
591
|
+
preference = stored_preferences.build
|
592
|
+
attributes.each_pair { |attribute, value| preference[attribute] = value }
|
593
|
+
end
|
594
|
+
preference.value = preferred(name, group)
|
595
|
+
preference.save!
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
@preferences_changed.clear
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# Finds all stored preferences with the given attributes. This will do a
|
604
|
+
# smart lookup by looking at the in-memory collection if it was eager-
|
605
|
+
# loaded.
|
606
|
+
def find_preferences(attributes)
|
607
|
+
if stored_preferences.loaded?
|
608
|
+
stored_preferences.select do |preference|
|
609
|
+
attributes.all? {|attribute, value| preference[attribute] == value}
|
610
|
+
end
|
611
|
+
else
|
612
|
+
stored_preferences.where(attributes)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
# Was removed from Rails 4, so inlne it here
|
617
|
+
def convert_number_column_value(value)
|
618
|
+
case value
|
619
|
+
when FalseClass
|
620
|
+
0
|
621
|
+
when TrueClass
|
622
|
+
1
|
623
|
+
when String
|
624
|
+
value.presence
|
625
|
+
else
|
626
|
+
value
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
ActiveRecord::Base.class_eval do
|
633
|
+
extend Preferences::MacroMethods
|
634
|
+
end
|