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
# 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}?"
# 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
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
# 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
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
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)
# 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
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
end
@preference_values = nil
end
end
end
end
end
ActiveRecord::Base.class_eval do
extend PluginAWeek::Preferences::MacroMethods
end