module Confidentiality module ClassMethods # Specify methods that can have a confidentiality value. Each method should have a corresponding column # with a _confidential suffix. # # create_table :people do |t| # t.string :name, :phone_number # t.boolean :phone_number_confidential # end # # class Person < ActiveRecord::Base # confidential_methods :phone_number # end # # The default read/write methods of ActiveRecord are replaced with ones that # respect the confidentiality value of the field. Some new methods are provided: # # * phone_number_without_confidentiality - The original method that ignores any confidentiality value # * phone_number_without_confidentiality= - An alias of the original write method for use on forms # # The original reader method changes to respect the confidentiality value: # # p = Person.find(:first) # p.phone_number # 12345 # p.phone_number_confidential # false # p.phone_number_confidential = true # p.phone_number # (confidential) # # To ignore the confidentiality value and return the real value, wrap the code in a Confidentiality.ignore block. # # Confidentiality.ignore do # p.phone_number # 12345 # end # # You can also make methods confidential based on the confidentiality value of a different method. # For example, an address may be made up of a few different fields, but the confidentiality is not # set for every field, but for the address as a whole. # # The :using option can be passed to say "use the confidentiality from this other method". # # class Person < ActiveRecord::Base # def address # [address1, address2, city, country] # end # # confidential_methods :address, :address1, :address2, :city, :country, :using => :address # end # # p = Person.find(:first) # p.address # ['1 Place', 'Townsville', 'Christchurch', 'New Zealand'] # p.city # Christchurch # p.address_confidential # false # p.address_confidential = true # p.address # ['(confidential)'] # p.city # (confidential) # # If :using is a Symbol, a confidentiality method name with a suffix of _confidentiality will be expected. # If :using is a String, it will be used as the confidentiality method name. def confidential_methods(*args) options, methods = args.extract_options!, args options.assert_valid_keys :using # To provide confidentiality for model attributes, the generated readers must be in place define_attribute_methods unless generated_methods? # Store a list of the methods that have confidentiality @confidential_methods ||= [] methods.map(&:to_s).each do |method| confidentiality_method = determine_confidentiality_method(options, method) @confidential_methods << method unless instance_methods.include?(confidentiality_method) raise "#{name} does not have instance method '#{confidentiality_method}'" end if instance_methods.include?("#{method}_with_confidentiality") raise DuplicateError, "confidentiality already defined for '#{method}'" end define_method "#{method}_with_confidentiality" do |*args| read_method_with_confidentiality(method, confidentiality_method, *args) end # Define _confidential and _confidential? methods when the :using option is present and they do not exist if method != confidentiality_method unless generated_methods.include?("#{method}_confidential") alias_method "#{method}_confidential", confidentiality_method end unless generated_methods.include?("#{method}_confidential?") alias_method "#{method}_confidential?", confidentiality_method end end alias_method_chain method, :confidentiality end end def reset_column_information_with_confidential_methods reset_column_information_without_confidential_methods define_attribute_methods # Re-alias the _with_confidentiality methods dealing with attributes if @confidential_methods @confidential_methods.each do |method| if column_names.include?(method) alias_method_chain method, :confidentiality end end end nil end private def determine_confidentiality_method(options, default) if options[:using].is_a?(String) options[:using] else "#{options[:using] || default}_confidential" end end end end