ee_preferences 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +8 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.rdoc +85 -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/generators/preferences/USAGE +5 -0
- data/generators/preferences/preferences_generator.rb +7 -0
- data/generators/preferences/templates/001_create_preferences.rb +16 -0
- data/init.rb +1 -0
- data/lib/preferences/preference_definition.rb +56 -0
- data/lib/preferences/version.rb +3 -0
- data/lib/preferences.rb +616 -0
- data/preferences.gemspec +20 -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 +1364 -0
- data/test/test_helper.rb +26 -0
- data/test/unit/preference_definition_test.rb +237 -0
- data/test/unit/preference_test.rb +236 -0
- metadata +122 -0
data/lib/preferences.rb
ADDED
@@ -0,0 +1,616 @@
|
|
1
|
+
require 'preferences/preference_definition'
|
2
|
+
|
3
|
+
# Adds support for defining preferences on ActiveRecord models.
|
4
|
+
#
|
5
|
+
# == Saving preferences
|
6
|
+
#
|
7
|
+
# Preferences are not automatically saved when they are set. You must save
|
8
|
+
# the record that the preferences were set on.
|
9
|
+
#
|
10
|
+
# For example,
|
11
|
+
#
|
12
|
+
# class User < ActiveRecord::Base
|
13
|
+
# preference :notifications
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# u = User.new(:login => 'admin', :prefers_notifications => false)
|
17
|
+
# u.save!
|
18
|
+
#
|
19
|
+
# u = User.find_by_login('admin')
|
20
|
+
# u.attributes = {:prefers_notifications => true}
|
21
|
+
# u.save!
|
22
|
+
#
|
23
|
+
# == Validations
|
24
|
+
#
|
25
|
+
# Since the generated accessors for a preference allow the preference to be
|
26
|
+
# treated just like regular ActiveRecord attributes, they can also be
|
27
|
+
# validated against in the same way. For example,
|
28
|
+
#
|
29
|
+
# class User < ActiveRecord::Base
|
30
|
+
# preference :color, :string
|
31
|
+
#
|
32
|
+
# validates_presence_of :preferred_color
|
33
|
+
# validates_inclusion_of :preferred_color, :in => %w(red green blue)
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# u = User.new
|
37
|
+
# u.valid? # => false
|
38
|
+
# u.errors.on(:preferred_color) # => "can't be blank"
|
39
|
+
#
|
40
|
+
# u.preferred_color = 'white'
|
41
|
+
# u.valid? # => false
|
42
|
+
# u.errors.on(:preferred_color) # => "is not included in the list"
|
43
|
+
#
|
44
|
+
# u.preferred_color = 'red'
|
45
|
+
# u.valid? # => true
|
46
|
+
module Preferences
|
47
|
+
module MacroMethods
|
48
|
+
# Defines a new preference for all records in the model. By default,
|
49
|
+
# preferences are assumed to have a boolean data type, so all values will
|
50
|
+
# be typecasted to true/false based on ActiveRecord rules.
|
51
|
+
#
|
52
|
+
# Configuration options:
|
53
|
+
# * <tt>:default</tt> - The default value for the preference. Default is nil.
|
54
|
+
# * <tt>:group_defaults</tt> - Defines the default values to use for various
|
55
|
+
# groups. This should map group_name -> defaults. For ActiveRecord groups,
|
56
|
+
# use the class name.
|
57
|
+
#
|
58
|
+
# == Examples
|
59
|
+
#
|
60
|
+
# The example below shows the various ways to define a preference for a
|
61
|
+
# particular model.
|
62
|
+
#
|
63
|
+
# class User < ActiveRecord::Base
|
64
|
+
# preference :notifications, :default => false
|
65
|
+
# preference :color, :string, :default => 'red', :group_defaults => {:car => 'black'}
|
66
|
+
# preference :favorite_number, :integer
|
67
|
+
# preference :data, :any # Allows any data type to be stored
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# All preferences are also inherited by subclasses.
|
71
|
+
#
|
72
|
+
# == Associations
|
73
|
+
#
|
74
|
+
# After the first preference is defined, the following associations are
|
75
|
+
# created for the model:
|
76
|
+
# * +stored_preferences+ - A collection of all the custom preferences
|
77
|
+
# specified for a record. This will not include default preferences
|
78
|
+
# unless they have been explicitly set.
|
79
|
+
#
|
80
|
+
# == Named scopes
|
81
|
+
#
|
82
|
+
# In addition to the above associations, the following named scopes get
|
83
|
+
# generated for the model:
|
84
|
+
# * +with_preferences+ - Finds all records with a given set of preferences
|
85
|
+
# * +without_preferences+ - Finds all records without a given set of preferences
|
86
|
+
#
|
87
|
+
# In addition to utilizing preferences stored in the database, each of the
|
88
|
+
# above scopes also take into account the defaults that have been defined
|
89
|
+
# for each preference.
|
90
|
+
#
|
91
|
+
# Example:
|
92
|
+
#
|
93
|
+
# User.with_preferences(:notifications => true)
|
94
|
+
# User.with_preferences(:notifications => true, :color => 'blue')
|
95
|
+
#
|
96
|
+
# # Searching with group preferences
|
97
|
+
# car = Car.find(:first)
|
98
|
+
# User.with_preferences(car => {:color => 'blue'})
|
99
|
+
# User.with_preferences(:notifications => true, car => {:color => 'blue'})
|
100
|
+
#
|
101
|
+
# == Generated accessors
|
102
|
+
#
|
103
|
+
# In addition to calling <tt>prefers?</tt> and +preferred+ on a record,
|
104
|
+
# you can also use the shortcut accessor methods that are generated when a
|
105
|
+
# preference is defined. For example,
|
106
|
+
#
|
107
|
+
# class User < ActiveRecord::Base
|
108
|
+
# preference :notifications
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# ...generates the following methods:
|
112
|
+
# * <tt>prefers_notifications?</tt> - Whether a value has been specified, i.e. <tt>record.prefers?(:notifications)</tt>
|
113
|
+
# * <tt>prefers_notifications</tt> - The actual value stored, i.e. <tt>record.prefers(:notifications)</tt>
|
114
|
+
# * <tt>prefers_notifications=(value)</tt> - Sets a new value, i.e. <tt>record.write_preference(:notifications, value)</tt>
|
115
|
+
# * <tt>prefers_notifications_changed?</tt> - Whether the preference has unsaved changes
|
116
|
+
# * <tt>prefers_notifications_was</tt> - The last saved value for the preference
|
117
|
+
# * <tt>prefers_notifications_change</tt> - A list of [original_value, new_value] if the preference has changed
|
118
|
+
# * <tt>prefers_notifications_will_change!</tt> - Forces the preference to get updated
|
119
|
+
# * <tt>reset_prefers_notifications!</tt> - Reverts any unsaved changes to the preference
|
120
|
+
#
|
121
|
+
# ...and the equivalent +preferred+ methods:
|
122
|
+
# * <tt>preferred_notifications?</tt>
|
123
|
+
# * <tt>preferred_notifications</tt>
|
124
|
+
# * <tt>preferred_notifications=(value)</tt>
|
125
|
+
# * <tt>preferred_notifications_changed?</tt>
|
126
|
+
# * <tt>preferred_notifications_was</tt>
|
127
|
+
# * <tt>preferred_notifications_change</tt>
|
128
|
+
# * <tt>preferred_notifications_will_change!</tt>
|
129
|
+
# * <tt>reset_preferred_notifications!</tt>
|
130
|
+
#
|
131
|
+
# Notice that there are two tenses used depending on the context of the
|
132
|
+
# preference. Conventionally, <tt>prefers_notifications?</tt> is better
|
133
|
+
# for accessing boolean preferences, while +preferred_color+ is better for
|
134
|
+
# accessing non-boolean preferences.
|
135
|
+
#
|
136
|
+
# Example:
|
137
|
+
#
|
138
|
+
# user = User.find(:first)
|
139
|
+
# user.prefers_notifications? # => false
|
140
|
+
# user.prefers_notifications # => false
|
141
|
+
# user.preferred_color? # => true
|
142
|
+
# user.preferred_color # => 'red'
|
143
|
+
# user.preferred_color = 'blue' # => 'blue'
|
144
|
+
#
|
145
|
+
# user.prefers_notifications = true
|
146
|
+
#
|
147
|
+
# car = Car.find(:first)
|
148
|
+
# user.preferred_color = 'red', car # => 'red'
|
149
|
+
# user.preferred_color(car) # => 'red'
|
150
|
+
# user.preferred_color?(car) # => true
|
151
|
+
#
|
152
|
+
# user.save! # => true
|
153
|
+
def preference(name, *args)
|
154
|
+
unless included_modules.include?(InstanceMethods)
|
155
|
+
class_inheritable_hash :preference_definitions
|
156
|
+
self.preference_definitions = {}
|
157
|
+
|
158
|
+
has_many :stored_preferences, :as => :owner, :class_name => 'Preference'
|
159
|
+
|
160
|
+
after_save :update_preferences
|
161
|
+
before_destroy :destroy_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
|
+
{:joins => joins, :conditions => 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
|
+
preferences[preference.name] = preference.value unless preferences.include?(preference.name)
|
313
|
+
end
|
314
|
+
|
315
|
+
# Add defaults
|
316
|
+
preference_definitions.each do |name, definition|
|
317
|
+
preferences[name] = definition.default_value(group_type) unless preferences.include?(name)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
preferences.inject({}) do |typed_preferences, (name, value)|
|
322
|
+
typed_preferences[name] = value.nil? ? value : preference_definitions[name].type_cast(value)
|
323
|
+
typed_preferences
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Queries whether or not a value is present for the given preference.
|
328
|
+
# This is dependent on how the value is type-casted.
|
329
|
+
#
|
330
|
+
# == Examples
|
331
|
+
#
|
332
|
+
# class User < ActiveRecord::Base
|
333
|
+
# preference :color, :string, :default => 'red'
|
334
|
+
# end
|
335
|
+
#
|
336
|
+
# user = User.create
|
337
|
+
# user.preferred(:color) # => "red"
|
338
|
+
# user.preferred?(:color) # => true
|
339
|
+
# user.preferred?(:color, 'cars') # => true
|
340
|
+
# user.preferred?(:color, Car.first) # => true
|
341
|
+
#
|
342
|
+
# user.write_preference(:color, nil)
|
343
|
+
# user.preferred(:color) # => nil
|
344
|
+
# user.preferred?(:color) # => false
|
345
|
+
def preferred?(name, group = nil)
|
346
|
+
name = name.to_s
|
347
|
+
assert_valid_preference(name)
|
348
|
+
|
349
|
+
value = preferred(name, group)
|
350
|
+
preference_definitions[name].query(value)
|
351
|
+
end
|
352
|
+
alias_method :prefers?, :preferred?
|
353
|
+
|
354
|
+
# Gets the actual value stored for the given preference, or the default
|
355
|
+
# value if nothing is present.
|
356
|
+
#
|
357
|
+
# == Examples
|
358
|
+
#
|
359
|
+
# class User < ActiveRecord::Base
|
360
|
+
# preference :color, :string, :default => 'red'
|
361
|
+
# end
|
362
|
+
#
|
363
|
+
# user = User.create
|
364
|
+
# user.preferred(:color) # => "red"
|
365
|
+
# user.preferred(:color, 'cars') # => "red"
|
366
|
+
# user.preferred(:color, Car.first) # => "red"
|
367
|
+
#
|
368
|
+
# user.write_preference(:color, 'blue')
|
369
|
+
# user.preferred(:color) # => "blue"
|
370
|
+
def preferred(name, group = nil)
|
371
|
+
name = name.to_s
|
372
|
+
assert_valid_preference(name)
|
373
|
+
|
374
|
+
if preferences_group(group).include?(name)
|
375
|
+
# Value for this group/name has been written, but not saved yet:
|
376
|
+
# grab from the pending values
|
377
|
+
value = preferences_group(group)[name]
|
378
|
+
else
|
379
|
+
# Grab the first preference; if it doesn't exist, use the default value
|
380
|
+
group_id, group_type = Preference.split_group(group)
|
381
|
+
preference = find_preferences(:name => name, :group_id => group_id, :group_type => group_type).first unless preferences_group_loaded?(group)
|
382
|
+
|
383
|
+
value = preference ? preference.value : preference_definitions[name].default_value(group_type)
|
384
|
+
preferences_group(group)[name] = value
|
385
|
+
end
|
386
|
+
|
387
|
+
definition = preference_definitions[name]
|
388
|
+
value = definition.type_cast(value) unless value.nil?
|
389
|
+
value
|
390
|
+
end
|
391
|
+
alias_method :prefers, :preferred
|
392
|
+
|
393
|
+
# Sets a new value for the given preference. The actual Preference record
|
394
|
+
# is *not* created until this record is saved. In this way, preferences
|
395
|
+
# act *exactly* the same as attributes. They can be written to and
|
396
|
+
# validated against, but won't actually be written to the database until
|
397
|
+
# the record is saved.
|
398
|
+
#
|
399
|
+
# == Examples
|
400
|
+
#
|
401
|
+
# user = User.find(:first)
|
402
|
+
# user.write_preference(:color, 'red') # => "red"
|
403
|
+
# user.save!
|
404
|
+
#
|
405
|
+
# user.write_preference(:color, 'blue', Car.first) # => "blue"
|
406
|
+
# user.save!
|
407
|
+
def write_preference(name, value, group = nil)
|
408
|
+
name = name.to_s
|
409
|
+
assert_valid_preference(name)
|
410
|
+
|
411
|
+
preferences_changed = preferences_changed_group(group)
|
412
|
+
if preferences_changed.include?(name)
|
413
|
+
old = preferences_changed[name]
|
414
|
+
preferences_changed.delete(name) unless preference_value_changed?(name, old, value)
|
415
|
+
else
|
416
|
+
old = clone_preference_value(name, group)
|
417
|
+
preferences_changed[name] = old if preference_value_changed?(name, old, value)
|
418
|
+
end
|
419
|
+
|
420
|
+
value = convert_number_column_value(value) if preference_definitions[name].number?
|
421
|
+
preferences_group(group)[name] = value
|
422
|
+
|
423
|
+
value
|
424
|
+
end
|
425
|
+
|
426
|
+
# Whether any attributes have unsaved changes.
|
427
|
+
#
|
428
|
+
# == Examples
|
429
|
+
#
|
430
|
+
# user = User.find(:first)
|
431
|
+
# user.preferences_changed? # => false
|
432
|
+
# user.write_preference(:color, 'red')
|
433
|
+
# user.preferences_changed? # => true
|
434
|
+
# user.save
|
435
|
+
# user.preferences_changed? # => false
|
436
|
+
#
|
437
|
+
# # Groups
|
438
|
+
# user.preferences_changed?(:car) # => false
|
439
|
+
# user.write_preference(:color, 'red', :car)
|
440
|
+
# user.preferences_changed(:car) # => true
|
441
|
+
def preferences_changed?(group = nil)
|
442
|
+
!preferences_changed_group(group).empty?
|
443
|
+
end
|
444
|
+
|
445
|
+
# A list of the preferences that have unsaved changes.
|
446
|
+
#
|
447
|
+
# == Examples
|
448
|
+
#
|
449
|
+
# user = User.find(:first)
|
450
|
+
# user.preferences_changed # => []
|
451
|
+
# user.write_preference(:color, 'red')
|
452
|
+
# user.preferences_changed # => ["color"]
|
453
|
+
# user.save
|
454
|
+
# user.preferences_changed # => []
|
455
|
+
#
|
456
|
+
# # Groups
|
457
|
+
# user.preferences_changed(:car) # => []
|
458
|
+
# user.write_preference(:color, 'red', :car)
|
459
|
+
# user.preferences_changed(:car) # => ["color"]
|
460
|
+
def preferences_changed(group = nil)
|
461
|
+
preferences_changed_group(group).keys
|
462
|
+
end
|
463
|
+
|
464
|
+
# A map of the preferences that have changed in the current object.
|
465
|
+
#
|
466
|
+
# == Examples
|
467
|
+
#
|
468
|
+
# user = User.find(:first)
|
469
|
+
# user.preferred(:color) # => nil
|
470
|
+
# user.preference_changes # => {}
|
471
|
+
#
|
472
|
+
# user.write_preference(:color, 'red')
|
473
|
+
# user.preference_changes # => {"color" => [nil, "red"]}
|
474
|
+
# user.save
|
475
|
+
# user.preference_changes # => {}
|
476
|
+
#
|
477
|
+
# # Groups
|
478
|
+
# user.preferred(:color, :car) # => nil
|
479
|
+
# user.preference_changes(:car) # => {}
|
480
|
+
# user.write_preference(:color, 'red', :car)
|
481
|
+
# user.preference_changes(:car) # => {"color" => [nil, "red"]}
|
482
|
+
def preference_changes(group = nil)
|
483
|
+
preferences_changed(group).inject({}) do |changes, preference|
|
484
|
+
changes[preference] = preference_change(preference, group)
|
485
|
+
changes
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# Reloads the pereferences of this object as well as its attributes
|
490
|
+
def reload(*args) #:nodoc:
|
491
|
+
result = super
|
492
|
+
|
493
|
+
@preferences.clear if @preferences
|
494
|
+
@preferences_changed.clear if @preferences_changed
|
495
|
+
|
496
|
+
result
|
497
|
+
end
|
498
|
+
|
499
|
+
private
|
500
|
+
# Asserts that the given name is a valid preference in this model. If it
|
501
|
+
# is not, then an ArgumentError exception is raised.
|
502
|
+
def assert_valid_preference(name)
|
503
|
+
raise(ArgumentError, "Unknown preference: #{name}") unless preference_definitions.include?(name)
|
504
|
+
end
|
505
|
+
|
506
|
+
# Gets the set of preferences identified by the given group
|
507
|
+
def preferences_group(group)
|
508
|
+
@preferences ||= {}
|
509
|
+
@preferences[group.is_a?(Symbol) ? group.to_s : group] ||= {}
|
510
|
+
end
|
511
|
+
|
512
|
+
# Determines whether the given group of preferences has already been
|
513
|
+
# loaded from the database
|
514
|
+
def preferences_group_loaded?(group)
|
515
|
+
preference_definitions.length == preferences_group(group).length
|
516
|
+
end
|
517
|
+
|
518
|
+
# Generates a clone of the current value stored for the preference with
|
519
|
+
# the given name / group
|
520
|
+
def clone_preference_value(name, group)
|
521
|
+
value = preferred(name, group)
|
522
|
+
value.duplicable? ? value.clone : value
|
523
|
+
rescue TypeError, NoMethodError
|
524
|
+
value
|
525
|
+
end
|
526
|
+
|
527
|
+
# Keeps track of all preferences that have been changed so that they can
|
528
|
+
# be properly updated in the database. Maps group -> preference -> value.
|
529
|
+
def preferences_changed_group(group)
|
530
|
+
@preferences_changed ||= {}
|
531
|
+
@preferences_changed[group.is_a?(Symbol) ? group.to_s : group] ||= {}
|
532
|
+
end
|
533
|
+
|
534
|
+
# Determines whether a preference changed in the given group
|
535
|
+
def preference_changed?(name, group)
|
536
|
+
preferences_changed_group(group).include?(name)
|
537
|
+
end
|
538
|
+
|
539
|
+
# Builds an array of [original_value, new_value] for the given preference.
|
540
|
+
# If the perference did not change, this will return nil.
|
541
|
+
def preference_change(name, group)
|
542
|
+
[preferences_changed_group(group)[name], preferred(name, group)] if preference_changed?(name, group)
|
543
|
+
end
|
544
|
+
|
545
|
+
# Gets the last saved value for the given preference
|
546
|
+
def preference_was(name, group)
|
547
|
+
preference_changed?(name, group) ? preferences_changed_group(group)[name] : preferred(name, group)
|
548
|
+
end
|
549
|
+
|
550
|
+
# Forces the given preference to be saved regardless of whether the value
|
551
|
+
# is actually diferent
|
552
|
+
def preference_will_change!(name, group)
|
553
|
+
preferences_changed_group(group)[name] = clone_preference_value(name, group)
|
554
|
+
end
|
555
|
+
|
556
|
+
# Reverts any unsaved changes to the given preference
|
557
|
+
def reset_preference!(name, group)
|
558
|
+
write_preference(name, preferences_changed_group(group)[name], group) if preference_changed?(name, group)
|
559
|
+
end
|
560
|
+
|
561
|
+
# Determines whether the old value is different from the new value for the
|
562
|
+
# given preference. This will use the typecasted value to determine
|
563
|
+
# equality.
|
564
|
+
def preference_value_changed?(name, old, value)
|
565
|
+
definition = preference_definitions[name]
|
566
|
+
if definition.type == :integer && (old.nil? || old == 0)
|
567
|
+
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
568
|
+
# Hence we don't record it as a change if the value changes from nil to ''.
|
569
|
+
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
570
|
+
# be typecast back to 0 (''.to_i => 0)
|
571
|
+
value = nil if value.blank?
|
572
|
+
else
|
573
|
+
value = definition.type_cast(value)
|
574
|
+
end
|
575
|
+
|
576
|
+
old != value
|
577
|
+
end
|
578
|
+
|
579
|
+
# Updates any preferences that have been changed/added since the record
|
580
|
+
# was last saved
|
581
|
+
def update_preferences
|
582
|
+
if @preferences_changed
|
583
|
+
@preferences_changed.each do |group, preferences|
|
584
|
+
group_id, group_type = Preference.split_group(group)
|
585
|
+
|
586
|
+
preferences.keys.each do |name|
|
587
|
+
# Find an existing preference or build a new one
|
588
|
+
attributes = {:name => name, :group_id => group_id, :group_type => group_type}
|
589
|
+
preference = find_preferences(attributes).first || stored_preferences.build(attributes)
|
590
|
+
preference.value = preferred(name, group)
|
591
|
+
preference.save!
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
@preferences_changed.clear
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
# Finds all stored preferences with the given attributes. This will do a
|
600
|
+
# smart lookup by looking at the in-memory collection if it was eager-
|
601
|
+
# loaded.
|
602
|
+
def find_preferences(attributes)
|
603
|
+
if stored_preferences.loaded?
|
604
|
+
stored_preferences.select do |preference|
|
605
|
+
attributes.all? {|attribute, value| preference[attribute] == value}
|
606
|
+
end
|
607
|
+
else
|
608
|
+
stored_preferences.find(:all, :conditions => attributes)
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
ActiveRecord::Base.class_eval do
|
615
|
+
extend Preferences::MacroMethods
|
616
|
+
end
|
data/preferences.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'preferences/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "ee_preferences"
|
6
|
+
s.version = Preferences::VERSION
|
7
|
+
s.authors = ["Aaron Pfeifer"]
|
8
|
+
s.email = "aaron@pluginaweek.org"
|
9
|
+
s.homepage = "http://www.pluginaweek.org"
|
10
|
+
s.description = "Adds support for easily creating custom preferences for ActiveRecord models"
|
11
|
+
s.summary = "Custom preferences for ActiveRecord models"
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
15
|
+
s.rdoc_options = %w(--line-numbers --inline-source --title preferences --main README.rdoc)
|
16
|
+
s.extra_rdoc_files = %w(README.rdoc CHANGELOG.rdoc LICENSE)
|
17
|
+
|
18
|
+
s.add_development_dependency("rake")
|
19
|
+
s.add_development_dependency("plugin_test_helper", ">= 0.3.1")
|
20
|
+
end
|