Skip to content

Commit

Permalink
start support import attributes for associations
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas Statter committed Jan 14, 2013
1 parent 489c8f3 commit dd8b8b8
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 51 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Original file line Diff line number Diff line change
@@ -1 +1 @@
0.12.1 0.13.0
1 change: 0 additions & 1 deletion lib/datashift/delimiters.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ module Delimiters
# Support multiple associations being added to a base object to be specified in a single column. # Support multiple associations being added to a base object to be specified in a single column.
# #
# Entry represents the association to find via supplied name, value to use in the lookup. # Entry represents the association to find via supplied name, value to use in the lookup.
# Can contain multiple lookup name/value pairs, separated by multi_assoc_delim ( | )
# #
# Default syntax : # Default syntax :
# #
Expand Down
24 changes: 18 additions & 6 deletions lib/datashift/method_details_manager.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ def initialize( klass )
end end


def add(method_details) def add(method_details)
#puts "DEBUG: MGR - Add {#method_details.operator_type}\n#{method_details.inspect}"
@method_details[method_details.operator_type.to_sym] ||= {} @method_details[method_details.operator_type.to_sym] ||= {}

# Mapped by Type and MethodDetail name
@method_details[method_details.operator_type.to_sym][method_details.name] = method_details

# Helper list of all available by type
@method_details_list[method_details.operator_type.to_sym] ||= [] @method_details_list[method_details.operator_type.to_sym] ||= []


@method_details[method_details.operator_type.to_sym][method_details.name] = method_details
@method_details_list[method_details.operator_type.to_sym] << method_details @method_details_list[method_details.operator_type.to_sym] << method_details
@method_details_list[method_details.operator_type.to_sym].uniq! @method_details_list[method_details.operator_type.to_sym].uniq!
end end
Expand All @@ -38,10 +43,11 @@ def <<(method_details)
def find(name, type) def find(name, type)
method_details = get(type) method_details = get(type)


method_details ? method_details[name] : nil method_details ? method_details[name] : nil
end end


# type is expected to be one of MethodDetail::supportedtype_enum # type is expected to be one of MethodDetail::supported_types_enum
# Returns all MethodDetail(s) for supplied type
def get( type ) def get( type )
@method_details[type.to_sym] @method_details[type.to_sym]
end end
Expand All @@ -51,9 +57,15 @@ def get_list( type )
end end


alias_method(:get_list_of_method_details, :get_list) alias_method(:get_list_of_method_details, :get_list)


def get_operators( op_type ) # Get list of the inbound or externally supplied names
get_list(op_type).collect { |md| md.operator } def get_names(type)
get_list(type).collect { |md| md.name }
end

# Get list of Rails model operators
def get_operators(type)
get_list(type).collect { |md| md.operator }
end end


alias_method(:get_list_of_operators, :get_list) alias_method(:get_list_of_operators, :get_list)
Expand Down
73 changes: 46 additions & 27 deletions lib/datashift/method_dictionary.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -34,50 +34,51 @@ def self.find_operators(klass, options = {} )
has_many[klass] = klass.reflect_on_all_associations(:has_many).map { |i| i.name.to_s } has_many[klass] = klass.reflect_on_all_associations(:has_many).map { |i| i.name.to_s }
klass.reflect_on_all_associations(:has_and_belongs_to_many).inject(has_many[klass]) { |x,i| x << i.name.to_s } klass.reflect_on_all_associations(:has_and_belongs_to_many).inject(has_many[klass]) { |x,i| x << i.name.to_s }
end end

# puts "DEBUG: Has Many Associations:", has_many[klass].inspect


# Find the belongs_to associations which can be populated via Model.belongs_to_name = OtherArModelObject # Find the belongs_to associations which can be populated via Model.belongs_to_name = OtherArModelObject
if( options[:reload] || belongs_to[klass].nil? ) if( options[:reload] || belongs_to[klass].nil? )
belongs_to[klass] = klass.reflect_on_all_associations(:belongs_to).map { |i| i.name.to_s } belongs_to[klass] = klass.reflect_on_all_associations(:belongs_to).map { |i| i.name.to_s }
end end


#puts "Belongs To Associations:", belongs_to[klass].inspect

# Find the has_one associations which can be populated via Model.has_one_name = OtherArModelObject # Find the has_one associations which can be populated via Model.has_one_name = OtherArModelObject
if( options[:reload] || has_one[klass].nil? ) if( options[:reload] || has_one[klass].nil? )
has_one[klass] = klass.reflect_on_all_associations(:has_one).map { |i| i.name.to_s } has_one[klass] = klass.reflect_on_all_associations(:has_one).map { |i| i.name.to_s }
end end


#puts "has_one Associations:", self.has_one[klass].inspect

# Find the model's column associations which can be populated via xxxxxx= value # Find the model's column associations which can be populated via xxxxxx= value
# Note, not all reflections return method names in same style so we convert all to # Note, not all reflections return method names in same style so we convert all to
# the raw form i.e without the '=' for consistency # the raw form i.e without the '=' for consistency
if( options[:reload] || assignments[klass].nil? ) if( options[:reload] || assignments[klass].nil? )


# TODO investigate difference with attribute_names - maybe column names can be assigned to an attribute
# so in terms of method calls on klass attribute_names might be safer
assignments[klass] = klass.column_names assignments[klass] = klass.column_names


if(options[:instance_methods] == true) if(options[:instance_methods])
setters = klass.instance_methods.grep(/\w+=/).collect {|x| x.to_s }

#puts "\nDEBUG: #{klass}.methods\n#{klass.methods.sort.collect.inspect}\n"
# TODO - Since 3.2 this seems to return lots more stuff including validations which might not be appropriate setters = klass.accessible_attributes.sort
if(klass.respond_to? :defined_activerecord_methods)
setters = setters - klass.defined_activerecord_methods.to_a # Since 3.2 :instance_methods returns lots more stuff like validations,
# which since not appropriate we remove with defined_activerecord_methods
if(klass.respond_to? :defined_activerecord_methods)
setters += klass.instance_methods.grep(/\w+=/).sort - klass.defined_activerecord_methods
end end

# get into same format as other names # get into same format as other names
assignments[klass] += setters.map{|i| i.gsub(/=/, '')} assignments[klass] += setters.map{|i| i.gsub(/=/, '')}
end end


assignments[klass] -= has_many[klass] if(has_many[klass]) assignments[klass] -= has_many[klass] if(has_many[klass])

# TODO remove assignments with id
# assignments => tax_id but already in belongs_to => tax
assignments[klass] -= belongs_to[klass] if(belongs_to[klass]) assignments[klass] -= belongs_to[klass] if(belongs_to[klass])

assignments[klass] -= self.has_one[klass] if(self.has_one[klass]) assignments[klass] -= self.has_one[klass] if(self.has_one[klass])


assignments[klass].uniq! assignments[klass].uniq!


#puts "\nDEBUG: DICT Setters\n#{assignments[klass]}\n"

assignments[klass].each do |assign| assignments[klass].each do |assign|
column_types[klass] ||= {} column_types[klass] ||= {}
column_def = klass.columns.find{ |col| col.name == assign } column_def = klass.columns.find{ |col| col.name == assign }
Expand Down Expand Up @@ -119,14 +120,14 @@ def self.build_method_details( klass )
method_details_mgrs[klass] = method_details_mgr method_details_mgrs[klass] = method_details_mgr


end end

# TODO - check out regexp to do this work better plus Inflections ?? # TODO - check out regexp to do this work better plus Inflections ??
# Want to be able to handle any of ["Count On hand", 'count_on_hand', "Count OnHand", "COUNT ONHand" etc] # Want to be able to handle any of ["Count On hand", 'count_on_hand', "Count OnHand", "COUNT ONHand" etc]
def self.substitutions(external_name) def self.substitutions(external_name)
name = external_name.to_s name = external_name.to_s


[ [
name, name.downcase,
name.tableize, name.tableize,
name.gsub(' ', '_'), name.gsub(' ', '_'),
name.gsub(' ', '_').downcase, name.gsub(' ', '_').downcase,
Expand All @@ -137,18 +138,24 @@ def self.substitutions(external_name)
] ]
end end



# Find the proper format of name, appropriate call + column type for a given name. # Find the proper format of name, appropriate call + column type for a given name.
# e.g Given users entry in spread sheet check for pluralization, missing underscores etc # e.g Given users entry in spread sheet check for pluralization, missing underscores etc
# #
# If not nil, returned method can be used directly in for example klass.new.send( call, .... ) # If not nil, returned method can be used directly in for example klass.new.send( call, .... )
# #
def self.find_method_detail( klass, external_name ) def self.find_method_detail( klass, external_name, conditions = nil )


method_details_mgr = get_method_details_mgr( klass ) method_details_mgr = get_method_details_mgr( klass )


# md_mgr.all_available_operators.each { |l| puts "DEBUG: Mapped Method : #{l.inspect}" } # first try for an exact match across all association types
substitutions(external_name).each do |n| MethodDetail::supported_types_enum.each do |t|

method_detail = method_details_mgr.find(external_name, t)
return method_detail.clone if(method_detail)
end

# Now try various alternatives of the name
substitutions(external_name).each do |n|
# Try each association type, returning first that contains matching operator with name n # Try each association type, returning first that contains matching operator with name n
MethodDetail::supported_types_enum.each do |t| MethodDetail::supported_types_enum.each do |t|
method_detail = method_details_mgr.find(n, t) method_detail = method_details_mgr.find(n, t)
Expand All @@ -165,14 +172,25 @@ def self.find_method_detail_if_column( klass, external_name )


method_details_mgr = get_method_details_mgr( klass ) method_details_mgr = get_method_details_mgr( klass )


substitutions(external_name).each do |n| # first try for an exact match across all association types
method_detail = method_details_mgr.find(n, :assignment) MethodDetail::supported_types_enum.each do |t|
return method_detail if(method_detail && method_detail.col_type) method_detail = method_details_mgr.find(external_name, t)
return method_detail.clone if(method_detail && method_detail.col_type)
end

# Now try various alternatives
substitutions(external_name).each do |n|
# Try each association type, returning first that contains matching operator with name n
MethodDetail::supported_types_enum.each do |t|
method_detail = method_details_mgr.find(n, t)
return method_detail.clone if(method_detail && method_detail.col_type)
end
end end

nil nil
end end



def self.clear def self.clear
belongs_to.clear belongs_to.clear
has_many.clear has_many.clear
Expand All @@ -190,7 +208,8 @@ def self.get_method_details_mgr( klass )
method_details_mgrs[klass] || MethodDetailsManager.new( klass ) method_details_mgrs[klass] || MethodDetailsManager.new( klass )
end end




# Store a Mgr per mapped klass
def self.method_details_mgrs def self.method_details_mgrs
@method_details_mgrs ||= {} @method_details_mgrs ||= {}
@method_details_mgrs @method_details_mgrs
Expand Down
29 changes: 18 additions & 11 deletions lib/datashift/method_mapper.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def map_inbound_headers_to_methods( klass, columns, options = {} )
DataShift::MethodDictionary.build_method_details(klass) DataShift::MethodDictionary.build_method_details(klass)
end end


mgr = DataShift::MethodDictionary.method_details_mgrs[klass]

forced = [*options[:force_inclusion]].compact.collect { |f| f.to_s.downcase } forced = [*options[:force_inclusion]].compact.collect { |f| f.to_s.downcase }


@method_details, @missing_methods = [], [] @method_details, @missing_methods = [], []
Expand All @@ -99,26 +101,29 @@ def map_inbound_headers_to_methods( klass, columns, options = {} )
end end


raw_col_name, lookup = raw_col_data.split(MethodMapper::column_delim) raw_col_name, lookup = raw_col_data.split(MethodMapper::column_delim)


md = MethodDictionary::find_method_detail( klass, raw_col_name ) md = MethodDictionary::find_method_detail(klass, raw_col_name)


# TODO be nice if we could check that the assoc on klass responds to the specified if(md.nil?)
# lookup key now (nice n early) #puts "DEBUG: Check Forced\n #{forced}.include?(#{raw_col_name}) #{forced.include?(raw_col_name.downcase)}"
# active_record_helper = "find_by_#{lookup}"
if(md.nil? && (options[:include_all] || forced.include?(raw_col_name.downcase)) ) if(options[:include_all] || forced.include?(raw_col_name.downcase))
md = MethodDictionary::add(klass, raw_col_name) md = MethodDictionary::add(klass, raw_col_name)
end
end end


if(md) if(md)

md.name = raw_col_name md.name = raw_col_name
md.column_index = col_index md.column_index = col_index


# TODO we should check that the assoc on klass responds to the specified
# lookup key now (nice n early)
# active_record_helper = "find_by_#{lookup}"
if(lookup) if(lookup)
find_by, find_value = lookup.split(MethodMapper::column_delim) find_by, find_value = lookup.split(MethodMapper::column_delim)
md.find_by_value = find_value md.find_by_value = find_value
md.find_by_operator = find_by # TODO and klass.x.respond_to?(active_record_helper)) md.find_by_operator = find_by # TODO and klass.x.respond_to?(active_record_helper))
#puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}" puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}"
end end
else else
# TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil # TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil
Expand Down Expand Up @@ -149,7 +154,9 @@ def operator_names()
# Returns true if discovered methods contain every operator in mandatory_list # Returns true if discovered methods contain every operator in mandatory_list
def contains_mandatory?( mandatory_list ) def contains_mandatory?( mandatory_list )
a = [*mandatory_list].collect { |f| f.downcase } a = [*mandatory_list].collect { |f| f.downcase }
puts a.inspect
b = operator_names.collect { |f| f.downcase } b = operator_names.collect { |f| f.downcase }
puts b.inspect
(a - b).empty? (a - b).empty?
end end


Expand Down
23 changes: 18 additions & 5 deletions lib/loaders/loader_base.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def initialize(object_class, find_operators = true, object = nil, options = {})
# Gather names of all possible 'setter' methods on AR class (instance variables and associations) # Gather names of all possible 'setter' methods on AR class (instance variables and associations)
if((find_operators && !MethodDictionary::for?(object_class)) || options[:reload]) if((find_operators && !MethodDictionary::for?(object_class)) || options[:reload])
#puts "DEBUG Building Method Dictionary for class #{object_class}" #puts "DEBUG Building Method Dictionary for class #{object_class}"
DataShift::MethodDictionary.find_operators( @load_object_class, :reload => options[:reload], :instance_methods => options[:instance_methods] )
meth_dict_opts = options.extract!(:reload, :instance_methods)
DataShift::MethodDictionary.find_operators( @load_object_class, meth_dict_opts)


# Create dictionary of data on all possible 'setter' methods which can be used to # Create dictionary of data on all possible 'setter' methods which can be used to
# populate or integrate an object of type @load_object_class # populate or integrate an object of type @load_object_class
Expand Down Expand Up @@ -136,9 +138,13 @@ def report
# #
# [:force_inclusion] : List of columns that do not map to any operator but should be includeed in processing. # [:force_inclusion] : List of columns that do not map to any operator but should be includeed in processing.
# #
# This provides the opportunity for loaders to provide specific methods to handle these fields # This provides the opportunity for :
# when no direct operator is available on the modle or it's associations #
# 1) loaders to provide specific methods to handle these fields, when no direct operator
# is available on the model or it's associations
# #
# 2) Handle delegated methods i.e no direct association but method is on a model throuygh it's delegate
#
# [:include_all] : Include all headers in processing - takes precedence of :force_inclusion # [:include_all] : Include all headers in processing - takes precedence of :force_inclusion
# #
def populate_method_mapper_from_headers( headers, options = {} ) def populate_method_mapper_from_headers( headers, options = {} )
Expand Down Expand Up @@ -278,7 +284,14 @@ def configure_from( yaml_file )
# prepend or append with any provided extensions # prepend or append with any provided extensions
def prepare_data(method_detail, value) def prepare_data(method_detail, value)


@current_value = value @current_value, @current_attribute_hash = value.to_s.split(Delimiters::attribute_list_start)

if(@current_attribute_hash)
# @current_attribute_hash
@current_attribute_hash = nil unless @current_attribute_hash.include?('}')
end

@current_attribute_hash ||= {}


@current_method_detail = method_detail @current_method_detail = method_detail


Expand All @@ -293,7 +306,7 @@ def prepare_data(method_detail, value)
@current_value = "#{prefixes(operator)}#{@current_value}" if(prefixes(operator)) @current_value = "#{prefixes(operator)}#{@current_value}" if(prefixes(operator))
@current_value = "#{@current_value}#{postfixes(operator)}" if(postfixes(operator)) @current_value = "#{@current_value}#{postfixes(operator)}" if(postfixes(operator))


@current_value return @current_value, @current_attribute_hash
end end


# Return the find_by operator and the rest of the (row,columns) data # Return the find_by operator and the rest of the (row,columns) data
Expand Down
Binary file modified spec/fixtures/db/datashift_test_models_db.sqlite
Binary file not shown.

0 comments on commit dd8b8b8

Please sign in to comment.