preferences 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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