require 'rubygems' require 'fastercsv' require 'open-uri' require 'active_support' class CsvDictionary attr_accessor :url, :key_sprintf, :key_name, :sheet # set in options attr_accessor :dictionary, :value_name def replace_attributes!(attributes) load_dictionary unless dictionary_loaded? attributes[value_name] = lookup(attributes[key_name]) attributes.delete(key_name) end def lookup(key) load_dictionary unless dictionary_loaded? dictionary[interpret_key(key)] rescue nil end private def initialize(key_name, options = {}) @key_name = key_name @sheet = options[:sheet] ? options[:sheet] - 1 : nil @key_sprintf = options[:key_sprintf] || '%s' @url = options[:url] validate end def validate raise 'Please specify a key name.' if key_name.blank? raise 'Please specify a url.' if url.blank? raise ':sheet option is only supported for Google Docs Spreadsheet URLs.' if not sheet.nil? and not google_docs_spreadsheet_url?(url) if google_docs_spreadsheet_url?(url) and /\&gid=(.*)(\&|$)/.match(url) raise "Your Google Docs Spreadsheet URL includes a sheet number (&gid=#{$1}). Please remove it and set the sheet with :sheet => #{$1.to_i + 1}" end end def google_docs_spreadsheet_url?(url) url.include?('spreadsheets.google.com') end def dictionary_loaded? !dictionary.nil? end def load_dictionary self.dictionary = {} open(interpret_url(url)) do |data| FasterCSV.parse(data, :headers => :first_row) do |row| self.value_name ||= row.headers[1].to_sym next if row.fields[0..1].any? { |c| c.blank? } key = interpret_key(row.fields[0]) value = row.fields[1].to_s.strip dictionary[key] = value end end end def interpret_url(url) if google_docs_spreadsheet_url?(url) url = url.gsub(/\&output=.*(\&|$)/, '') url << "&output=csv&gid=#{sheet}" end url end def interpret_key(k) k = k.to_s.strip k = k.to_i if /\%[0-9\.]*d/.match(key_sprintf) key_sprintf % k end end module ActiveRecord module CsvDictionaryExt def self.included(klass) klass.class_eval do self.class_inheritable_accessor :csv_dictionaries extend ClassMethods end end module ClassMethods def csv_dictionary(key_name, options = {}) class_eval { @old_method_missing = instance_method(:method_missing) } self.csv_dictionaries ||= {} csv_dictionaries[key_name] = CsvDictionary.new(key_name, options) extend InstanceMethods end def clear_csv_dictionaries self.csv_dictionaries = {} end def replace_csv_dictionary_attributes!(attributes) csv_dictionaries.values.each { |d| d.replace_attributes!(attributes) } end end module InstanceMethods def all_attributes_or_csv_dictionary_attributes_exists?(attribute_names) attribute_names = expand_attribute_names_for_aggregates(attribute_names) attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) or csv_dictionaries.has_key?(name.to_sym) } end # like Rails 2.3.2 # copied from http://github.com/rails/rails/raw/dd2eb1ea7c34eb6496feaf7e42100f37a8dae76b/activerecord/lib/active_record/base.rb def method_missing_with_csv_dictionary_attributes(method_id, *arguments, &block) if match = DynamicFinderMatch.match(method_id) attribute_names = match.attribute_names return method_missing_without_csv_dictionary_attributes(method_id, arguments, block) unless all_attributes_or_csv_dictionary_attributes_exists?(attribute_names) if match.finder? finder = match.finder bang = match.bang? # def self.find_by_login_and_activated(*args) # options = args.extract_options! # attributes = construct_attributes_from_arguments( # [:login,:activated], # args # ) # # replace_csv_dictionary_attributes!(attributes) # ADDED BY SEAMUS # # finder_options = { :conditions => attributes } # validate_find_options(options) # set_readonly_option!(options) # # if options[:conditions] # with_scope(:find => finder_options) do # find(:first, options) # end # else # find(:first, options.merge(finder_options)) # end # end self.class_eval %{ def self.#{method_id}(*args) options = args.extract_options! attributes = construct_attributes_from_arguments( [:#{attribute_names.join(',:')}], args ) replace_csv_dictionary_attributes!(attributes) finder_options = { :conditions => attributes } validate_find_options(options) set_readonly_option!(options) #{'result = ' if bang}if options[:conditions] with_scope(:find => finder_options) do find(:#{finder}, options) end else find(:#{finder}, options.merge(finder_options)) end #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang} end }, __FILE__, __LINE__ send(method_id, *arguments) elsif match.instantiator? instantiator = match.instantiator # def self.find_or_create_by_user_id(*args) # guard_protected_attributes = false # # if args[0].is_a?(Hash) # guard_protected_attributes = true # attributes = args[0].with_indifferent_access # find_attributes = attributes.slice(*[:user_id]) # else # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args) # end # # replace_csv_dictionary_attributes!(attributes) # ADDED BY SEAMUS # replace_csv_dictionary_attributes!(find_attributes) # ADDED BY SEAMUS # # options = { :conditions => find_attributes } # set_readonly_option!(options) # # record = find(:first, options) # # if record.nil? # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } # yield(record) if block_given? # record.save # record # else # record # end # end self.class_eval %{ def self.#{method_id}(*args) guard_protected_attributes = false if args[0].is_a?(Hash) guard_protected_attributes = true attributes = args[0].with_indifferent_access find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}]) replace_csv_dictionary_attributes!(attributes) replace_csv_dictionary_attributes!(find_attributes) else find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) replace_csv_dictionary_attributes!(attributes) # only have to do this once, since find_attributes.object_id == attributes.object_id end options = { :conditions => find_attributes } set_readonly_option!(options) record = find(:first, options) if record.nil? record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } #{'yield(record) if block_given?'} #{'record.save' if instantiator == :create} record else record end end }, __FILE__, __LINE__ send(method_id, *arguments, &block) end else method_missing_without_csv_dictionary_attributes(method_id, arguments, block) end end alias_method_chain :method_missing, :csv_dictionary_attributes end end end ActiveRecord::Base.send :include, ActiveRecord::CsvDictionaryExt