diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 9364af2..f0a29a8 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,5 +1,7 @@ == master +* Remove the PluginAWeek namespace + == 0.1.5 / 2008-11-16 * Add all prefers/preferred accessors for preferences to be analogous to ActiveRecord column accessors diff --git a/lib/preferences.rb b/lib/preferences.rb index e37de08..7ee21b9 100644 --- a/lib/preferences.rb +++ b/lib/preferences.rb @@ -1,336 +1,334 @@ require 'preferences/preference_definition' -module PluginAWeek #:nodoc: - # Adds support for defining preferences on ActiveRecord models. - # - # == Saving preferences - # - # Preferences are not automatically saved when they are set. You must save - # the record that the preferences were set on. - # - # For example, - # - # class User < ActiveRecord::Base - # preference :notifications - # end - # - # u = User.new(:login => 'admin', :prefers_notifications => false) - # u.save! - # - # u = User.find_by_login('admin') - # u.attributes = {:prefers_notifications => true} - # u.save! - # - # == Validations - # - # Since the generated accessors for a preference allow the preference to be - # treated just like regular ActiveRecord column attributes, they can also be - # validated against in the same way. For example, - # - # class User < ActiveRecord::Base - # preference :color, :string - # - # validates_presence_of :preferred_color - # validates_inclusion_of :preferred_color, :in => %w(red green blue) - # end - # - # u = User.new - # u.valid? # => false - # u.errors.on(:preferred_color) # => "can't be blank" - # - # u.preferred_color = 'white' - # u.valid? # => false - # u.errors.on(:preferred_color) # => "is not included in the list" - # - # u.preferred_color = 'red' - # u.valid? # => true - module Preferences - module MacroMethods - # Defines a new preference for all records in the model. By default, - # preferences are assumed to have a boolean data type, so all values will - # be typecasted to true/false based on ActiveRecord rules. - # - # Configuration options: - # * +default+ - The default value for the preference. Default is nil. - # - # == Examples - # - # The example below shows the various ways to define a preference for a - # particular model. - # - # class User < ActiveRecord::Base - # preference :notifications, :default => false - # preference :color, :string, :default => 'red' - # preference :favorite_number, :integer - # preference :data, :any # Allows any data type to be stored - # end - # - # All preferences are also inherited by subclasses. - # - # == Associations - # - # After the first preference is defined, the following associations are - # created for the model: - # * +stored_preferences+ - A collection of all the custom preferences specified for a record. This will not include default preferences unless they have been explicitly set. - # - # == Generated accessors - # - # In addition to calling prefers? and +preferred+ on a record, you - # can also use the shortcut accessor methods that are generated when a - # preference is defined. For example, - # - # class User < ActiveRecord::Base - # preference :notifications - # end - # - # ...generates the following methods: - # * prefers_notifications? - Whether a value has been specified, i.e. record.prefers?(:notifications) - # * prefers_notifications - The actual value stored, i.e. record.prefers(:notifications) - # * prefers_notifications=(value) - Sets a new value, i.e. record.set_preference(:notifications, value) - # * preferred_notifications? - Whether a value has been specified, i.e. record.preferred?(:notifications) - # * preferred_notifications - The actual value stored, i.e. record.preferred(:notifications) - # * preferred_notifications=(value) - Sets a new value, i.e. record.set_preference(:notifications, value) - # - # Notice that there are two tenses used depending on the context of the - # preference. Conventionally, prefers_notifications? is better - # for accessing boolean preferences, while +preferred_color+ is better for - # accessing non-boolean preferences. - # - # Example: - # - # user = User.find(:first) - # user.prefers_notifications? # => false - # user.prefers_notifications # => false - # user.preferred_color? # => true - # user.preferred_color # => 'red' - # user.preferred_color = 'blue' # => 'blue' - # - # user.prefers_notifications = true - # - # car = Car.find(:first) - # user.preferred_color = 'red', car # => 'red' - # user.preferred_color(car) # => 'red' - # user.preferred_color?(car) # => true - # - # user.save! # => true - def preference(attribute, *args) - unless included_modules.include?(InstanceMethods) - class_inheritable_hash :preference_definitions - self.preference_definitions = {} - - class_inheritable_hash :default_preferences - self.default_preferences = {} - - has_many :stored_preferences, - :as => :owner, - :class_name => 'Preference' - - after_save :update_preferences - - include PluginAWeek::Preferences::InstanceMethods - end +# Adds support for defining preferences on ActiveRecord models. +# +# == Saving preferences +# +# Preferences are not automatically saved when they are set. You must save +# the record that the preferences were set on. +# +# For example, +# +# class User < ActiveRecord::Base +# preference :notifications +# end +# +# u = User.new(:login => 'admin', :prefers_notifications => false) +# u.save! +# +# u = User.find_by_login('admin') +# u.attributes = {:prefers_notifications => true} +# u.save! +# +# == Validations +# +# Since the generated accessors for a preference allow the preference to be +# treated just like regular ActiveRecord column attributes, they can also be +# validated against in the same way. For example, +# +# class User < ActiveRecord::Base +# preference :color, :string +# +# validates_presence_of :preferred_color +# validates_inclusion_of :preferred_color, :in => %w(red green blue) +# end +# +# u = User.new +# u.valid? # => false +# u.errors.on(:preferred_color) # => "can't be blank" +# +# u.preferred_color = 'white' +# u.valid? # => false +# u.errors.on(:preferred_color) # => "is not included in the list" +# +# u.preferred_color = 'red' +# u.valid? # => true +module Preferences + module MacroMethods + # Defines a new preference for all records in the model. By default, + # preferences are assumed to have a boolean data type, so all values will + # be typecasted to true/false based on ActiveRecord rules. + # + # Configuration options: + # * +default+ - The default value for the preference. Default is nil. + # + # == Examples + # + # The example below shows the various ways to define a preference for a + # particular model. + # + # class User < ActiveRecord::Base + # preference :notifications, :default => false + # preference :color, :string, :default => 'red' + # preference :favorite_number, :integer + # preference :data, :any # Allows any data type to be stored + # end + # + # All preferences are also inherited by subclasses. + # + # == Associations + # + # After the first preference is defined, the following associations are + # created for the model: + # * +stored_preferences+ - A collection of all the custom preferences specified for a record. This will not include default preferences unless they have been explicitly set. + # + # == Generated accessors + # + # In addition to calling prefers? and +preferred+ on a record, you + # can also use the shortcut accessor methods that are generated when a + # preference is defined. For example, + # + # class User < ActiveRecord::Base + # preference :notifications + # end + # + # ...generates the following methods: + # * prefers_notifications? - Whether a value has been specified, i.e. record.prefers?(:notifications) + # * prefers_notifications - The actual value stored, i.e. record.prefers(:notifications) + # * prefers_notifications=(value) - Sets a new value, i.e. record.set_preference(:notifications, value) + # * preferred_notifications? - Whether a value has been specified, i.e. record.preferred?(:notifications) + # * preferred_notifications - The actual value stored, i.e. record.preferred(:notifications) + # * preferred_notifications=(value) - Sets a new value, i.e. record.set_preference(:notifications, value) + # + # Notice that there are two tenses used depending on the context of the + # preference. Conventionally, prefers_notifications? is better + # for accessing boolean preferences, while +preferred_color+ is better for + # accessing non-boolean preferences. + # + # Example: + # + # user = User.find(:first) + # user.prefers_notifications? # => false + # user.prefers_notifications # => false + # user.preferred_color? # => true + # user.preferred_color # => 'red' + # user.preferred_color = 'blue' # => 'blue' + # + # user.prefers_notifications = true + # + # car = Car.find(:first) + # user.preferred_color = 'red', car # => 'red' + # user.preferred_color(car) # => 'red' + # user.preferred_color?(car) # => true + # + # user.save! # => true + def preference(attribute, *args) + unless included_modules.include?(InstanceMethods) + class_inheritable_hash :preference_definitions + self.preference_definitions = {} - # Create the definition - attribute = attribute.to_s - definition = PreferenceDefinition.new(attribute, *args) - self.preference_definitions[attribute] = definition - self.default_preferences[attribute] = definition.default_value + class_inheritable_hash :default_preferences + self.default_preferences = {} - # Create short-hand accessor methods, making sure that the attribute - # is method-safe in terms of what characters are allowed - attribute = attribute.gsub(/[^A-Za-z0-9_-]/, '').underscore + has_many :stored_preferences, + :as => :owner, + :class_name => 'Preference' - # Query lookup - define_method("preferred_#{attribute}?") do |*group| - preferred?(attribute, group.first) - end - alias_method "prefers_#{attribute}?", "preferred_#{attribute}?" + after_save :update_preferences - # Reader - define_method("preferred_#{attribute}") do |*group| - preferred(attribute, group.first) - end - alias_method "prefers_#{attribute}", "preferred_#{attribute}" - - # Writer - define_method("preferred_#{attribute}=") do |*args| - set_preference(*([attribute] + [args].flatten)) - end - alias_method "prefers_#{attribute}=", "preferred_#{attribute}=" - - definition + include Preferences::InstanceMethods end - end - - module InstanceMethods - def self.included(base) #:nodoc: - base.class_eval do - alias_method :prefs, :preferences - end + + # Create the definition + attribute = attribute.to_s + definition = PreferenceDefinition.new(attribute, *args) + self.preference_definitions[attribute] = definition + self.default_preferences[attribute] = definition.default_value + + # Create short-hand accessor methods, making sure that the attribute + # is method-safe in terms of what characters are allowed + attribute = attribute.gsub(/[^A-Za-z0-9_-]/, '').underscore + + # Query lookup + define_method("preferred_#{attribute}?") do |*group| + preferred?(attribute, group.first) end + alias_method "prefers_#{attribute}?", "preferred_#{attribute}?" - # Finds all preferences, including defaults, for the current record. If - # any custom group preferences have been stored, then this will include - # all default preferences within that particular group. - # - # == Examples - # - # A user with no stored values: - # user = User.find(:first) - # user.preferences - # => {"language"=>"English", "color"=>nil} - # - # A user with stored values for a particular group: - # user.preferred_color = 'red', 'cars' - # user.preferences - # => {"language"=>"English", "color"=>nil, "cars"=>{"language=>"English", "color"=>"red"}} - # - # Getting preference values *just* for the owning record (i.e. excluding groups): - # user.preferences(nil) - # => {"language"=>"English", "color"=>nil} - # - # Getting preference values for a particular group: - # user.preferences('cars') - # => {"language"=>"English", "color"=>"red"} - def preferences(*args) - if args.empty? - group = nil - conditions = {} - else - group = args.first - - # Split the actual group into its different parts (id/type) in case - # a record is passed in - group_id, group_type = Preference.split_group(group) - conditions = {:group_id => group_id, :group_type => group_type} - end - - # Find all of the stored preferences - stored_preferences = self.stored_preferences.find(:all, :conditions => conditions) - - # Hashify attribute -> value or group -> attribute -> value - stored_preferences.inject(self.class.default_preferences.dup) do |all_preferences, preference| - if !group && (preference_group = preference.group) - preferences = all_preferences[preference_group] ||= self.class.default_preferences.dup - else - preferences = all_preferences - end - - preferences[preference.attribute] = preference.value - all_preferences - end + # Reader + define_method("preferred_#{attribute}") do |*group| + preferred(attribute, group.first) end + alias_method "prefers_#{attribute}", "preferred_#{attribute}" - # Queries whether or not a value is present for the given attribute. This - # is dependent on how the value is type-casted. - # - # == Examples - # - # class User < ActiveRecord::Base - # preference :color, :string, :default => 'red' - # end - # - # user = User.create - # user.preferred(:color) # => "red" - # user.preferred?(:color) # => true - # user.preferred?(:color, 'cars') # => true - # user.preferred?(:color, Car.first) # => true - # - # user.set_preference(:color, nil) - # user.preferred(:color) # => nil - # user.preferred?(:color) # => false - def preferred?(attribute, group = nil) - attribute = attribute.to_s - - value = preferred(attribute, group) - preference_definitions[attribute].query(value) + # Writer + define_method("preferred_#{attribute}=") do |*args| + set_preference(*([attribute] + [args].flatten)) end - alias_method :prefers?, :preferred? + alias_method "prefers_#{attribute}=", "preferred_#{attribute}=" - # Gets the actual value stored for the given attribute, or the default - # value if nothing is present. - # - # == Examples - # - # class User < ActiveRecord::Base - # preference :color, :string, :default => 'red' - # end - # - # user = User.create - # user.preferred(:color) # => "red" - # user.preferred(:color, 'cars') # => "red" - # user.preferred(:color, Car.first) # => "red" - # - # user.set_preference(:color, 'blue') - # user.preferred(:color) # => "blue" - def preferred(attribute, group = nil) - attribute = attribute.to_s + definition + end + end + + module InstanceMethods + def self.included(base) #:nodoc: + base.class_eval do + alias_method :prefs, :preferences + end + end + + # Finds all preferences, including defaults, for the current record. If + # any custom group preferences have been stored, then this will include + # all default preferences within that particular group. + # + # == Examples + # + # A user with no stored values: + # user = User.find(:first) + # user.preferences + # => {"language"=>"English", "color"=>nil} + # + # A user with stored values for a particular group: + # user.preferred_color = 'red', 'cars' + # user.preferences + # => {"language"=>"English", "color"=>nil, "cars"=>{"language=>"English", "color"=>"red"}} + # + # Getting preference values *just* for the owning record (i.e. excluding groups): + # user.preferences(nil) + # => {"language"=>"English", "color"=>nil} + # + # Getting preference values for a particular group: + # user.preferences('cars') + # => {"language"=>"English", "color"=>"red"} + def preferences(*args) + if args.empty? + group = nil + conditions = {} + else + group = args.first - if @preference_values && @preference_values[attribute] && @preference_values[attribute].include?(group) - # Value for this attribute/group has been written, but not saved yet: - # grab from the pending values - value = @preference_values[attribute][group] + # Split the actual group into its different parts (id/type) in case + # a record is passed in + group_id, group_type = Preference.split_group(group) + conditions = {:group_id => group_id, :group_type => group_type} + end + + # Find all of the stored preferences + stored_preferences = self.stored_preferences.find(:all, :conditions => conditions) + + # Hashify attribute -> value or group -> attribute -> value + stored_preferences.inject(self.class.default_preferences.dup) do |all_preferences, preference| + if !group && (preference_group = preference.group) + preferences = all_preferences[preference_group] ||= self.class.default_preferences.dup else - # Split the group being filtered - group_id, group_type = Preference.split_group(group) - - # Grab the first preference; if it doesn't exist, use the default value - preference = stored_preferences.find(:first, :conditions => {:attribute => attribute, :group_id => group_id, :group_type => group_type}) - value = preference ? preference.value : preference_definitions[attribute].default_value + preferences = all_preferences end - value + preferences[preference.attribute] = preference.value + all_preferences end - alias_method :prefers, :preferred + end + + # Queries whether or not a value is present for the given attribute. This + # is dependent on how the value is type-casted. + # + # == Examples + # + # class User < ActiveRecord::Base + # preference :color, :string, :default => 'red' + # end + # + # user = User.create + # user.preferred(:color) # => "red" + # user.preferred?(:color) # => true + # user.preferred?(:color, 'cars') # => true + # user.preferred?(:color, Car.first) # => true + # + # user.set_preference(:color, nil) + # user.preferred(:color) # => nil + # user.preferred?(:color) # => false + def preferred?(attribute, group = nil) + attribute = attribute.to_s - # Sets a new value for the given attribute. The actual Preference record - # is *not* created until this record is saved. In this way, preferences - # act *exactly* the same as attributes. They can be written to and - # validated against, but won't actually be written to the database until - # the record is saved. - # - # == Examples - # - # user = User.find(:first) - # user.set_preference(:color, 'red') # => "red" - # user.save! - # - # user.set_preference(:color, 'blue', Car.first) # => "blue" - # user.save! - def set_preference(attribute, value, group = nil) - attribute = attribute.to_s - - @preference_values ||= {} - @preference_values[attribute] ||= {} - @preference_values[attribute][group] = value + value = preferred(attribute, group) + preference_definitions[attribute].query(value) + end + alias_method :prefers?, :preferred? + + # Gets the actual value stored for the given attribute, or the default + # value if nothing is present. + # + # == Examples + # + # class User < ActiveRecord::Base + # preference :color, :string, :default => 'red' + # end + # + # user = User.create + # user.preferred(:color) # => "red" + # user.preferred(:color, 'cars') # => "red" + # user.preferred(:color, Car.first) # => "red" + # + # user.set_preference(:color, 'blue') + # user.preferred(:color) # => "blue" + def preferred(attribute, group = nil) + attribute = attribute.to_s + + if @preference_values && @preference_values[attribute] && @preference_values[attribute].include?(group) + # Value for this attribute/group has been written, but not saved yet: + # grab from the pending values + value = @preference_values[attribute][group] + else + # Split the group being filtered + group_id, group_type = Preference.split_group(group) - value + # Grab the first preference; if it doesn't exist, use the default value + preference = stored_preferences.find(:first, :conditions => {:attribute => attribute, :group_id => group_id, :group_type => group_type}) + value = preference ? preference.value : preference_definitions[attribute].default_value end - private - # Updates any preferences that have been changed/added since the record - # was last saved - def update_preferences - if @preference_values - @preference_values.each do |attribute, grouped_records| - grouped_records.each do |group, value| - group_id, group_type = Preference.split_group(group) - attributes = {:attribute => attribute, :group_id => group_id, :group_type => group_type} - - # Find an existing preference or build a new one - preference = stored_preferences.find(:first, :conditions => attributes) || stored_preferences.build(attributes) - preference.value = value - preference.save! - end + value + end + alias_method :prefers, :preferred + + # Sets a new value for the given attribute. The actual Preference record + # is *not* created until this record is saved. In this way, preferences + # act *exactly* the same as attributes. They can be written to and + # validated against, but won't actually be written to the database until + # the record is saved. + # + # == Examples + # + # user = User.find(:first) + # user.set_preference(:color, 'red') # => "red" + # user.save! + # + # user.set_preference(:color, 'blue', Car.first) # => "blue" + # user.save! + def set_preference(attribute, value, group = nil) + attribute = attribute.to_s + + @preference_values ||= {} + @preference_values[attribute] ||= {} + @preference_values[attribute][group] = value + + value + end + + private + # Updates any preferences that have been changed/added since the record + # was last saved + def update_preferences + if @preference_values + @preference_values.each do |attribute, grouped_records| + grouped_records.each do |group, value| + group_id, group_type = Preference.split_group(group) + attributes = {:attribute => attribute, :group_id => group_id, :group_type => group_type} + + # Find an existing preference or build a new one + preference = stored_preferences.find(:first, :conditions => attributes) || stored_preferences.build(attributes) + preference.value = value + preference.save! end - - @preference_values = nil end + + @preference_values = nil end - end + end end end ActiveRecord::Base.class_eval do - extend PluginAWeek::Preferences::MacroMethods + extend Preferences::MacroMethods end diff --git a/lib/preferences/preference_definition.rb b/lib/preferences/preference_definition.rb index c2901f1..4ff4cf6 100644 --- a/lib/preferences/preference_definition.rb +++ b/lib/preferences/preference_definition.rb @@ -1,44 +1,42 @@ -module PluginAWeek #:nodoc: - module Preferences - # Represents the definition of a preference for a particular model - class PreferenceDefinition - def initialize(attribute, *args) #:nodoc: - options = args.extract_options! - options.assert_valid_keys(:default) - - @type = args.first ? args.first.to_s : 'boolean' - - # Create a column that will be responsible for typecasting - @column = ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, options[:default], @type == 'any' ? nil : @type) - end - - # The attribute which is being preferenced - def attribute - @column.name - end - - # The default value to use for the preference in case none have been - # previously defined - def default_value - @column.default - end +module Preferences + # Represents the definition of a preference for a particular model + class PreferenceDefinition + def initialize(attribute, *args) #:nodoc: + options = args.extract_options! + options.assert_valid_keys(:default) - # Typecasts the value based on the type of preference that was defined. - # This uses ActiveRecord's typecast functionality so the same rules for - # typecasting a model's columns apply here. - def type_cast(value) - @type == 'any' ? value : @column.type_cast(value) - end + @type = args.first ? args.first.to_s : 'boolean' - # Typecasts the value to true/false depending on the type of preference - def query(value) - if !(value = type_cast(value)) - false - elsif @column.number? - !value.zero? - else - !value.blank? - end + # Create a column that will be responsible for typecasting + @column = ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, options[:default], @type == 'any' ? nil : @type) + end + + # The attribute which is being preferenced + def attribute + @column.name + end + + # The default value to use for the preference in case none have been + # previously defined + def default_value + @column.default + end + + # Typecasts the value based on the type of preference that was defined. + # This uses ActiveRecord's typecast functionality so the same rules for + # typecasting a model's columns apply here. + def type_cast(value) + @type == 'any' ? value : @column.type_cast(value) + end + + # Typecasts the value to true/false depending on the type of preference + def query(value) + if !(value = type_cast(value)) + false + elsif @column.number? + !value.zero? + else + !value.blank? end end end diff --git a/test/unit/preference_definition_test.rb b/test/unit/preference_definition_test.rb index 9c6ad15..c77fd11 100644 --- a/test/unit/preference_definition_test.rb +++ b/test/unit/preference_definition_test.rb @@ -2,7 +2,7 @@ class PreferenceDefinitionByDefaultTest < Test::Unit::TestCase def setup - @definition = PluginAWeek::Preferences::PreferenceDefinition.new(:notifications) + @definition = Preferences::PreferenceDefinition.new(:notifications) end def test_should_have_an_attribute @@ -24,13 +24,13 @@ def test_should_type_cast_values_as_booleans class PreferenceDefinitionTest < Test::Unit::TestCase def test_should_raise_exception_if_invalid_option_specified - assert_raise(ArgumentError) {PluginAWeek::Preferences::PreferenceDefinition.new(:notifications, :invalid => true)} + assert_raise(ArgumentError) {Preferences::PreferenceDefinition.new(:notifications, :invalid => true)} end end class PreferenceDefinitionWithDefaultValueTest < Test::Unit::TestCase def setup - @definition = PluginAWeek::Preferences::PreferenceDefinition.new(:notifications, :boolean, :default => 1) + @definition = Preferences::PreferenceDefinition.new(:notifications, :boolean, :default => 1) end def test_should_type_cast_default_values @@ -40,7 +40,7 @@ def test_should_type_cast_default_values class PreferenceDefinitionWithAnyTypeTest < Test::Unit::TestCase def setup - @definition = PluginAWeek::Preferences::PreferenceDefinition.new(:notifications, :any) + @definition = Preferences::PreferenceDefinition.new(:notifications, :any) end def test_should_not_type_cast @@ -76,7 +76,7 @@ def test_should_query_true_if_value_is_not_blank class PreferenceDefinitionWithBooleanTypeTest < Test::Unit::TestCase def setup - @definition = PluginAWeek::Preferences::PreferenceDefinition.new(:notifications) + @definition = Preferences::PreferenceDefinition.new(:notifications) end def test_should_not_type_cast_if_value_is_nil @@ -122,7 +122,7 @@ def test_should_query_false_if_value_is_not_true_string class PreferenceDefinitionWithNumericTypeTest < Test::Unit::TestCase def setup - @definition = PluginAWeek::Preferences::PreferenceDefinition.new(:notifications, :integer) + @definition = Preferences::PreferenceDefinition.new(:notifications, :integer) end def test_should_type_cast_true_to_integer @@ -152,7 +152,7 @@ def test_should_query_false_if_value_is_zero class PreferenceDefinitionWithStringTypeTest < Test::Unit::TestCase def setup - @definition = PluginAWeek::Preferences::PreferenceDefinition.new(:notifications, :string) + @definition = Preferences::PreferenceDefinition.new(:notifications, :string) end def test_should_type_cast_integers_to_strings