preferences 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,22 @@
1
1
  == master
2
2
 
3
+ == 0.4.0 / 2010-03-07
4
+
5
+ * Add {preference}_changed?, {preference}_was, {preference}_changed, {preference}_will_change!, and reset_{preference}!
6
+ * Add #preferences_changed?, #preferences_changed, and #preference_changes
7
+ * Fix preferences that are reverted externally still getting stored
8
+ * Fix preference definition types not being used to typecast values
9
+ * No longer allow both group and non-group preferences to be looked up at once (except for named scopes)
10
+ * Add support for using Symbols to reference groups
11
+ * Fix #reload not causing unsaved preferences to get reset
12
+ * Raise exception if unknown preference is accessed
13
+ * Rename #set_preference to #write_preference
14
+ * Add caching of preference lookups
15
+ * Fix preferences being stored even if they didn't change
16
+ * Release gems via rake-gemcutter instead of rubyforge
17
+ * Add a generator for db migration to make installation a bit easier [Tim Lowrimore]
18
+ * Add named scopes: #with_preferences and #without_preferences
19
+
3
20
  == 0.3.1 / 2009-04-25
4
21
 
5
22
  * Rename Preference#attribute to #name to avoid conflicts with reserved methods in ActiveRecord
data/README.rdoc CHANGED
@@ -40,6 +40,17 @@ a separate table and making it dead-simple to define and manage preferences.
40
40
 
41
41
  == Usage
42
42
 
43
+ === Installation
44
+
45
+ +preferences+ requires an additional database table to work. You can generate
46
+ a migration for this table like so:
47
+
48
+ script/generate preferences
49
+
50
+ Then simply migrate your database:
51
+
52
+ rake db:migrate
53
+
43
54
  === Defining preferences
44
55
 
45
56
  To define the preferences for a model, you can do so right within the model:
@@ -101,8 +112,8 @@ Reader method:
101
112
  user.preferred(:language) # => "English"
102
113
 
103
114
  Write method:
104
- user.set_preference(:hot_salsa, false) # => false
105
- user.set_preference(:language, "English") # => "English"
115
+ user.write_preference(:hot_salsa, false) # => false
116
+ user.write_preference(:language, "English") # => "English"
106
117
 
107
118
  === Accessing all preferences
108
119
 
@@ -135,7 +146,7 @@ through an example:
135
146
  car = Car.find(:first)
136
147
 
137
148
  user.preferred_color = 'red', car
138
- # user.set_preference(:color, 'red', car) # The generic way
149
+ # user.write_preference(:color, 'red', car) # The generic way
139
150
 
140
151
  This will create a color preference of "red" for the given car. In this way,
141
152
  you can have "color" preferences for different records.
@@ -151,14 +162,13 @@ preferences by name. For example,
151
162
 
152
163
  user = User.find(:first)
153
164
 
154
- user.preferred_color = 'red', 'automobiles'
155
- user.preferred_color = 'tan', 'clothing'
165
+ user.preferred_color = 'red', :automobiles
166
+ user.preferred_color = 'tan', :clothing
156
167
 
157
- user.preferred_color('automobiles') # => "red"
158
- user.preferred_color('clothing') # => "tan"
159
-
160
- user.preferences # => {"color"=>nil, "automobiles"=>{"color"=>"red"}, "clothing=>{"color=>"tan"}}
161
- user.preferences('automobiles') # => {"color"=>"red"}
168
+ user.preferred_color(:automobiles) # => "red"
169
+ user.preferred_color(:clothing) # => "tan"
170
+
171
+ user.preferences(:automobiles) # => {"color"=>"red"}
162
172
 
163
173
  === Saving preferences
164
174
 
@@ -171,6 +181,27 @@ Preferences are treated in a similar fashion to attributes. For example,
171
181
 
172
182
  Preferences are stored in a separate table called "preferences".
173
183
 
184
+ === Tracking changes
185
+
186
+ Similar to ActiveRecord attributes, unsaved changes to preferences can be
187
+ tracked. For example,
188
+
189
+ user.preferred_language # => "English"
190
+ user.preferred_language_changed? # => false
191
+ user.preferred_language = 'Spanish'
192
+ user.preferred_language_changed? # => true
193
+ user.preferred_language_was # => "English"
194
+ user.preferred_language_change # => ["English", "Spanish"]
195
+ user.reset_preferred_language!
196
+ user.preferred_language # => "English"
197
+
198
+ Assigning the same value leaves the preference unchanged:
199
+
200
+ user.preferred_language # => "English"
201
+ user.preferred_language = 'English'
202
+ user.preferred_language_changed? # => false
203
+ user.preferred_language_change # => nil
204
+
174
205
  == Testing
175
206
 
176
207
  Before you can run any tests, the following gem must be installed:
data/Rakefile CHANGED
@@ -1,15 +1,17 @@
1
+ require 'rubygems'
2
+ require 'rake'
1
3
  require 'rake/testtask'
2
4
  require 'rake/rdoctask'
3
5
  require 'rake/gempackagetask'
4
- require 'rake/contrib/sshpublisher'
5
6
 
6
7
  spec = Gem::Specification.new do |s|
7
8
  s.name = 'preferences'
8
- s.version = '0.3.1'
9
+ s.version = '0.4.0'
9
10
  s.platform = Gem::Platform::RUBY
10
- s.summary = 'Adds support for easily creating custom preferences for models'
11
+ s.summary = 'Adds support for easily creating custom preferences for ActiveRecord models'
12
+ s.description = s.summary
11
13
 
12
- s.files = FileList['{app,lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
14
+ s.files = FileList['{app,generators,lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
13
15
  s.require_path = 'lib'
14
16
  s.has_rdoc = true
15
17
  s.test_files = Dir['test/**/*_test.rb']
@@ -52,20 +54,27 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
52
54
  rdoc.options << '--line-numbers' << '--inline-source'
53
55
  rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb', 'app/**/*.rb')
54
56
  end
55
-
57
+
58
+ desc 'Generate a gemspec file.'
59
+ task :gemspec do
60
+ File.open("#{spec.name}.gemspec", 'w') do |f|
61
+ f.write spec.to_ruby
62
+ end
63
+ end
64
+
56
65
  Rake::GemPackageTask.new(spec) do |p|
57
66
  p.gem_spec = spec
58
- p.need_tar = true
59
- p.need_zip = true
60
67
  end
61
68
 
62
69
  desc 'Publish the beta gem.'
63
70
  task :pgem => [:package] do
71
+ require 'rake/contrib/sshpublisher'
64
72
  Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
65
73
  end
66
74
 
67
75
  desc 'Publish the API documentation.'
68
76
  task :pdoc => [:rdoc] do
77
+ require 'rake/contrib/sshpublisher'
69
78
  Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
70
79
  end
71
80
 
@@ -74,15 +83,8 @@ task :publish => [:pgem, :pdoc, :release]
74
83
 
75
84
  desc 'Publish the release files to RubyForge.'
76
85
  task :release => [:gem, :package] do
77
- require 'rubyforge'
78
-
79
- ruby_forge = RubyForge.new.configure
80
- ruby_forge.login
86
+ require 'rake/gemcutter'
81
87
 
82
- %w(gem tgz zip).each do |ext|
83
- file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
84
- puts "Releasing #{File.basename(file)}..."
85
-
86
- ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
87
- end
88
+ Rake::Gemcutter::Tasks.new(spec)
89
+ Rake::Task['gem:push'].invoke
88
90
  end
@@ -29,7 +29,7 @@ class Preference < ActiveRecord::Base
29
29
  if group.is_a?(ActiveRecord::Base)
30
30
  group_id, group_type = group.id, group.class.base_class.name.to_s
31
31
  else
32
- group_id, group_type = nil, group
32
+ group_id, group_type = nil, group.is_a?(Symbol) ? group.to_s : group
33
33
  end
34
34
 
35
35
  [group_id, group_type]
@@ -0,0 +1,5 @@
1
+ Usage:
2
+
3
+ script/generate preferences
4
+
5
+ This will create a migration that will add the proper table to store preferences.
@@ -0,0 +1,7 @@
1
+ class PreferencesGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template '001_create_preferences.rb', 'db/migrate', :migration_file_name => 'create_preferences'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ class CreatePreferences < ActiveRecord::Migration
2
+ def self.up
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
+
13
+ def self.down
14
+ drop_table :preferences
15
+ end
16
+ end
data/lib/preferences.rb CHANGED
@@ -74,6 +74,27 @@ module Preferences
74
74
  # specified for a record. This will not include default preferences
75
75
  # unless they have been explicitly set.
76
76
  #
77
+ # == Named scopes
78
+ #
79
+ # In addition to the above associations, the following named scopes get
80
+ # generated for the model:
81
+ # * +with_preferences+ - Finds all records with a given set of preferences
82
+ # * +without_preferences+ - Finds all records without a given set of preferences
83
+ #
84
+ # In addition to utilizing preferences stored in the database, each of the
85
+ # above scopes also take into account the defaults that have been defined
86
+ # for each preference.
87
+ #
88
+ # Example:
89
+ #
90
+ # User.with_preferences(:notifications => true)
91
+ # User.with_preferences(:notifications => true, :color => 'blue')
92
+ #
93
+ # # Searching with group preferences
94
+ # car = Car.find(:first)
95
+ # User.with_preferences(car => {:color => 'blue'})
96
+ # User.with_preferences(:notifications => true, car => {:color => 'blue'})
97
+ #
77
98
  # == Generated accessors
78
99
  #
79
100
  # In addition to calling <tt>prefers?</tt> and +preferred+ on a record,
@@ -87,10 +108,22 @@ module Preferences
87
108
  # ...generates the following methods:
88
109
  # * <tt>prefers_notifications?</tt> - Whether a value has been specified, i.e. <tt>record.prefers?(:notifications)</tt>
89
110
  # * <tt>prefers_notifications</tt> - The actual value stored, i.e. <tt>record.prefers(:notifications)</tt>
90
- # * <tt>prefers_notifications=(value)</tt> - Sets a new value, i.e. <tt>record.set_preference(:notifications, value)</tt>
91
- # * <tt>preferred_notifications?</tt> - Whether a value has been specified, i.e. <tt>record.preferred?(:notifications)</tt>
92
- # * <tt>preferred_notifications</tt> - The actual value stored, i.e. <tt>record.preferred(:notifications)</tt>
93
- # * <tt>preferred_notifications=(value)</tt> - Sets a new value, i.e. <tt>record.set_preference(:notifications, value)</tt>
111
+ # * <tt>prefers_notifications=(value)</tt> - Sets a new value, i.e. <tt>record.write_preference(:notifications, value)</tt>
112
+ # * <tt>prefers_notifications_changed?</tt> - Whether the preference has unsaved changes
113
+ # * <tt>prefers_notifications_was</tt> - The last saved value for the preference
114
+ # * <tt>prefers_notifications_change</tt> - A list of [original_value, new_value] if the preference has changed
115
+ # * <tt>prefers_notifications_will_change!</tt> - Forces the preference to get updated
116
+ # * <tt>reset_prefers_notifications!</tt> - Reverts any unsaved changes to the preference
117
+ #
118
+ # ...and the equivalent +preferred+ methods:
119
+ # * <tt>preferred_notifications?</tt>
120
+ # * <tt>preferred_notifications</tt>
121
+ # * <tt>preferred_notifications=(value)</tt>
122
+ # * <tt>preferred_notifications_changed?</tt>
123
+ # * <tt>preferred_notifications_was</tt>
124
+ # * <tt>preferred_notifications_change</tt>
125
+ # * <tt>preferred_notifications_will_change!</tt>
126
+ # * <tt>reset_preferred_notifications!</tt>
94
127
  #
95
128
  # Notice that there are two tenses used depending on the context of the
96
129
  # preference. Conventionally, <tt>prefers_notifications?</tt> is better
@@ -126,6 +159,11 @@ module Preferences
126
159
 
127
160
  after_save :update_preferences
128
161
 
162
+ # Named scopes
163
+ named_scope :with_preferences, lambda {|preferences| build_preference_scope(preferences)}
164
+ named_scope :without_preferences, lambda {|preferences| build_preference_scope(preferences, true)}
165
+
166
+ extend Preferences::ClassMethods
129
167
  include Preferences::InstanceMethods
130
168
  end
131
169
 
@@ -153,14 +191,93 @@ module Preferences
153
191
 
154
192
  # Writer
155
193
  define_method("preferred_#{name}=") do |*args|
156
- set_preference(*([name] + [args].flatten))
194
+ write_preference(*args.flatten.unshift(name))
157
195
  end
158
196
  alias_method "prefers_#{name}=", "preferred_#{name}="
159
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
+
160
224
  definition
161
225
  end
162
226
  end
163
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
+ preference = preference.to_s
253
+ value = preference_definitions[preference.to_s].type_cast(value)
254
+ is_default = default_preferences[preference.to_s] == value
255
+
256
+ group_id, group_type = Preference.split_group(group)
257
+ table = "preferences_#{group_id}_#{group_type}_#{preference}"
258
+
259
+ # Since each preference is a different record, they need their own
260
+ # join so that the proper conditions can be set
261
+ joins << "LEFT JOIN preferences AS #{table} ON #{table}.owner_id = #{table_name}.#{primary_key} AND " + sanitize_sql(
262
+ "#{table}.owner_type" => base_class.name.to_s,
263
+ "#{table}.group_id" => group_id,
264
+ "#{table}.group_type" => group_type,
265
+ "#{table}.name" => preference
266
+ )
267
+
268
+ if inverse
269
+ statements << "#{table}.id IS NOT NULL AND #{table}.value " + (value.nil? ? ' IS NOT NULL' : ' != ?') + (!is_default ? " OR #{table}.id IS NULL" : '')
270
+ else
271
+ statements << "#{table}.id IS NOT NULL AND #{table}.value " + (value.nil? ? ' IS NULL' : ' = ?') + (is_default ? " OR #{table}.id IS NULL" : '')
272
+ end
273
+ values << value unless value.nil?
274
+ end
275
+
276
+ sql = statements.map! {|statement| "(#{statement})"} * ' AND '
277
+ {:joins => joins, :conditions => values.unshift(sql)}
278
+ end
279
+ end
280
+
164
281
  module InstanceMethods
165
282
  def self.included(base) #:nodoc:
166
283
  base.class_eval do
@@ -169,8 +286,8 @@ module Preferences
169
286
  end
170
287
 
171
288
  # Finds all preferences, including defaults, for the current record. If
172
- # any custom group preferences have been stored, then this will include
173
- # all default preferences within that particular group.
289
+ # looking up custom group preferences, then this will include all default
290
+ # preferences within that particular group as well.
174
291
  #
175
292
  # == Examples
176
293
  #
@@ -182,46 +299,23 @@ module Preferences
182
299
  #
183
300
  # A user with stored values for a particular group:
184
301
  #
185
- # user.preferred_color = 'red', 'cars'
186
- # user.preferences
187
- # => {"language"=>"English", "color"=>nil, "cars"=>{"language=>"English", "color"=>"red"}}
188
- #
189
- # Getting preference values *just* for the owning record (i.e. excluding groups):
190
- #
191
- # user.preferences(nil)
192
- # => {"language"=>"English", "color"=>nil}
193
- #
194
- # Getting preference values for a particular group:
195
- #
196
- # user.preferences('cars')
197
- # => {"language"=>"English", "color"=>"red"}
198
- def preferences(*args)
199
- if args.empty?
200
- group = nil
201
- conditions = {}
202
- else
203
- group = args.first
302
+ # user.preferred_color = 'red', :cars
303
+ # user.preferences(:cars)
304
+ # => {"language=>"English", "color"=>"red"}
305
+ def preferences(group = nil)
306
+ unless preferences_group_loaded?(group)
307
+ preferences = preferences_group(group)
204
308
 
205
- # Split the actual group into its different parts (id/type) in case
206
- # a record is passed in
207
309
  group_id, group_type = Preference.split_group(group)
208
- conditions = {:group_id => group_id, :group_type => group_type}
209
- end
210
-
211
- # Find all of the stored preferences
212
- stored_preferences = self.stored_preferences.find(:all, :conditions => conditions)
213
-
214
- # Hashify name -> value or group -> name -> value
215
- stored_preferences.inject(self.class.default_preferences.dup) do |all_preferences, preference|
216
- if !group && (preference_group = preference.group)
217
- preferences = all_preferences[preference_group] ||= self.class.default_preferences.dup
218
- else
219
- preferences = all_preferences
310
+ find_preferences(:group_id => group_id, :group_type => group_type).each do |preference|
311
+ preferences[preference.name] ||= preference.value
220
312
  end
221
313
 
222
- preferences[preference.name] = preference.value
223
- all_preferences
314
+ # Add defaults
315
+ preferences.reverse_merge!(self.class.default_preferences.dup)
224
316
  end
317
+
318
+ preferences_group(group).dup
225
319
  end
226
320
 
227
321
  # Queries whether or not a value is present for the given preference.
@@ -239,11 +333,12 @@ module Preferences
239
333
  # user.preferred?(:color, 'cars') # => true
240
334
  # user.preferred?(:color, Car.first) # => true
241
335
  #
242
- # user.set_preference(:color, nil)
336
+ # user.write_preference(:color, nil)
243
337
  # user.preferred(:color) # => nil
244
338
  # user.preferred?(:color) # => false
245
339
  def preferred?(name, group = nil)
246
340
  name = name.to_s
341
+ assert_valid_preference(name)
247
342
 
248
343
  value = preferred(name, group)
249
344
  preference_definitions[name].query(value)
@@ -264,24 +359,27 @@ module Preferences
264
359
  # user.preferred(:color, 'cars') # => "red"
265
360
  # user.preferred(:color, Car.first) # => "red"
266
361
  #
267
- # user.set_preference(:color, 'blue')
362
+ # user.write_preference(:color, 'blue')
268
363
  # user.preferred(:color) # => "blue"
269
364
  def preferred(name, group = nil)
270
365
  name = name.to_s
366
+ assert_valid_preference(name)
271
367
 
272
- if @preference_values && @preference_values[group] && @preference_values[group].include?(name)
368
+ if preferences_group(group).include?(name)
273
369
  # Value for this group/name has been written, but not saved yet:
274
370
  # grab from the pending values
275
- value = @preference_values[group][name]
371
+ value = preferences_group(group)[name]
276
372
  else
277
- # Split the group being filtered
373
+ # Grab the first preference; if it doesn't exist, use the default value
278
374
  group_id, group_type = Preference.split_group(group)
375
+ preference = find_preferences(:name => name, :group_id => group_id, :group_type => group_type).first unless preferences_group_loaded?(group)
279
376
 
280
- # Grab the first preference; if it doesn't exist, use the default value
281
- preference = stored_preferences.find(:first, :conditions => {:name => name, :group_id => group_id, :group_type => group_type})
282
377
  value = preference ? preference.value : preference_definitions[name].default_value
378
+ preferences_group(group)[name] = value
283
379
  end
284
380
 
381
+ definition = preference_definitions[name]
382
+ value = definition.type_cast(value) unless value.nil?
285
383
  value
286
384
  end
287
385
  alias_method :prefers, :preferred
@@ -295,40 +393,213 @@ module Preferences
295
393
  # == Examples
296
394
  #
297
395
  # user = User.find(:first)
298
- # user.set_preference(:color, 'red') # => "red"
396
+ # user.write_preference(:color, 'red') # => "red"
299
397
  # user.save!
300
398
  #
301
- # user.set_preference(:color, 'blue', Car.first) # => "blue"
399
+ # user.write_preference(:color, 'blue', Car.first) # => "blue"
302
400
  # user.save!
303
- def set_preference(name, value, group = nil)
401
+ def write_preference(name, value, group = nil)
304
402
  name = name.to_s
403
+ assert_valid_preference(name)
404
+
405
+ preferences_changed = preferences_changed_group(group)
406
+ if preferences_changed.include?(name)
407
+ old = preferences_changed[name]
408
+ preferences_changed.delete(name) unless preference_value_changed?(name, old, value)
409
+ else
410
+ old = clone_preference_value(name, group)
411
+ preferences_changed[name] = old if preference_value_changed?(name, old, value)
412
+ end
305
413
 
306
- @preference_values ||= {}
307
- @preference_values[group] ||= {}
308
- @preference_values[group][name] = value
414
+ value = convert_number_column_value(value) if preference_definitions[name].number?
415
+ preferences_group(group)[name] = value
309
416
 
310
417
  value
311
418
  end
312
419
 
420
+ # Whether any attributes have unsaved changes.
421
+ #
422
+ # == Examples
423
+ #
424
+ # user = User.find(:first)
425
+ # user.preferences_changed? # => false
426
+ # user.write_preference(:color, 'red')
427
+ # user.preferences_changed? # => true
428
+ # user.save
429
+ # user.preferences_changed? # => false
430
+ #
431
+ # # Groups
432
+ # user.preferences_changed?(:car) # => false
433
+ # user.write_preference(:color, 'red', :car)
434
+ # user.preferences_changed(:car) # => true
435
+ def preferences_changed?(group = nil)
436
+ !preferences_changed_group(group).empty?
437
+ end
438
+
439
+ # A list of the preferences that have unsaved changes.
440
+ #
441
+ # == Examples
442
+ #
443
+ # user = User.find(:first)
444
+ # user.preferences_changed # => []
445
+ # user.write_preference(:color, 'red')
446
+ # user.preferences_changed # => ["color"]
447
+ # user.save
448
+ # user.preferences_changed # => []
449
+ #
450
+ # # Groups
451
+ # user.preferences_changed(:car) # => []
452
+ # user.write_preference(:color, 'red', :car)
453
+ # user.preferences_changed(:car) # => ["color"]
454
+ def preferences_changed(group = nil)
455
+ preferences_changed_group(group).keys
456
+ end
457
+
458
+ # A map of the preferences that have changed in the current object.
459
+ #
460
+ # == Examples
461
+ #
462
+ # user = User.find(:first)
463
+ # user.preferred(:color) # => nil
464
+ # user.preference_changes # => {}
465
+ #
466
+ # user.write_preference(:color, 'red')
467
+ # user.preference_changes # => {"color" => [nil, "red"]}
468
+ # user.save
469
+ # user.preference_changes # => {}
470
+ #
471
+ # # Groups
472
+ # user.preferred(:color, :car) # => nil
473
+ # user.preference_changes(:car) # => {}
474
+ # user.write_preference(:color, 'red', :car)
475
+ # user.preference_changes(:car) # => {"color" => [nil, "red"]}
476
+ def preference_changes(group = nil)
477
+ preferences_changed(group).inject({}) do |changes, preference|
478
+ changes[preference] = preference_change(preference, group)
479
+ changes
480
+ end
481
+ end
482
+
483
+ # Reloads the pereferences of this object as well as its attributes
484
+ def reload(*args) #:nodoc:
485
+ result = super
486
+
487
+ @preferences.clear if @preferences
488
+ @preferences_changed.clear if @preferences_changed
489
+
490
+ result
491
+ end
492
+
313
493
  private
494
+ # Asserts that the given name is a valid preference in this model. If it
495
+ # is not, then an ArgumentError exception is raised.
496
+ def assert_valid_preference(name)
497
+ raise(ArgumentError, "Unknown preference: #{name}") unless preference_definitions.include?(name)
498
+ end
499
+
500
+ # Gets the set of preferences identified by the given group
501
+ def preferences_group(group)
502
+ @preferences ||= {}
503
+ @preferences[group.is_a?(Symbol) ? group.to_s : group] ||= {}
504
+ end
505
+
506
+ # Determines whether the given group of preferences has already been
507
+ # loaded from the database
508
+ def preferences_group_loaded?(group)
509
+ preference_definitions.length == preferences_group(group).length
510
+ end
511
+
512
+ # Generates a clone of the current value stored for the preference with
513
+ # the given name / group
514
+ def clone_preference_value(name, group)
515
+ value = preferred(name, group)
516
+ value.duplicable? ? value.clone : value
517
+ rescue TypeError, NoMethodError
518
+ value
519
+ end
520
+
521
+ # Keeps track of all preferences that have been changed so that they can
522
+ # be properly updated in the database. Maps group -> preference -> value.
523
+ def preferences_changed_group(group)
524
+ @preferences_changed ||= {}
525
+ @preferences_changed[group.is_a?(Symbol) ? group.to_s : group] ||= {}
526
+ end
527
+
528
+ # Determines whether a preference changed in the given group
529
+ def preference_changed?(name, group)
530
+ preferences_changed_group(group).include?(name)
531
+ end
532
+
533
+ # Builds an array of [original_value, new_value] for the given preference.
534
+ # If the perference did not change, this will return nil.
535
+ def preference_change(name, group)
536
+ [preferences_changed_group(group)[name], preferred(name, group)] if preference_changed?(name, group)
537
+ end
538
+
539
+ # Gets the last saved value for the given preference
540
+ def preference_was(name, group)
541
+ preference_changed?(name, group) ? preferences_changed_group(group)[name] : preferred(name, group)
542
+ end
543
+
544
+ # Forces the given preference to be saved regardless of whether the value
545
+ # is actually diferent
546
+ def preference_will_change!(name, group)
547
+ preferences_changed_group(group)[name] = clone_preference_value(name, group)
548
+ end
549
+
550
+ # Reverts any unsaved changes to the given preference
551
+ def reset_preference!(name, group)
552
+ write_preference(name, preferences_changed_group(group)[name], group) if preference_changed?(name, group)
553
+ end
554
+
555
+ # Determines whether the old value is different from the new value for the
556
+ # given preference. This will use the typecasted value to determine
557
+ # equality.
558
+ def preference_value_changed?(name, old, value)
559
+ definition = preference_definitions[name]
560
+ if definition.type == :integer && (old.nil? || old == 0)
561
+ # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
562
+ # Hence we don't record it as a change if the value changes from nil to ''.
563
+ # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
564
+ # be typecast back to 0 (''.to_i => 0)
565
+ value = nil if value.blank?
566
+ else
567
+ value = definition.type_cast(value)
568
+ end
569
+
570
+ old != value
571
+ end
572
+
314
573
  # Updates any preferences that have been changed/added since the record
315
574
  # was last saved
316
575
  def update_preferences
317
- if @preference_values
318
- @preference_values.each do |group, new_preferences|
576
+ if @preferences_changed
577
+ @preferences_changed.each do |group, preferences|
319
578
  group_id, group_type = Preference.split_group(group)
320
579
 
321
- new_preferences.each do |name, value|
322
- attributes = {:name => name, :group_id => group_id, :group_type => group_type}
323
-
580
+ preferences.keys.each do |name|
324
581
  # Find an existing preference or build a new one
325
- preference = stored_preferences.find(:first, :conditions => attributes) || stored_preferences.build(attributes)
326
- preference.value = value
582
+ attributes = {:name => name, :group_id => group_id, :group_type => group_type}
583
+ preference = find_preferences(attributes).first || stored_preferences.build(attributes)
584
+ preference.value = preferred(name, group)
327
585
  preference.save!
328
586
  end
329
587
  end
330
588
 
331
- @preference_values = nil
589
+ @preferences_changed.clear
590
+ end
591
+ end
592
+
593
+ # Finds all stored preferences with the given attributes. This will do a
594
+ # smart lookup by looking at the in-memory collection if it was eager-
595
+ # loaded.
596
+ def find_preferences(attributes)
597
+ if stored_preferences.loaded?
598
+ stored_preferences.select do |preference|
599
+ attributes.all? {|attribute, value| preference[attribute] == value}
600
+ end
601
+ else
602
+ stored_preferences.find(:all, :conditions => attributes)
332
603
  end
333
604
  end
334
605
  end