pluginaweek-preferences 0.3.1
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 +56 -0
- data/LICENSE +20 -0
- data/README.rdoc +185 -0
- data/Rakefile +96 -0
- data/app/models/preference.rb +65 -0
- data/init.rb +1 -0
- data/lib/preferences.rb +339 -0
- data/lib/preferences/preference_definition.rb +43 -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 +7 -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 +426 -0
- data/test/test_helper.rb +13 -0
- data/test/unit/preference_definition_test.rb +185 -0
- data/test/unit/preference_test.rb +234 -0
- metadata +84 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
== master
|
2
|
+
|
3
|
+
== 0.3.1 / 2009-04-25
|
4
|
+
|
5
|
+
* Rename Preference#attribute to #name to avoid conflicts with reserved methods in ActiveRecord
|
6
|
+
|
7
|
+
== 0.3.0 / 2009-04-13
|
8
|
+
|
9
|
+
* Add dependency on Rails 2.3
|
10
|
+
* Remove dependency on plugins_plus
|
11
|
+
|
12
|
+
== 0.2.0 / 2008-12-14
|
13
|
+
|
14
|
+
* Remove the PluginAWeek namespace
|
15
|
+
|
16
|
+
== 0.1.5 / 2008-11-16
|
17
|
+
|
18
|
+
* Add all prefers/preferred accessors for preferences to be analogous to ActiveRecord column accessors
|
19
|
+
* Fix preferences defined in STI subclasses not working [Quinn Shanahan]
|
20
|
+
|
21
|
+
== 0.1.4 / 2008-10-26
|
22
|
+
|
23
|
+
* Change how the base module is included to prevent namespacing conflicts
|
24
|
+
|
25
|
+
== 0.1.3 / 2008-06-29
|
26
|
+
|
27
|
+
* Add +prefs+ as an alias for +preferences+
|
28
|
+
* Fix +preferences+ not properly selecting preferences when a group is specified
|
29
|
+
* Improve test coverage
|
30
|
+
|
31
|
+
== 0.1.2 / 2008-06-22
|
32
|
+
|
33
|
+
* Remove log files from gems
|
34
|
+
|
35
|
+
== 0.1.1 / 2008-06-20
|
36
|
+
|
37
|
+
* Rename preference_values hash to preferences
|
38
|
+
* Rename preferences association to stored_preferences
|
39
|
+
|
40
|
+
== 0.1.0 / 2008-06-19
|
41
|
+
|
42
|
+
* Avoid string evaluation for dynamic methods
|
43
|
+
* Return hashes for the preference_values, e.g.
|
44
|
+
|
45
|
+
user.preference_values # => {'color' => 'red', 'number' => 11, 'website' => {'background' => 'white', 'foreground' => 'black'}}
|
46
|
+
user.preference_values('website') # => {'background' => 'white', 'foreground' => 'black'}
|
47
|
+
|
48
|
+
* Add more generic grouping of preferences than with just other records, e.g.
|
49
|
+
|
50
|
+
user.preferred_color('cars')
|
51
|
+
|
52
|
+
* Remove support for an options hash when specifying :for associations for preference
|
53
|
+
|
54
|
+
== 0.0.1 / 2008-05-10
|
55
|
+
|
56
|
+
* Initial public release
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008-2009 Aaron Pfeifer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
== preferences
|
2
|
+
|
3
|
+
+preferences+ adds support for easily creating custom preferences for models.
|
4
|
+
|
5
|
+
== Resources
|
6
|
+
|
7
|
+
API
|
8
|
+
|
9
|
+
* http://api.pluginaweek.org/preferences
|
10
|
+
|
11
|
+
Bugs
|
12
|
+
|
13
|
+
* http://pluginaweek.lighthouseapp.com/projects/13286-preferences
|
14
|
+
|
15
|
+
Development
|
16
|
+
|
17
|
+
* http://github.com/pluginaweek/preferences
|
18
|
+
|
19
|
+
Source
|
20
|
+
|
21
|
+
* git://github.com/pluginaweek/preferences.git
|
22
|
+
|
23
|
+
== Description
|
24
|
+
|
25
|
+
Preferences for models within an application, such as for users, is a pretty
|
26
|
+
common idiom. Although the rule of thumb is to keep the number of preferences
|
27
|
+
available to a minimum, sometimes it's necessary if you want users to be able to
|
28
|
+
disable things like e-mail notifications.
|
29
|
+
|
30
|
+
Generally, basic preferences can be accomplished through simple designs, such as
|
31
|
+
additional columns or a bit vector described and implemented by preference_fu[http://agilewebdevelopment.com/plugins/preferencefu].
|
32
|
+
However, as you find the need for non-binary preferences and the number of
|
33
|
+
preferences becomes unmanageable as individual columns in the database, the next
|
34
|
+
step is often to create a separate "preferences" table. This is where the
|
35
|
+
+preferences+ plugin comes in.
|
36
|
+
|
37
|
+
+preferences+ encapsulates this design by exposing preferences using simple
|
38
|
+
attribute accessors on the model, hiding the fact that preferences are stored in
|
39
|
+
a separate table and making it dead-simple to define and manage preferences.
|
40
|
+
|
41
|
+
== Usage
|
42
|
+
|
43
|
+
=== Defining preferences
|
44
|
+
|
45
|
+
To define the preferences for a model, you can do so right within the model:
|
46
|
+
|
47
|
+
class User < ActiveRecord::Base
|
48
|
+
preference :hot_salsa
|
49
|
+
preference :dark_chocolate, :default => true
|
50
|
+
preference :color, :string
|
51
|
+
preference :favorite_number
|
52
|
+
preference :language, :string, :default => 'English'
|
53
|
+
end
|
54
|
+
|
55
|
+
In the above model, 5 preferences have been defined:
|
56
|
+
* hot_salsa
|
57
|
+
* dark_chocolate
|
58
|
+
* color
|
59
|
+
* favorite_number
|
60
|
+
* language
|
61
|
+
|
62
|
+
For each preference, a data type and default value can be specified. If no
|
63
|
+
data type is given, it's assumed to be a boolean value. If no default value is
|
64
|
+
given, the default is assumed to be nil.
|
65
|
+
|
66
|
+
=== Accessing preferences
|
67
|
+
|
68
|
+
Once preferences have been defined for a model, they can be accessed either
|
69
|
+
using the accessor methods that are generated for each preference or the generic
|
70
|
+
methods that are not specific to a particular preference.
|
71
|
+
|
72
|
+
==== Accessors
|
73
|
+
|
74
|
+
There are several shortcut methods that are generated for each preference
|
75
|
+
defined on a model. These reflect the same set of methods (attribute accessors)
|
76
|
+
that are generated for a model's columns. Examples of these are shown below:
|
77
|
+
|
78
|
+
Query methods:
|
79
|
+
user.prefers_hot_salsa? # => false
|
80
|
+
user.preferred_language? # => true
|
81
|
+
|
82
|
+
Reader methods:
|
83
|
+
user.prefers_hot_salsa # => false
|
84
|
+
user.preferred_language # => "English"
|
85
|
+
|
86
|
+
Writer methods:
|
87
|
+
user.prefers_hot_salsa = false # => false
|
88
|
+
user.preferred_language = 'English' # => "English"
|
89
|
+
|
90
|
+
==== Generic methods
|
91
|
+
|
92
|
+
Each preference accessor is essentially a wrapper for the various generic methods
|
93
|
+
shown below:
|
94
|
+
|
95
|
+
Query method:
|
96
|
+
user.prefers?(:hot_salsa) # => false
|
97
|
+
user.preferred?(:language) # => true
|
98
|
+
|
99
|
+
Reader method:
|
100
|
+
user.prefers(:hot_salsa) # => false
|
101
|
+
user.preferred(:language) # => "English"
|
102
|
+
|
103
|
+
Write method:
|
104
|
+
user.set_preference(:hot_salsa, false) # => false
|
105
|
+
user.set_preference(:language, "English") # => "English"
|
106
|
+
|
107
|
+
=== Accessing all preferences
|
108
|
+
|
109
|
+
To get the collection of all custom, stored preferences for a particular record,
|
110
|
+
you can access the +stored_preferences+ has_many association which is automatically
|
111
|
+
generated:
|
112
|
+
|
113
|
+
user.stored_preferences
|
114
|
+
|
115
|
+
In addition to this, you can get a hash of all stored preferences *and* default
|
116
|
+
preferences, by accessing the +preferences+ helper:
|
117
|
+
|
118
|
+
user.preferences # => {"language"=>"English", "color"=>nil}
|
119
|
+
|
120
|
+
This hash will contain the value for every preference that has been defined for
|
121
|
+
the model, whether that's the default value or one that has been previously
|
122
|
+
stored.
|
123
|
+
|
124
|
+
A short-hand alternative for preferences is also available:
|
125
|
+
|
126
|
+
user.prefs # => {"language"=>"English", "color"=>nil}
|
127
|
+
|
128
|
+
=== Grouping preferences
|
129
|
+
|
130
|
+
In addition to defining generic preferences for the owning record, you can also
|
131
|
+
group preferences by ActiveRecord objects or arbitrary names. This is best shown
|
132
|
+
through an example:
|
133
|
+
|
134
|
+
user = User.find(:first)
|
135
|
+
car = Car.find(:first)
|
136
|
+
|
137
|
+
user.preferred_color = 'red', car
|
138
|
+
# user.set_preference(:color, 'red', car) # The generic way
|
139
|
+
|
140
|
+
This will create a color preference of "red" for the given car. In this way,
|
141
|
+
you can have "color" preferences for different records.
|
142
|
+
|
143
|
+
To access the preference for a particular record, you can use the same accessor
|
144
|
+
methods as before:
|
145
|
+
|
146
|
+
user.preferred_color(car)
|
147
|
+
# user.preferred(:color, car) # The generic way
|
148
|
+
|
149
|
+
In addition to grouping preferences for a particular record, you can also group
|
150
|
+
preferences by name. For example,
|
151
|
+
|
152
|
+
user = User.find(:first)
|
153
|
+
|
154
|
+
user.preferred_color = 'red', 'automobiles'
|
155
|
+
user.preferred_color = 'tan', 'clothing'
|
156
|
+
|
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"}
|
162
|
+
|
163
|
+
=== Saving preferences
|
164
|
+
|
165
|
+
Note that preferences are not saved until the owning record is saved.
|
166
|
+
Preferences are treated in a similar fashion to attributes. For example,
|
167
|
+
|
168
|
+
user = user.find(:first)
|
169
|
+
user.attributes = {:prefers_hot_salsa => false, :preferred_color => 'red'}
|
170
|
+
user.save!
|
171
|
+
|
172
|
+
Preferences are stored in a separate table called "preferences".
|
173
|
+
|
174
|
+
== Testing
|
175
|
+
|
176
|
+
Before you can run any tests, the following gem must be installed:
|
177
|
+
* plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
|
178
|
+
|
179
|
+
To run against a specific version of Rails:
|
180
|
+
|
181
|
+
rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
|
182
|
+
|
183
|
+
== Dependencies
|
184
|
+
|
185
|
+
* Rails 2.3 or later
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
spec = Gem::Specification.new do |s|
|
7
|
+
s.name = 'preferences'
|
8
|
+
s.version = '0.3.1'
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.summary = 'Adds support for easily creating custom preferences for ActiveRecord models'
|
11
|
+
s.description = s.summary
|
12
|
+
|
13
|
+
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.require_path = 'lib'
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.test_files = Dir['test/**/*_test.rb']
|
17
|
+
|
18
|
+
s.author = 'Aaron Pfeifer'
|
19
|
+
s.email = 'aaron@pluginaweek.org'
|
20
|
+
s.homepage = 'http://www.pluginaweek.org'
|
21
|
+
s.rubyforge_project = 'pluginaweek'
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Default: run all tests.'
|
25
|
+
task :default => :test
|
26
|
+
|
27
|
+
desc "Test the #{spec.name} plugin."
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'lib'
|
30
|
+
t.test_files = spec.test_files
|
31
|
+
t.verbose = true
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
require 'rcov/rcovtask'
|
36
|
+
namespace :test do
|
37
|
+
desc "Test the #{spec.name} plugin with Rcov."
|
38
|
+
Rcov::RcovTask.new(:rcov) do |t|
|
39
|
+
t.libs << 'lib'
|
40
|
+
t.test_files = spec.test_files
|
41
|
+
t.rcov_opts << '--exclude="^(?!lib/|app/)"'
|
42
|
+
t.verbose = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue LoadError
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Generate documentation for the #{spec.name} plugin."
|
49
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = spec.name
|
52
|
+
rdoc.template = '../rdoc_template.rb'
|
53
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
54
|
+
rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb', 'app/**/*.rb')
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'Generate a gemspec file.'
|
58
|
+
task :gemspec do
|
59
|
+
File.open("#{spec.name}.gemspec", 'w') do |f|
|
60
|
+
f.write spec.to_ruby
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Rake::GemPackageTask.new(spec) do |p|
|
65
|
+
p.gem_spec = spec
|
66
|
+
p.need_tar = true
|
67
|
+
p.need_zip = true
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'Publish the beta gem.'
|
71
|
+
task :pgem => [:package] do
|
72
|
+
Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
|
73
|
+
end
|
74
|
+
|
75
|
+
desc 'Publish the API documentation.'
|
76
|
+
task :pdoc => [:rdoc] do
|
77
|
+
Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'Publish the API docs and gem'
|
81
|
+
task :publish => [:pgem, :pdoc, :release]
|
82
|
+
|
83
|
+
desc 'Publish the release files to RubyForge.'
|
84
|
+
task :release => [:gem, :package] do
|
85
|
+
require 'rubyforge'
|
86
|
+
|
87
|
+
ruby_forge = RubyForge.new.configure
|
88
|
+
ruby_forge.login
|
89
|
+
|
90
|
+
%w(gem tgz zip).each do |ext|
|
91
|
+
file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
|
92
|
+
puts "Releasing #{File.basename(file)}..."
|
93
|
+
|
94
|
+
ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Represents a preferred value for a particular preference on a model.
|
2
|
+
#
|
3
|
+
# == Grouped preferences
|
4
|
+
#
|
5
|
+
# In addition to simple named preferences, preferences can also be grouped by
|
6
|
+
# a particular value, be it a string or ActiveRecord object. For example, a
|
7
|
+
# User may have a preferred color for a particular Car. In this case, the
|
8
|
+
# +owner+ is the User record, the +name+ is "color", and the +group+ is the
|
9
|
+
# Car record. This allows preferences to have a sort of context around them.
|
10
|
+
class Preference < ActiveRecord::Base
|
11
|
+
belongs_to :owner, :polymorphic => true
|
12
|
+
belongs_to :group, :polymorphic => true
|
13
|
+
|
14
|
+
validates_presence_of :name, :owner_id, :owner_type
|
15
|
+
validates_presence_of :group_type, :if => :group_id?
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Splits the given group into its corresponding id and type. For simple
|
19
|
+
# primitives, the id will be nil. For complex types, specifically
|
20
|
+
# ActiveRecord objects, the id is the unique identifier stored in the
|
21
|
+
# database for the record.
|
22
|
+
#
|
23
|
+
# For example,
|
24
|
+
#
|
25
|
+
# Preference.split_group('google') # => [nil, "google"]
|
26
|
+
# Preference.split_group(1) # => [nil, 1]
|
27
|
+
# Preference.split_group(User.find(1)) # => [1, "User"]
|
28
|
+
def split_group(group = nil)
|
29
|
+
if group.is_a?(ActiveRecord::Base)
|
30
|
+
group_id, group_type = group.id, group.class.base_class.name.to_s
|
31
|
+
else
|
32
|
+
group_id, group_type = nil, group
|
33
|
+
end
|
34
|
+
|
35
|
+
[group_id, group_type]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# The definition of the preference as defined in the owner's model
|
40
|
+
def definition
|
41
|
+
# Optimize number of queries to the database by only looking up the actual
|
42
|
+
# owner record for STI cases when the definition can't be found in the
|
43
|
+
# stored owner type class
|
44
|
+
owner_type && (find_definition(owner_type.constantize) || find_definition(owner.class))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Typecasts the value depending on the preference definition's declared type
|
48
|
+
def value
|
49
|
+
value = read_attribute(:value)
|
50
|
+
value = definition.type_cast(value) if definition
|
51
|
+
value
|
52
|
+
end
|
53
|
+
|
54
|
+
# Only searches for the group record if the group id is specified
|
55
|
+
def group_with_optional_lookup
|
56
|
+
group_id ? group_without_optional_lookup : group_type
|
57
|
+
end
|
58
|
+
alias_method_chain :group, :optional_lookup
|
59
|
+
|
60
|
+
private
|
61
|
+
# Finds the definition for this preference in the given owner class.
|
62
|
+
def find_definition(owner_class)
|
63
|
+
owner_class.respond_to?(:preference_definitions) && owner_class.preference_definitions[name]
|
64
|
+
end
|
65
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'preferences'
|
data/lib/preferences.rb
ADDED
@@ -0,0 +1,339 @@
|
|
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
|
+
#
|
55
|
+
# == Examples
|
56
|
+
#
|
57
|
+
# The example below shows the various ways to define a preference for a
|
58
|
+
# particular model.
|
59
|
+
#
|
60
|
+
# class User < ActiveRecord::Base
|
61
|
+
# preference :notifications, :default => false
|
62
|
+
# preference :color, :string, :default => 'red'
|
63
|
+
# preference :favorite_number, :integer
|
64
|
+
# preference :data, :any # Allows any data type to be stored
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# All preferences are also inherited by subclasses.
|
68
|
+
#
|
69
|
+
# == Associations
|
70
|
+
#
|
71
|
+
# After the first preference is defined, the following associations are
|
72
|
+
# created for the model:
|
73
|
+
# * +stored_preferences+ - A collection of all the custom preferences
|
74
|
+
# specified for a record. This will not include default preferences
|
75
|
+
# unless they have been explicitly set.
|
76
|
+
#
|
77
|
+
# == Generated accessors
|
78
|
+
#
|
79
|
+
# In addition to calling <tt>prefers?</tt> and +preferred+ on a record,
|
80
|
+
# you can also use the shortcut accessor methods that are generated when a
|
81
|
+
# preference is defined. For example,
|
82
|
+
#
|
83
|
+
# class User < ActiveRecord::Base
|
84
|
+
# preference :notifications
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# ...generates the following methods:
|
88
|
+
# * <tt>prefers_notifications?</tt> - Whether a value has been specified, i.e. <tt>record.prefers?(:notifications)</tt>
|
89
|
+
# * <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>
|
94
|
+
#
|
95
|
+
# Notice that there are two tenses used depending on the context of the
|
96
|
+
# preference. Conventionally, <tt>prefers_notifications?</tt> is better
|
97
|
+
# for accessing boolean preferences, while +preferred_color+ is better for
|
98
|
+
# accessing non-boolean preferences.
|
99
|
+
#
|
100
|
+
# Example:
|
101
|
+
#
|
102
|
+
# user = User.find(:first)
|
103
|
+
# user.prefers_notifications? # => false
|
104
|
+
# user.prefers_notifications # => false
|
105
|
+
# user.preferred_color? # => true
|
106
|
+
# user.preferred_color # => 'red'
|
107
|
+
# user.preferred_color = 'blue' # => 'blue'
|
108
|
+
#
|
109
|
+
# user.prefers_notifications = true
|
110
|
+
#
|
111
|
+
# car = Car.find(:first)
|
112
|
+
# user.preferred_color = 'red', car # => 'red'
|
113
|
+
# user.preferred_color(car) # => 'red'
|
114
|
+
# user.preferred_color?(car) # => true
|
115
|
+
#
|
116
|
+
# user.save! # => true
|
117
|
+
def preference(name, *args)
|
118
|
+
unless included_modules.include?(InstanceMethods)
|
119
|
+
class_inheritable_hash :preference_definitions
|
120
|
+
self.preference_definitions = {}
|
121
|
+
|
122
|
+
class_inheritable_hash :default_preferences
|
123
|
+
self.default_preferences = {}
|
124
|
+
|
125
|
+
has_many :stored_preferences, :as => :owner, :class_name => 'Preference'
|
126
|
+
|
127
|
+
after_save :update_preferences
|
128
|
+
|
129
|
+
include Preferences::InstanceMethods
|
130
|
+
end
|
131
|
+
|
132
|
+
# Create the definition
|
133
|
+
name = name.to_s
|
134
|
+
definition = PreferenceDefinition.new(name, *args)
|
135
|
+
self.preference_definitions[name] = definition
|
136
|
+
self.default_preferences[name] = definition.default_value
|
137
|
+
|
138
|
+
# Create short-hand accessor methods, making sure that the name
|
139
|
+
# is method-safe in terms of what characters are allowed
|
140
|
+
name = name.gsub(/[^A-Za-z0-9_-]/, '').underscore
|
141
|
+
|
142
|
+
# Query lookup
|
143
|
+
define_method("preferred_#{name}?") do |*group|
|
144
|
+
preferred?(name, group.first)
|
145
|
+
end
|
146
|
+
alias_method "prefers_#{name}?", "preferred_#{name}?"
|
147
|
+
|
148
|
+
# Reader
|
149
|
+
define_method("preferred_#{name}") do |*group|
|
150
|
+
preferred(name, group.first)
|
151
|
+
end
|
152
|
+
alias_method "prefers_#{name}", "preferred_#{name}"
|
153
|
+
|
154
|
+
# Writer
|
155
|
+
define_method("preferred_#{name}=") do |*args|
|
156
|
+
set_preference(*([name] + [args].flatten))
|
157
|
+
end
|
158
|
+
alias_method "prefers_#{name}=", "preferred_#{name}="
|
159
|
+
|
160
|
+
definition
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
module InstanceMethods
|
165
|
+
def self.included(base) #:nodoc:
|
166
|
+
base.class_eval do
|
167
|
+
alias_method :prefs, :preferences
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# 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.
|
174
|
+
#
|
175
|
+
# == Examples
|
176
|
+
#
|
177
|
+
# A user with no stored values:
|
178
|
+
#
|
179
|
+
# user = User.find(:first)
|
180
|
+
# user.preferences
|
181
|
+
# => {"language"=>"English", "color"=>nil}
|
182
|
+
#
|
183
|
+
# A user with stored values for a particular group:
|
184
|
+
#
|
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
|
204
|
+
|
205
|
+
# Split the actual group into its different parts (id/type) in case
|
206
|
+
# a record is passed in
|
207
|
+
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
|
220
|
+
end
|
221
|
+
|
222
|
+
preferences[preference.name] = preference.value
|
223
|
+
all_preferences
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Queries whether or not a value is present for the given preference.
|
228
|
+
# This is dependent on how the value is type-casted.
|
229
|
+
#
|
230
|
+
# == Examples
|
231
|
+
#
|
232
|
+
# class User < ActiveRecord::Base
|
233
|
+
# preference :color, :string, :default => 'red'
|
234
|
+
# end
|
235
|
+
#
|
236
|
+
# user = User.create
|
237
|
+
# user.preferred(:color) # => "red"
|
238
|
+
# user.preferred?(:color) # => true
|
239
|
+
# user.preferred?(:color, 'cars') # => true
|
240
|
+
# user.preferred?(:color, Car.first) # => true
|
241
|
+
#
|
242
|
+
# user.set_preference(:color, nil)
|
243
|
+
# user.preferred(:color) # => nil
|
244
|
+
# user.preferred?(:color) # => false
|
245
|
+
def preferred?(name, group = nil)
|
246
|
+
name = name.to_s
|
247
|
+
|
248
|
+
value = preferred(name, group)
|
249
|
+
preference_definitions[name].query(value)
|
250
|
+
end
|
251
|
+
alias_method :prefers?, :preferred?
|
252
|
+
|
253
|
+
# Gets the actual value stored for the given preference, or the default
|
254
|
+
# value if nothing is present.
|
255
|
+
#
|
256
|
+
# == Examples
|
257
|
+
#
|
258
|
+
# class User < ActiveRecord::Base
|
259
|
+
# preference :color, :string, :default => 'red'
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
# user = User.create
|
263
|
+
# user.preferred(:color) # => "red"
|
264
|
+
# user.preferred(:color, 'cars') # => "red"
|
265
|
+
# user.preferred(:color, Car.first) # => "red"
|
266
|
+
#
|
267
|
+
# user.set_preference(:color, 'blue')
|
268
|
+
# user.preferred(:color) # => "blue"
|
269
|
+
def preferred(name, group = nil)
|
270
|
+
name = name.to_s
|
271
|
+
|
272
|
+
if @preference_values && @preference_values[group] && @preference_values[group].include?(name)
|
273
|
+
# Value for this group/name has been written, but not saved yet:
|
274
|
+
# grab from the pending values
|
275
|
+
value = @preference_values[group][name]
|
276
|
+
else
|
277
|
+
# Split the group being filtered
|
278
|
+
group_id, group_type = Preference.split_group(group)
|
279
|
+
|
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
|
+
value = preference ? preference.value : preference_definitions[name].default_value
|
283
|
+
end
|
284
|
+
|
285
|
+
value
|
286
|
+
end
|
287
|
+
alias_method :prefers, :preferred
|
288
|
+
|
289
|
+
# Sets a new value for the given preference. The actual Preference record
|
290
|
+
# is *not* created until this record is saved. In this way, preferences
|
291
|
+
# act *exactly* the same as attributes. They can be written to and
|
292
|
+
# validated against, but won't actually be written to the database until
|
293
|
+
# the record is saved.
|
294
|
+
#
|
295
|
+
# == Examples
|
296
|
+
#
|
297
|
+
# user = User.find(:first)
|
298
|
+
# user.set_preference(:color, 'red') # => "red"
|
299
|
+
# user.save!
|
300
|
+
#
|
301
|
+
# user.set_preference(:color, 'blue', Car.first) # => "blue"
|
302
|
+
# user.save!
|
303
|
+
def set_preference(name, value, group = nil)
|
304
|
+
name = name.to_s
|
305
|
+
|
306
|
+
@preference_values ||= {}
|
307
|
+
@preference_values[group] ||= {}
|
308
|
+
@preference_values[group][name] = value
|
309
|
+
|
310
|
+
value
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
# Updates any preferences that have been changed/added since the record
|
315
|
+
# was last saved
|
316
|
+
def update_preferences
|
317
|
+
if @preference_values
|
318
|
+
@preference_values.each do |group, new_preferences|
|
319
|
+
group_id, group_type = Preference.split_group(group)
|
320
|
+
|
321
|
+
new_preferences.each do |name, value|
|
322
|
+
attributes = {:name => name, :group_id => group_id, :group_type => group_type}
|
323
|
+
|
324
|
+
# 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
|
327
|
+
preference.save!
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
@preference_values = nil
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
ActiveRecord::Base.class_eval do
|
338
|
+
extend Preferences::MacroMethods
|
339
|
+
end
|