preferences 0.3.1 → 0.4.0

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.
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