dm_preferences 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ Usage:
2
+
3
+ rails generate preferences
4
+
5
+ This will create a migration that will add the proper table to store 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
@@ -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