module AttributeChoices
def self.included(base) #:nodoc:
base.extend(AttributeChoicesMacro)
end
module AttributeChoicesMacro
# Associate a list of display values for an attribute with a list of discreet values
#
# The arguments are:
#
# * +attribute+ - The attribute whose values you want to map to display values
# * +choices+ - Either an +Array+ of tupples where the first value of the tupple is the attribute \
# value and the second one is the display value mapping, or a +Hash+ where the key is the \
# attribute value and the value is the display value mapping.
# * options - An optional hash of options:
# * :localize - If set to +true+, then I18n.trasnlate is used to translate the value \
# returned by the +_display+ instance methods as well as translate the display \
# values returned by the +_choices+ class method
# * :validate - If set to +true+, +validates_inclusion_of+ is used to ensure that the attribute \
# only accepts the values passed in with the +choices+
#
# For example:
# class User < ActiveRecord::Base
# attribute_choices :gender, { 'm' => "Male", 'f' => 'Female'}
# attribute_choices :age_group, [
# ['18-24', '18 to 24 years old],
# ['25-45', '25 to 45 years old']
# ], :localize => true, :validate => false
# end
#
# The macro adds an instance method named after the attribute, suffixed with _display
# (e.g. gender_display for :gender) that returns the display value of the
# attribute for a given value, or nil if a mapping for a value is missing.
#
# It also adds a class method named after the attribute, suffixed with _choices
# (e.g. User.gender_choices for :gender) that returns an array of choices and values
# in a fomrat that is suitable for passing directly to the Rails select_* helpers.
#
# NOTE: You can use a Hash for the +choices+ argument which is converted to an Array. The order of the \
# tupples of the resulting Array is only guaranteed to be preserved if you are using Ruby 1.9
def attribute_choices(attribute, choices, *args)
assert_valid_attribute(attribute.to_s)
write_inheritable_hash :attribute_choices_storage, {}
class_inheritable_reader :attribute_choices_storage
write_inheritable_hash :attribute_choices_options, {}
class_inheritable_reader :attribute_choices_options
attribute = attribute.to_sym
options = args.extract_options!
options.reverse_merge!(:validate => false, :localize => false)
options.assert_valid_keys(:validate, :localize)
attribute_choices_options[attribute.to_sym] = options
if options[:localize]
attribute_choices_storage[attribute] = choices.to_a.collect {|t| [t.first, I18n.translate(t.last)]}
else
attribute_choices_storage[attribute] = choices.to_a
end
define_method("#{attribute.to_s}_display") do
tupple = attribute_choices_storage[attribute].assoc(read_attribute(attribute))
tupple && tupple.last
end
self.class.instance_eval do
define_method("#{attribute.to_s}_choices") do
attribute_choices_storage[attribute].collect(&:reverse)
end
end
if column_names.include?(attribute.to_s) && options[:validate]
validates_inclusion_of attribute.to_sym, :in => attribute_choices_storage[attribute].collect {|i| i.first}
end
end
private
def assert_valid_attribute(attr_name)
unless column_names.include?(attr_name.to_s)
raise ArgumentError, "Model attribute '#{attr_name.to_s}' doesn't exist"
end
end
end
end