Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Rename to preferences

Add documentation
Completely rewrite from scratch
  • Loading branch information...
commit ffcf63dc83110fa7c2ffcd6e12368d2e5d2069dc 1 parent 812e7ab
Aaron Pfeifer authored
3  CHANGELOG
... ...
@@ -1,2 +1,5 @@
1 1
 *SVN*
2 2
 
  3
+*0.0.1* (May 10th, 2008)
  4
+
  5
+* Initial public release
4  MIT-LICENSE
... ...
@@ -1,4 +1,4 @@
1  
-Copyright (c) 2006-2007 Aaron Pfeifer & Neil Abraham
  1
+Copyright (c) 2008 Aaron Pfeifer
2 2
 
3 3
 Permission is hereby granted, free of charge, to any person obtaining
4 4
 a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 17
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 18
 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 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.
  20
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
148  README
... ...
@@ -1,46 +1,146 @@
1  
-== acts_as_preferenced
  1
+== preferences
2 2
 
3  
-acts_as_preferenced .
  3
++preferences+ adds support for easily creating custom preferences for models.
4 4
 
5 5
 == Resources
6 6
 
7 7
 Wiki
8 8
 
9  
-* http://wiki.pluginaweek.org/Acts_as_preferenced
10  
-
11  
-Announcement
12  
-
13  
-* http://www.pluginaweek.org/
  9
+* http://wiki.pluginaweek.org/Preferences
14 10
 
15 11
 Source
16 12
 
17  
-* http://svn.pluginaweek.org/trunk/plugins/active_record/acts/acts_as_preferenced
  13
+* http://svn.pluginaweek.org/trunk/plugins/preferences
18 14
 
19 15
 Development
20 16
 
21  
-* http://dev.pluginaweek.org/browser/trunk/plugins/active_record/acts/acts_as_preferenced
  17
+* http://dev.pluginaweek.org/browser/trunk/preferences
22 18
 
23 19
 == Description
24 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
25 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,
26 132
 
27  
-== Examples
  133
+  user = user.find(:first)
  134
+  user.attributes = {:preferred_color => 'red'}
  135
+  user.save!
28 136
 
29  
-user.preferred_email_address (text)
30  
-user.prefers_milk? (boolean)
31  
-user.preferred_color (enum)
  137
+Preferences are stored in a separate table assumed to be called "preferences".
32 138
 
33  
-preference :email_address (infers :any)
34  
-preference :email_address,  :any
35  
-preference :milk,           :boolean, :default => true
36  
-preference :color,          :enum,    :in => %w(red white blue), :default => 'red'
  139
+== Testing
37 140
 
38  
-# If user can choose what products to hide:
39  
-user.prefers_hidden_for?(product)
40  
-user.preferred_color_for?(product)
  141
+Before you can run any tests, the following gem must be installed:
  142
+* plugin_test_helper[http://wiki.pluginaweek.org/Plugin_test_helper]
41 143
 
42  
-preference :hiddden, :boolean, :default => false, :for => 'Product'
43  
-preference :color,   :enum,    :in => %w(red white blue), :default => 'red', :for => 'Product'
  144
+== Dependencies
44 145
 
45  
-# If you want to specify different specs for a different type
46  
-preference :color,   :enum,    :in => %w(black white), :default => 'white', :for => 'Car'
  146
+* plugins_plus[http://wiki.pluginaweek.org/Plugins_plus]
27  Rakefile
@@ -3,7 +3,7 @@ require 'rake/rdoctask'
3 3
 require 'rake/gempackagetask'
4 4
 require 'rake/contrib/sshpublisher'
5 5
 
6  
-PKG_NAME           = 'acts_as_preferenced'
  6
+PKG_NAME           = 'preferences'
7 7
 PKG_VERSION        = '0.0.1'
8 8
 PKG_FILE_NAME      = "#{PKG_NAME}-#{PKG_VERSION}"
9 9
 RUBY_FORGE_PROJECT = 'pluginaweek'
@@ -11,17 +11,18 @@ RUBY_FORGE_PROJECT = 'pluginaweek'
11 11
 desc 'Default: run unit tests.'
12 12
 task :default => :test
13 13
 
14  
-desc 'Test the acts_as_preferenced plugin.'
  14
+desc 'Test the preferences plugin.'
15 15
 Rake::TestTask.new(:test) do |t|
16 16
   t.libs << 'lib'
17 17
   t.pattern = 'test/**/*_test.rb'
18 18
   t.verbose = true
19 19
 end
20 20
 
21  
-desc 'Generate documentation for the acts_as_preferenced plugin.'
  21
+desc 'Generate documentation for the preferences plugin.'
22 22
 Rake::RDocTask.new(:rdoc) do |rdoc|
23 23
   rdoc.rdoc_dir = 'rdoc'
24  
-  rdoc.title    = 'ActsAsPreferenced'
  24
+  rdoc.title    = 'Preferences'
  25
+  rdoc.template = '../rdoc_template.rb'
25 26
   rdoc.options << '--line-numbers' << '--inline-source'
26 27
   rdoc.rdoc_files.include('README')
27 28
   rdoc.rdoc_files.include('lib/**/*.rb')
@@ -31,16 +32,16 @@ spec = Gem::Specification.new do |s|
31 32
   s.name            = PKG_NAME
32 33
   s.version         = PKG_VERSION
33 34
   s.platform        = Gem::Platform::RUBY
34  
-  s.summary         = ''
  35
+  s.summary         = 'Adds support for easily creating custom preferences for models'
35 36
   
36  
-  s.files           = FileList['{app,db,lib,tasks,test}/**/*'].to_a + %w(init.rb MIT-LICENSE Rakefile README)
  37
+  s.files           = FileList['{app,lib,test}/**/*'].to_a + %w(CHANGELOG init.rb MIT-LICENSE Rakefile README)
37 38
   s.require_path    = 'lib'
38  
-  s.autorequire     = 'acts_as_preferenced'
  39
+  s.autorequire     = 'preferences'
39 40
   s.has_rdoc        = true
40 41
   s.test_files      = Dir['test/**/*_test.rb']
41 42
   
42  
-  s.author          = 'Aaron Pfeifer, Neil Abraham'
43  
-  s.email           = 'info@pluginaweek.org'
  43
+  s.author          = 'Aaron Pfeifer'
  44
+  s.email           = 'aaron@pluginaweek.org'
44 45
   s.homepage        = 'http://www.pluginaweek.org'
45 46
 end
46 47
   
@@ -52,16 +53,16 @@ end
52 53
 
53 54
 desc 'Publish the beta gem'
54 55
 task :pgem => [:package] do
55  
-  Rake::SshFilePublisher.new('pluginaweek@pluginaweek.org', '/home/pluginaweek/gems.pluginaweek.org/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
  56
+  Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
56 57
 end
57 58
 
58 59
 desc 'Publish the API documentation'
59 60
 task :pdoc => [:rdoc] do
60  
-  Rake::SshDirPublisher.new('pluginaweek@pluginaweek.org', "/home/pluginaweek/api.pluginaweek.org/#{PKG_NAME}", 'rdoc').upload
  61
+  Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{PKG_NAME}", 'rdoc').upload
61 62
 end
62 63
 
63 64
 desc 'Publish the API docs and gem'
64  
-task :publish => [:pdoc, :release]
  65
+task :publish => [:pgem, :pdoc, :release]
65 66
 
66 67
 desc 'Publish the release files to RubyForge.'
67 68
 task :release => [:gem, :package] do
@@ -76,4 +77,4 @@ task :release => [:gem, :package] do
76 77
     
77 78
     ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
78 79
   end
79  
-end
  80
+end
48  app/models/preference.rb
... ...
@@ -1,26 +1,34 @@
  1
+# Represents a preferred value for a particular preference on a model.
1 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.
2 10
 class Preference < ActiveRecord::Base
3  
-  belongs_to            :definition,
4  
-                          :class_name => 'PreferenceDefinition',
5  
-                          :foreign_key => 'definition_id'
6  
-  belongs_to            :owner,
7  
-                          :polymorphic => true
8  
-  belongs_to            :preferenced,
9  
-                          :polymorphic => true
  11
+  belongs_to  :owner,
  12
+                :polymorphic => true
  13
+  belongs_to  :preferenced,
  14
+                :polymorphic => true
10 15
   
11  
-  validates_presence_of :definition_id,
  16
+  validates_presence_of :attribute,
12 17
                         :owner_id,
13  
-                        :preferenced_id,
14  
-                        :preferenced_type
15  
-                        
16  
-  delegate              :default_value,
17  
-                        :data_type,
18  
-                        :possible_values,
19  
-                          :to => :definition
  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
20 27
   
21  
-  # 
22  
-  def validate
23  
-    @errors.add 'preferenced_type', 'is not a valid type' unless definition.valid_preference?(preferenced_type)
24  
-    @errors.add 'value', "must be #{possible_values.to_sentence(:connector => 'or')}" unless definition.valid_value?(value)
  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
25 33
   end
26  
-end
  34
+end
34  app/models/preference_definition.rb
... ...
@@ -1,34 +0,0 @@
1  
-#
2  
-class PreferenceDefinition < ActiveRecord::Base
3  
-  has_many              :preferences,
4  
-                          :foreign_key => 'definition_id'
5  
-  
6  
-  validates_presence_of :name
7  
-  validates_format_of   :name,
8  
-                          :with => /\w/
9  
-  
10  
-  #
11  
-  def default_value(preferenced_type = nil)
12  
-    self.class.parent.default_value_for_preference(name, preferenced_type)
13  
-  end
14  
-  
15  
-  #
16  
-  def data_type(preferenced_type = nil)
17  
-    self.class.parent.data_type_for_preference(name, preferenced_type)
18  
-  end
19  
-  
20  
-  #
21  
-  def possible_values(preferenced_type = nil)
22  
-    self.class.parent.possible_values_for_preference(name, preferenced_type)
23  
-  end
24  
-  
25  
-  #
26  
-  def valid_preference?(preferenced_type = nil)
27  
-    self.class.parent.valid_preference?(name, preferenced_type)
28  
-  end
29  
-  
30  
-  #
31  
-  def valid_value?(value)
32  
-    possible_values.nil? || possible_values.empty? || possible_values.include?(value)
33  
-  end
34  
-end
17  db/migrate/001_create_preference_definitions.rb
... ...
@@ -1,17 +0,0 @@
1  
-class CreatePreferenceDefinitions < ActiveRecord::Migration
2  
-  def self.up
3  
-    create_table :preference_definitions do |t|
4  
-      t.column :name,         :string,    :null => false
5  
-      t.column :description,  :text
6  
-      t.column :owner_type,   :string,    :null => false
7  
-      t.column :created_at,   :timestamp, :null => false
8  
-      t.column :updated_at,   :datetime,  :null => false
9  
-      t.column :deleted_at,   :datetime
10  
-    end
11  
-    add_index :preference_definitions, [:owner_type, :name], :unique => true
12  
-  end
13  
-  
14  
-  def self.down
15  
-    drop_table :preference_definitions
16  
-  end
17  
-end
16  db/migrate/001_create_preferences.rb
... ...
@@ -0,0 +1,16 @@
  1
+class CreatePreferences < ActiveRecord::Migration
  2
+  def self.up
  3
+    create_table :preferences do |t|
  4
+      t.string :attribute, :null => false
  5
+      t.references :owner, :polymorphic => true, :null => false
  6
+      t.references :preferenced, :polymorphic => true
  7
+      t.string :value
  8
+      t.timestamps
  9
+    end
  10
+    add_index :preferences, [:owner_id, :owner_type, :attribute, :preferenced_id, :preferenced_type], :unique => true, :name => 'index_preferences_on_owner_and_attribute_and_preference'
  11
+  end
  12
+  
  13
+  def self.down
  14
+    drop_table :preferences
  15
+  end
  16
+end
19  db/migrate/002_create_preferences.rb
... ...
@@ -1,19 +0,0 @@
1  
-class CreatePreferences < ActiveRecord::Migration
2  
-  def self.up
3  
-    create_table :preferences do |t|
4  
-      t.column :definition_id,    :integer,   :null => false, :unsigned => true, :references => :preference_definitions
5  
-      t.column :owner_id,         :integer,   :null => false, :unsigned => true, :references => nil
6  
-      t.column :owner_type,       :string,    :null => false
7  
-      t.column :preferenced_id,   :integer,   :unsigned => true, :references => nil
8  
-      t.column :preferenced_type, :string
9  
-      t.column :value,            :string
10  
-      t.column :created_at,       :timestamp, :null => false
11  
-      t.column :updated_at,       :datetime,  :null => false
12  
-    end
13  
-    add_index :preferences, [:owner_id, :owner_type, :definition_id, :preferenced_id, :preferenced_type], :unique => true, :name => 'index_preferences_on_owner_and_definition_and_preference'
14  
-  end
15  
-  
16  
-  def self.down
17  
-    drop_table :preferences
18  
-  end
19  
-end
2  init.rb
... ...
@@ -1 +1 @@
1  
-require 'acts_as_preferenced'
  1
+require 'preferences'
211  lib/acts_as_preferenced.rb
... ...
@@ -1,211 +0,0 @@
1  
-require 'class_associations'
2  
-
3  
-module PluginAWeek #:nodoc:
4  
-  module Acts #:nodoc:
5  
-    module Preferenced #:nodoc:
6  
-      # An unknown preference definition was specified
7  
-      class InvalidPreferenceDefinition < Exception
8  
-      end
9  
-      
10  
-      # An unknown preference type was specified
11  
-      class InvalidPreferenceType < Exception
12  
-      end
13  
-      
14  
-      # An invalid preference value was specified
15  
-      class InvalidPreferenceValue < Exception
16  
-      end
17  
-      
18  
-      def self.included(base) #:nodoc:
19  
-        base.extend(MacroMethods)
20  
-      end
21  
-      
22  
-      module SupportingClasses #:nodoc:
23  
-        #
24  
-        class PreferenceDefinition
25  
-          attr_reader :name
26  
-          attr_reader :data_type
27  
-          attr_reader :possible_values
28  
-          attr_reader :default_value
29  
-          
30  
-          def initialize(name, type, options)
31  
-            options.assert_valid_keys(:type, :in, :within, :default, :for)
32  
-            
33  
-            @name = name
34  
-            @data_type = type
35  
-            @possible_values = type == :enum ? options[:in] || options[:within] : []
36  
-            @default_value = options[:default]
37  
-            
38  
-            if @default_value.nil?
39  
-              @default_value =
40  
-                case type
41  
-                when :boolean
42  
-                  false
43  
-                else
44  
-                  @possible_values.first
45  
-              end
46  
-            end
47  
-          end
48  
-        end
49  
-      end
50  
-      
51  
-      module MacroMethods
52  
-        # 
53  
-        def acts_as_preferenced(options = {})
54  
-          options.symbolize_keys!.assert_valid_keys(:on_error)
55  
-          
56  
-          write_inheritable_attribute :preference_definitions, {}
57  
-          write_inheritable_attribute :preference_error_handler, options[:on_error]
58  
-          
59  
-          has_many  :preferences,
60  
-                      :as => :owner,
61  
-                      :dependent => :destroy do
62  
-            # 
63  
-            def find_by_preferenced(definition_id, record)
64  
-              find_by_definition_id_and_preferenced_id_and_preferenced_type(definition_id, record.id, record.class.name)
65  
-            end
66  
-            
67  
-            # 
68  
-            def find_or_initialize_by_preferenced(definition_id, record)
69  
-              find_by_preferenced(definition_id, record) ||
70  
-              build(
71  
-                :definition_id => definition_id,
72  
-                :preferenced_id => record.id,
73  
-                :preferenced_type => record.class.name
74  
-              )
75  
-            end
76  
-          end
77  
-          
78  
-          class << self
79  
-            has_many  :preference_definitions,
80  
-                        :include_superclasses => true
81  
-          end
82  
-          
83  
-          extend PluginAWeek::Acts::Preferenced::ClassMethods
84  
-          include PluginAWeek::Acts::Preferenced::InstanceMethods
85  
-        end
86  
-      end
87  
-      
88  
-      module ClassMethods
89  
-        #
90  
-        def data_type_for_preference(name, preferenced_type = nil)
91  
-          get_definition_value(name, preferenced_type, :data_type)
92  
-        end
93  
-        
94  
-        #
95  
-        def possible_values_for_preference(name, preferenced_type = nil)
96  
-          get_definition_value(name, preferenced_type, :possible_values)
97  
-        end
98  
-        
99  
-        #
100  
-        def default_value_for_preference(name, preferenced_type = nil)
101  
-          get_definition_value(name, preferenced_type, :default_value)
102  
-        end
103  
-        
104  
-        #
105  
-        def valid_preference?(name, preferenced_type = nil)
106  
-          begin
107  
-            get_definition(name, preferenced_type)
108  
-            true
109  
-          rescue InvalidPreferenceDefinition
110  
-            false
111  
-          end
112  
-        end
113  
-        
114  
-        #
115  
-        def preference(name, type, options = {})
116  
-          options.symbolize_keys!.reverse_merge!(
117  
-            :type => :any,
118  
-            :for => [self.name]
119  
-          )
120  
-          name = name.to_s
121  
-          
122  
-          definition = SupportingClasses::PreferenceDefinition.new(name, type, options)
123  
-          
124  
-          preference_definitions = read_inheritable_attribute(:preference_definitions)
125  
-          if (name_definitions = preference_definitions[name]).nil?
126  
-            name_definitions = preference_definitions[name] = {}
127  
-            define_preference_accessors(name, type)
128  
-          end
129  
-          
130  
-          Array(options[:for]).each {|type| name_definitions[type.constantize] = definition}
131  
-        end
132  
-        
133  
-        private
134  
-        VALID_PREFERENCE_TYPES = [:boolean, :enum, :any]
135  
-        
136  
-        #
137  
-        def define_preference_accessors(name, type) #:nodoc:
138  
-          type = type.to_sym
139  
-          raise InvalidPreferenceType, "type must be #{VALID_PREFERENCE_TYPES.to_sentence(:connector => 'or')}, was: #{type}" if !VALID_PREFERENCE_TYPES.include?(type)
140  
-          
141  
-          if type.to_sym == :boolean
142  
-            prefix = 'prefers'
143  
-            suffix = '?'
144  
-            query_value = 'value?'
145  
-          else
146  
-            prefix = 'preferred'
147  
-            suffix = ''
148  
-            query_value = 'value'
149  
-          end
150  
-          
151  
-          definition = self.preference_definitions.find_by_name(name)
152  
-          raise InvalidPreferenceDefinition, "Preference definition for #{name} not found for #{self.name}" if definition.nil?
153  
-          
154  
-          class_eval <<-end_eval
155  
-            def #{prefix}_#{name}#{suffix}
156  
-              #{prefix}_#{name}_for#{suffix}(self)
157  
-            end
158  
-            
159  
-            def #{prefix}_#{name}_for#{suffix}(record)
160  
-              preference = preferences.find_by_preferenced(#{definition.id}, record)
161  
-              preference ? preference.#{query_value} : self.class.default_value_for_preference('#{name}', record.class.name)
162  
-            end
163  
-            
164  
-            def #{prefix}_#{name}=(value, record = nil)
165  
-              preference = preferences.find_or_initialize_by_preferenced(#{definition.id}, record || self)
166  
-              preference.value = value
167  
-              success = preference.save
168  
-              
169  
-              if !success && handler = self.class.read_inheritable_attribute(:preference_error_handler)
170  
-                case handler
171  
-                  when :raise_exception
172  
-                    raise InvalidPreferenceValue, preference
173  
-                  when :add_errors_to_base
174  
-                    preference.errors.each {|attr, msg| errors.add(attr, msg)} if preference.errors.size > 0
175  
-                  else
176  
-                    handler.call(preference)
177  
-                end
178  
-              end
179  
-            end
180  
-          end_eval
181  
-        end
182  
-        
183  
-        # 
184  
-        def get_definition_value(name, preferenced_type, value_name) #:nodoc:
185  
-          definition = get_definition(name, preferenced_type)
186  
-          definition.send(value_name)
187  
-        end
188  
-        
189  
-        # 
190  
-        def get_definition(name, preferenced_type)
191  
-          preferenced_type = preferenced_type.nil? ? self : preferenced_type.constantize
192  
-          
193  
-          name_definitions = read_inheritable_attribute(:preference_definitions)[name]
194  
-          raise InvalidPreferenceDefinition, "Preference definition for #{name} not found for #{self.name}" if name_definitions.nil?
195  
-          
196  
-          preferenced_type = name_definitions.keys.find {|type| preferenced_type <= type}
197  
-          raise InvalidPreferenceDefinition, "#{preferenced_type} is not a preferenced type for #{name}" if preferenced_type.nil?
198  
-          
199  
-          name_definitions[preferenced_type]
200  
-        end
201  
-      end
202  
-      
203  
-      module InstanceMethods #:nodoc:
204  
-      end
205  
-    end
206  
-  end
207  
-end
208  
-
209  
-ActiveRecord::Base.class_eval do
210  
-  include PluginAWeek::Acts::Preferenced
211  
-end
235  lib/preferences.rb
... ...
@@ -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
50  lib/preferences/preference_definition.rb
... ...
@@ -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
8  test/acts_as_preferenced_test.rb
... ...
@@ -1,8 +0,0 @@
1  
-require 'test/unit'
2  
-
3  
-class ActsAsPreferencedTest < Test::Unit::TestCase
4  
-  # Replace this with your real tests.
5  
-  def test_this_plugin
6  
-    flunk
7  
-  end
8  
-end
2  test/app_root/app/models/car.rb
... ...
@@ -0,0 +1,2 @@
  1
+class Car < ActiveRecord::Base
  2
+end
7  test/app_root/app/models/user.rb
... ...
@@ -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
9  test/app_root/config/environment.rb
... ...
@@ -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
11  test/app_root/db/migrate/001_create_users.rb
... ...
@@ -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
11  test/app_root/db/migrate/002_create_cars.rb
... ...
@@ -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
9  test/app_root/db/migrate/003_migrate_preferences_to_version_1.rb
... ...
@@ -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
45  test/factory.rb
... ...
@@ -0,0 +1,45 @@
  1
+module Factory
  2
+  # Build actions for the class
  3
+  def self.build(klass, &block)
  4
+    name = klass.to_s.underscore
  5
+    define_method("#{name}_attributes", block)
  6
+    
  7
+    module_eval <<-end_eval
  8
+      def valid_#{name}_attributes(attributes = {})
  9
+        #{name}_attributes(attributes)
  10
+        attributes
  11
+      end
  12
+      
  13
+      def new_#{name}(attributes = {})
  14
+        #{klass}.new(valid_#{name}_attributes(attributes))
  15
+      end
  16
+      
  17
+      def create_#{name}(*args)
  18
+        record = new_#{name}(*args)
  19
+        record.save!
  20
+        record.reload
  21
+        record
  22
+      end
  23
+    end_eval
  24
+  end
  25
+  
  26
+  build Car do |attributes|
  27
+    attributes.reverse_merge!(
  28
+      :name => 'Porsche'
  29
+    )
  30
+  end
  31
+  
  32
+  build Preference do |attributes|
  33
+    attributes[:owner] = create_user unless attributes.include?(:owner)
  34
+    attributes.reverse_merge!(
  35
+      :attribute => 'notifications',
  36
+      :value => false
  37
+    )
  38
+  end
  39
+  
  40
+  build User do |attributes|
  41
+    attributes.reverse_merge!(
  42
+      :login => 'admin'
  43
+    )
  44
+  end
  45
+end
259  test/functional/preferences_test.rb
... ...
@@ -0,0 +1,259 @@
  1
+require "#{File.dirname(__FILE__)}/../test_helper"
  2
+
  3
+class PreferencesTest < Test::Unit::TestCase
  4
+  def setup
  5
+    @user = User.new
  6
+  end
  7
+  
  8
+  def test_should_raise_exception_if_invalid_options_specified
  9
+    assert_raise(ArgumentError) {User.preference :notifications, :invalid => true}
  10
+    assert_raise(ArgumentError) {User.preference :notifications, :boolean, :invalid => true}
  11
+  end
  12
+  
  13
+  def test_should_create_prefers_query_method
  14
+    assert @user.respond_to?(:prefers_notifications?)
  15
+  end
  16
+  
  17
+  def test_should_create_prefers_writer
  18
+    assert @user.respond_to?(:prefers_notifications=)
  19
+  end
  20
+  
  21
+  def test_should_create_preferred_reader
  22
+    assert @user.respond_to?(:preferred_notifications)
  23
+  end
  24
+  
  25
+  def test_should_create_preferred_writer
  26
+    assert @user.respond_to?(:preferred_notifications=)
  27
+  end
  28
+  
  29
+  def test_should_create_preference_definitions
  30
+    assert User.respond_to?(:preference_definitions)
  31
+  end
  32
+  
  33
+  def test_should_include_new_definitions_in_preference_definitions
  34
+    definition = User.preference :notifications
  35
+    assert_equal definition, User.preference_definitions['notifications']
  36
+  end
  37
+end
  38
+
  39
+class UserByDefaultTest < Test::Unit::TestCase
  40
+  def setup
  41
+    @user = User.new
  42
+  end
  43
+  
  44
+  def test_should_not_prefer_hot_salsa
  45
+    assert_nil @user.preferred_hot_salsa
  46
+  end
  47
+  
  48
+  def test_should_prefer_dark_chocolate
  49
+    assert_equal true, @user.preferred_dark_chocolate
  50
+  end
  51
+  
  52
+  def test_should_not_have_a_preferred_color
  53
+    assert_nil @user.preferred_color
  54
+  end
  55
+  
  56
+  def test_should_not_have_a_preferred_car
  57
+    assert_nil @user.preferred_car
  58
+  end
  59
+  
  60
+  def test_should_have_a_preferred_language
  61
+    assert_equal 'English', @user.preferred_language
  62
+  end
  63
+end
  64
+
  65
+class UserTest < Test::Unit::TestCase
  66
+  def setup
  67
+    @user = new_user
  68
+  end
  69
+  
  70
+  def test_should_be_able_to_change_hot_salsa_preference
  71
+    @user.prefers_hot_salsa = false
  72
+    assert_equal false, @user.prefers_hot_salsa?
  73
+    
  74
+    @user.prefers_hot_salsa = true
  75
+    assert_equal true, @user.prefers_hot_salsa?
  76
+  end
  77
+  
  78
+  def test_should_be_able_to_change_dark_chocolate_preference
  79
+    @user.prefers_dark_chocolate = false
  80
+    assert_equal false, @user.prefers_dark_chocolate?
  81
+    
  82
+    @user.prefers_dark_chocolate = true
  83
+    assert_equal true, @user.prefers_dark_chocolate?
  84
+  end
  85
+  
  86
+  def test_should_be_able_to_change_color_preference
  87
+    @user.preferred_color = 'blue'
  88
+    assert_equal 'blue', @user.preferred_color
  89
+  end
  90
+  
  91
+  def test_should_be_able_to_change_car_preference
  92
+    @user.preferred_car = 1
  93
+    assert_equal 1, @user.preferred_car
  94
+  end
  95
+  
  96
+  def test_should_be_able_to_change_language_preference
  97
+    @user.preferred_language = 'Latin'
  98
+    assert_equal 'Latin', @user.preferred_language
  99
+  end
  100
+  
  101
+  def test_should_be_able_to_use_generic_prefers_query_method
  102
+    @user.prefers_hot_salsa = true
  103
+    assert @user.prefers?(:hot_salsa)
  104
+  end
  105
+  
  106
+  def test_should_be_able_to_use_generic_preferred_method
  107
+    @user.preferred_color = 'blue'
  108
+    assert_equal 'blue', @user.preferred(:color)
  109
+  end
  110
+  
  111
+  def test_should_be_able_to_use_generic_set_preference_method