preferences 0.0.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 ADDED
@@ -0,0 +1,5 @@
1
+ *SVN*
2
+
3
+ *0.0.1* (May 10th, 2008)
4
+
5
+ * Initial public release
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 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 ADDED
@@ -0,0 +1,146 @@
1
+ == preferences
2
+
3
+ +preferences+ adds support for easily creating custom preferences for models.
4
+
5
+ == Resources
6
+
7
+ Wiki
8
+
9
+ * http://wiki.pluginaweek.org/Preferences
10
+
11
+ Source
12
+
13
+ * http://svn.pluginaweek.org/trunk/plugins/preferences
14
+
15
+ Development
16
+
17
+ * http://dev.pluginaweek.org/browser/trunk/preferences
18
+
19
+ == Description
20
+
21
+ Preferences for models within an application, such as for users, is a pretty
22
+ common idiom. Although the rule of thumb is to keep the number of preferences
23
+ available to a minimum, sometimes it's necessary if you want users to be able to
24
+ disable things like e-mail notifications.
25
+
26
+ Generally, basic preferences can be accomplish through simple designs, such as
27
+ additional columns or a bit vector described and implemented by preference_fu[http://agilewebdevelopment.com/plugins/preferencefu].
28
+ However, as you find the need for non-binary preferences and the number of
29
+ preferences becomes unmanageable as individual columns in the database, the next
30
+ step is often to create a seprate "preferences" table. This is where the +preferences+
31
+ plugin comes in.
32
+
33
+ +preferences+ encapsulates this design by hiding the fact that preferences are
34
+ stored in a separate table and making it dead-simple to define and manage
35
+ preferences.
36
+
37
+ == Usage
38
+
39
+ === Defining preferences
40
+
41
+ To define the preferences for a model, you can do so right within the model:
42
+
43
+ class User < ActiveRecord::Base
44
+ preference :hot_salsa
45
+ preference :dark_chocolate, :default => true
46
+ preference :color, :string
47
+ preference :favorite_number
48
+ preference :language, :string, :default => 'English'
49
+ end
50
+
51
+ In the above model, 5 preferences have been defined:
52
+ * hot_salsa
53
+ * dark_chocolate
54
+ * color
55
+ * favorite_number
56
+ * language
57
+
58
+ For each preference, a data type and default value can be specified. If no
59
+ data type is given, it's considered a boolean value. If not default value is
60
+ given, the default is assumed to be nil.
61
+
62
+ === Accessing preferences
63
+
64
+ Once preferences have been defined for a model, they can be accessed either using
65
+ the shortcut methods that are generated for each preference or the generic methods
66
+ that are not specific to a particular preference.
67
+
68
+ ==== Shortcut methods
69
+
70
+ There are several shortcut methods that are generated. They are shown below.
71
+
72
+ Query methods:
73
+ user.prefers_hot_salsa? # => false
74
+ user.prefers_dark_chocolate? # => false
75
+
76
+ Reader methods:
77
+ user.preferred_color # => nil
78
+ user.preferred_language # => "English"
79
+
80
+ Writer methods:
81
+ user.prefers_hot_salsa = false # => false
82
+ user.preferred_language = 'English' # => "English"
83
+
84
+ ==== Generic methods
85
+
86
+ Each shortcut method is essentially a wrapper for the various generic methods
87
+ show below:
88
+
89
+ Query method:
90
+ user.prefers?(:hot_salsa) # => false
91
+ user.prefers?(:dark_chocolate) # => false
92
+
93
+ Reader method:
94
+ user.preferred(:color) # => nil
95
+ user.preferred(:language) # => "English"
96
+
97
+ Write method:
98
+ user.set_preference(:hot_salsa, false) # => false
99
+ user.set_preference(:language, "English") # => "English"
100
+
101
+ === Accessing all preferences
102
+
103
+ To get the collection of all preferences for a particular user, you can access
104
+ the +preferences+ has_many association which is automatically generated:
105
+
106
+ user.preferences
107
+
108
+ === Preferences for other records
109
+
110
+ In addition to defining generic preferences for the owning record, you can also
111
+ define preferences for other records. This is best shown through an example:
112
+
113
+ user = User.find(:first)
114
+ car = Car.find(:first)
115
+
116
+ user.preferred_color = 'red', {:for => car}
117
+ # user.set_preference(:color, 'red', :for => car) # The generic way
118
+
119
+ This will create a preference for the color "red" for the given car. In this way,
120
+ you can have "color" preferences for different records.
121
+
122
+ To access the preference for a particular record, you can use the same accessor
123
+ methods as before:
124
+
125
+ user.preferred_color(:for => car)
126
+ # user.preferred(:color, :for => car) # The generic way
127
+
128
+ === Saving preferences
129
+
130
+ Note that preferences are not saved until the owning record is saved. Preferences
131
+ are treated in a similar fashion to attributes. For example,
132
+
133
+ user = user.find(:first)
134
+ user.attributes = {:preferred_color => 'red'}
135
+ user.save!
136
+
137
+ Preferences are stored in a separate table assumed to be called "preferences".
138
+
139
+ == Testing
140
+
141
+ Before you can run any tests, the following gem must be installed:
142
+ * plugin_test_helper[http://wiki.pluginaweek.org/Plugin_test_helper]
143
+
144
+ == Dependencies
145
+
146
+ * plugins_plus[http://wiki.pluginaweek.org/Plugins_plus]
data/Rakefile ADDED
@@ -0,0 +1,80 @@
1
+ require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/sshpublisher'
5
+
6
+ PKG_NAME = 'preferences'
7
+ PKG_VERSION = '0.0.1'
8
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
9
+ RUBY_FORGE_PROJECT = 'pluginaweek'
10
+
11
+ desc 'Default: run unit tests.'
12
+ task :default => :test
13
+
14
+ desc 'Test the preferences plugin.'
15
+ Rake::TestTask.new(:test) do |t|
16
+ t.libs << 'lib'
17
+ t.pattern = 'test/**/*_test.rb'
18
+ t.verbose = true
19
+ end
20
+
21
+ desc 'Generate documentation for the preferences plugin.'
22
+ Rake::RDocTask.new(:rdoc) do |rdoc|
23
+ rdoc.rdoc_dir = 'rdoc'
24
+ rdoc.title = 'Preferences'
25
+ rdoc.template = '../rdoc_template.rb'
26
+ rdoc.options << '--line-numbers' << '--inline-source'
27
+ rdoc.rdoc_files.include('README')
28
+ rdoc.rdoc_files.include('lib/**/*.rb')
29
+ end
30
+
31
+ spec = Gem::Specification.new do |s|
32
+ s.name = PKG_NAME
33
+ s.version = PKG_VERSION
34
+ s.platform = Gem::Platform::RUBY
35
+ s.summary = 'Adds support for easily creating custom preferences for models'
36
+
37
+ s.files = FileList['{app,lib,test}/**/*'].to_a + %w(CHANGELOG init.rb MIT-LICENSE Rakefile README)
38
+ s.require_path = 'lib'
39
+ s.autorequire = 'preferences'
40
+ s.has_rdoc = true
41
+ s.test_files = Dir['test/**/*_test.rb']
42
+
43
+ s.author = 'Aaron Pfeifer'
44
+ s.email = 'aaron@pluginaweek.org'
45
+ s.homepage = 'http://www.pluginaweek.org'
46
+ end
47
+
48
+ Rake::GemPackageTask.new(spec) do |p|
49
+ p.gem_spec = spec
50
+ p.need_tar = true
51
+ p.need_zip = true
52
+ end
53
+
54
+ desc 'Publish the beta gem'
55
+ task :pgem => [:package] do
56
+ Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
57
+ end
58
+
59
+ desc 'Publish the API documentation'
60
+ task :pdoc => [:rdoc] do
61
+ Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{PKG_NAME}", 'rdoc').upload
62
+ end
63
+
64
+ desc 'Publish the API docs and gem'
65
+ task :publish => [:pgem, :pdoc, :release]
66
+
67
+ desc 'Publish the release files to RubyForge.'
68
+ task :release => [:gem, :package] do
69
+ require 'rubyforge'
70
+
71
+ ruby_forge = RubyForge.new
72
+ ruby_forge.login
73
+
74
+ %w( gem tgz zip ).each do |ext|
75
+ file = "pkg/#{PKG_FILE_NAME}.#{ext}"
76
+ puts "Releasing #{File.basename(file)}..."
77
+
78
+ ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ # Represents a preferred value for a particular preference on a model.
2
+ #
3
+ # == Targeted preferences
4
+ #
5
+ # In addition to simple named preferences, preferences can also be targeted for
6
+ # a particular record. For example, a User may have a preferred color for a
7
+ # particular Car. In this case, the +owner+ is the User, the +preference+ is
8
+ # the color, and the +target+ is the Car. This allows preferences to have a sort
9
+ # of context around them.
10
+ class Preference < ActiveRecord::Base
11
+ belongs_to :owner,
12
+ :polymorphic => true
13
+ belongs_to :preferenced,
14
+ :polymorphic => true
15
+
16
+ validates_presence_of :attribute,
17
+ :owner_id,
18
+ :owner_type
19
+ validates_presence_of :preferenced_id,
20
+ :preferenced_type,
21
+ :if => Proc.new {|p| p.preferenced_id? || p.preferenced_type?}
22
+
23
+ # The definition for the attribute
24
+ def definition
25
+ owner_type.constantize.preference_definitions[attribute] if owner_type
26
+ end
27
+
28
+ # Typecasts the value depending on the preference definition's declared type
29
+ def value
30
+ value = read_attribute(:value)
31
+ value = definition.type_cast(value) if definition
32
+ value
33
+ end
34
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'preferences'
@@ -0,0 +1,50 @@
1
+ module PluginAWeek #:nodoc:
2
+ # Adds support for defining preferences on ActiveRecord models.
3
+ module Preferences
4
+ # Represents the definition of a preference for a particular model
5
+ class PreferenceDefinition
6
+ def initialize(attribute, *args) #:nodoc:
7
+ options = args.extract_options!
8
+ options.assert_valid_keys(:default)
9
+
10
+ @type = args.first ? args.first.to_s : 'boolean'
11
+
12
+ # Create a column that will be responsible for typecasting
13
+ @column = ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, options[:default], @type == 'any' ? nil : @type)
14
+ end
15
+
16
+ # The attribute which is being preferenced
17
+ def attribute
18
+ @column.name
19
+ end
20
+
21
+ # The default value to use for the preference in case none have been
22
+ # previously defined
23
+ def default_value
24
+ @column.default
25
+ end
26
+
27
+ # Typecasts the value based on the type of preference that was defined
28
+ def type_cast(value)
29
+ if @type == 'any'
30
+ value
31
+ else
32
+ @column.type_cast(value)
33
+ end
34
+ end
35
+
36
+ # Typecasts the value to true/false depending on the type of preference
37
+ def query(value)
38
+ unless value = type_cast(value)
39
+ false
40
+ else
41
+ if @column.number?
42
+ !value.zero?
43
+ else
44
+ !value.blank?
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,235 @@
1
+ require 'preferences/preference_definition'
2
+
3
+ module PluginAWeek #:nodoc:
4
+ # Adds support for defining preferences on ActiveRecord models.
5
+ #
6
+ # == Saving preferences
7
+ #
8
+ # Preferences are not automatically saved when they are set. You must save
9
+ # the record that the preferences were set on.
10
+ #
11
+ # For example,
12
+ #
13
+ # class User < ActiveRecord::Base
14
+ # preference :notifications
15
+ # end
16
+ #
17
+ # u = User.new(:login => 'admin', :prefers_notifications => false)
18
+ # u.save!
19
+ #
20
+ # u = User.find_by_login('admin')
21
+ # u.attributes = {:prefers_notifications => true}
22
+ # u.save!
23
+ module Preferences
24
+ def self.included(base) #:nodoc:
25
+ base.class_eval do
26
+ extend PluginAWeek::Preferences::MacroMethods
27
+ end
28
+ end
29
+
30
+ module MacroMethods
31
+ # Defines a new preference for all records in the model. By default, preferences
32
+ # are assumed to have a boolean data type, so all values will be typecasted
33
+ # to true/false based on ActiveRecord rules.
34
+ #
35
+ # Configuration options:
36
+ # * +default+ - The default value for the preference. Default is nil.
37
+ #
38
+ # == Examples
39
+ #
40
+ # The example below shows the various ways to define a preference for a
41
+ # particular model.
42
+ #
43
+ # class User < ActiveRecord::Base
44
+ # preference :notifications, :default => false
45
+ # preference :color, :string, :default => 'red'
46
+ # preference :favorite_number, :integer
47
+ # preference :data, :any # Allows any data type to be stored
48
+ # end
49
+ #
50
+ # All preferences are also inherited by subclasses.
51
+ #
52
+ # == Associations
53
+ #
54
+ # After the first preference is defined, the following associations are
55
+ # created for the model:
56
+ # * +preferences+ - A collection of all the preferences specified for a record
57
+ #
58
+ # == Generated shortcut methods
59
+ #
60
+ # In addition to calling <tt>prefers?</tt> and +preferred+ on a record, you
61
+ # can also use the shortcut methods that are generated when a preference is
62
+ # defined. For example,
63
+ #
64
+ # class User < ActiveRecord::Base
65
+ # preference :notifications
66
+ # end
67
+ #
68
+ # ...generates the following methods:
69
+ # * <tt>prefers_notifications?</tt> - The same as calling <tt>record.prefers?(:notifications)</tt>
70
+ # * <tt>prefers_notifications=(value)</tt> - The same as calling <tt>record.set_preference(:notifications, value)</tt>
71
+ # * <tt>preferred_notifications</tt> - The same as called <tt>record.preferred(:notifications)</tt>
72
+ # * <tt>preferred_notifications=(value)</tt> - The same as calling <tt>record.set_preference(:notifications, value)</tt>
73
+ #
74
+ # Notice that there are two tenses used depending on the context of the
75
+ # preference. Conventionally, <tt>prefers_notifications?</tt> is better
76
+ # for boolean preferences, while +preferred_color+ is better for non-boolean
77
+ # preferences.
78
+ #
79
+ # Example:
80
+ #
81
+ # user = User.find(:first)
82
+ # user.prefers_notifications? # => false
83
+ # user.prefers_color? # => true
84
+ # user.preferred_color # => 'red'
85
+ # user.preferred_color = 'blue' # => 'blue'
86
+ #
87
+ # user.prefers_notifications = true
88
+ #
89
+ # car = Car.find(:first)
90
+ # user.preferred_color = 'red', {:for => car} # => 'red'
91
+ # user.preferred_color(:for => car) # => 'red'
92
+ # user.prefers_color?(:for => car) # => true
93
+ #
94
+ # user.save! # => true
95
+ def preference(attribute, *args)
96
+ unless included_modules.include?(InstanceMethods)
97
+ class_inheritable_hash :preference_definitions
98
+
99
+ has_many :preferences,
100
+ :as => :owner
101
+
102
+ after_save :update_preferences
103
+
104
+ include PluginAWeek::Preferences::InstanceMethods
105
+ end
106
+
107
+ # Create the definition
108
+ attribute = attribute.to_s
109
+ definition = PreferenceDefinition.new(attribute, *args)
110
+ self.preference_definitions = {attribute => definition}
111
+
112
+ # Create short-hand helper methods, making sure that the attribute
113
+ # is method-safe in terms of what characters are allowed
114
+ attribute = attribute.gsub(/[^A-Za-z0-9_-]/, '').underscore
115
+ class_eval <<-end_eval
116
+ def prefers_#{attribute}?(options = {})
117
+ prefers?(#{attribute.dump}, options)
118
+ end
119
+
120
+ def prefers_#{attribute}=(args)
121
+ set_preference(*([#{attribute.dump}] + [args].flatten))
122
+ end
123
+
124
+ def preferred_#{attribute}(options = {})
125
+ preferred(#{attribute.dump}, options)
126
+ end
127
+
128
+ alias_method :preferred_#{attribute}=, :prefers_#{attribute}=
129
+ end_eval
130
+
131
+ definition
132
+ end
133
+ end
134
+
135
+ module InstanceMethods
136
+ # Queries whether or not a value has been specified for the given attribute.
137
+ # This is dependent on how the value is type-casted.
138
+ #
139
+ # Configuration options:
140
+ # * +for+ - The record being preferenced
141
+ #
142
+ # == Examples
143
+ #
144
+ # user = User.find(:first)
145
+ # user.prefers?(:notifications) # => true
146
+ #
147
+ # newsgroup = Newsgroup.find(:first)
148
+ # user.prefers?(:notifications, :for => newsgroup) # => false
149
+ def prefers?(attribute, options = {})
150
+ attribute = attribute.to_s
151
+
152
+ value = preferred(attribute, options)
153
+ preference_definitions[attribute].query(value)
154
+ end
155
+
156
+ # Gets the preferred value for the given attribute.
157
+ #
158
+ # Configuration options:
159
+ # * +for+ - The record being preferenced
160
+ #
161
+ # == Examples
162
+ #
163
+ # user = User.find(:first)
164
+ # user.preferred(:color) # => 'red'
165
+ #
166
+ # car = Car.find(:first)
167
+ # user.preferred(:color, :for => car) # => 'black'
168
+ def preferred(attribute, options = {})
169
+ options.assert_valid_keys(:for)
170
+ attribute = attribute.to_s
171
+
172
+ if @preference_values && @preference_values[attribute] && @preference_values[attribute].include?(options[:for])
173
+ value = @preference_values[attribute][options[:for]]
174
+ else
175
+ preferenced_id, preferenced_type = options[:for].id, options[:for].class.base_class.name.to_s if options[:for]
176
+ preference = preferences.find(:first, :conditions => {:attribute => attribute, :preferenced_id => preferenced_id, :preferenced_type => preferenced_type})
177
+ value = preference ? preference.value : preference_definitions[attribute].default_value
178
+ end
179
+
180
+ value
181
+ end
182
+
183
+ # Sets a new value for the given attribute. The actual Preference record
184
+ # is *not* created until the actual record is saved.
185
+ #
186
+ # Configuration options:
187
+ # * +for+ - The record being preferenced
188
+ #
189
+ # == Examples
190
+ #
191
+ # user = User.find(:first)
192
+ # user.set_preference(:notifications, false) # => false
193
+ # user.save!
194
+ #
195
+ # newsgroup = Newsgroup.find(:first)
196
+ # user.set_preference(:notifications, true, :for => newsgroup) # => true
197
+ # user.save!
198
+ def set_preference(attribute, value, options = {})
199
+ options.assert_valid_keys(:for)
200
+ attribute = attribute.to_s
201
+
202
+ @preference_values ||= {}
203
+ @preference_values[attribute] ||= {}
204
+ @preference_values[attribute][options[:for]] = value
205
+
206
+ value
207
+ end
208
+
209
+ private
210
+ # Updates any preferences that have been changed/added since the record
211
+ # was last saved
212
+ def update_preferences
213
+ if @preference_values
214
+ @preference_values.each do |attribute, preferenced_records|
215
+ preferenced_records.each do |preferenced, value|
216
+ preferenced_id, preferenced_type = preferenced.id, preferenced.class.base_class.name.to_s if preferenced
217
+ attributes = {:attribute => attribute, :preferenced_id => preferenced_id, :preferenced_type => preferenced_type}
218
+
219
+ # Find an existing preference or build a new one
220
+ preference = preferences.find(:first, :conditions => attributes) || preferences.build(attributes)
221
+ preference.value = value
222
+ preference.save!
223
+ end
224
+ end
225
+
226
+ @preference_values = nil
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ ActiveRecord::Base.class_eval do
234
+ include PluginAWeek::Preferences
235
+ end
@@ -0,0 +1,2 @@
1
+ class Car < ActiveRecord::Base
2
+ end
@@ -0,0 +1,7 @@
1
+ class User < ActiveRecord::Base
2
+ preference :hot_salsa
3
+ preference :dark_chocolate, :default => true
4
+ preference :color, :string
5
+ preference :car, :integer
6
+ preference :language, :string, :default => 'English'
7
+ end
@@ -0,0 +1,9 @@
1
+ require 'config/boot'
2
+ require "#{File.dirname(__FILE__)}/../../../../plugins_plus/boot"
3
+
4
+ Rails::Initializer.run do |config|
5
+ config.plugin_paths << '..'
6
+ config.plugins = %w(plugins_plus preferences)
7
+ config.cache_classes = false
8
+ config.whiny_nils = true
9
+ end
@@ -0,0 +1,11 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.string :login, :null => false
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :users
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class CreateCars < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :cars do |t|
4
+ t.string :name, :null => false
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :cars
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class MigratePreferencesToVersion1 < ActiveRecord::Migration
2
+ def self.up
3
+ Rails::Plugin.find(:preferences).migrate(1)
4
+ end
5
+
6
+ def self.down
7
+ Rails::Plugin.find(:preferences).migrate(0)
8
+ end
9
+ end