datashift 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/.document +5 -0
  2. data/Gemfile +25 -0
  3. data/Gemfile.lock +211 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.markdown +286 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +96 -0
  8. data/VERSION +5 -0
  9. data/bin/autospec +16 -0
  10. data/bin/convert_to_should_syntax +16 -0
  11. data/bin/erubis +16 -0
  12. data/bin/htmldiff +16 -0
  13. data/bin/jeweler +16 -0
  14. data/bin/ldiff +16 -0
  15. data/bin/nokogiri +16 -0
  16. data/bin/rackup +16 -0
  17. data/bin/rails +16 -0
  18. data/bin/rake +16 -0
  19. data/bin/rake2thor +16 -0
  20. data/bin/ri +16 -0
  21. data/bin/rspec +16 -0
  22. data/bin/spree +16 -0
  23. data/bin/thor +16 -0
  24. data/bin/tilt +16 -0
  25. data/bin/tt +16 -0
  26. data/datashift.gemspec +178 -0
  27. data/lib/applications/jruby/jexcel_file.rb +397 -0
  28. data/lib/applications/jruby/word.rb +79 -0
  29. data/lib/datashift.rb +114 -0
  30. data/lib/datashift/exceptions.rb +12 -0
  31. data/lib/datashift/file_definitions.rb +353 -0
  32. data/lib/datashift/mapping_file_definitions.rb +88 -0
  33. data/lib/datashift/method_detail.rb +237 -0
  34. data/lib/datashift/method_mapper.rb +257 -0
  35. data/lib/generators/csv_generator.rb +36 -0
  36. data/lib/generators/excel_generator.rb +122 -0
  37. data/lib/generators/generator_base.rb +14 -0
  38. data/lib/helpers/core_ext/to_b.rb +24 -0
  39. data/lib/helpers/spree_helper.rb +131 -0
  40. data/lib/java/poi-3.7/._poi-3.7-20101029.jar5645100390082102460.tmp +0 -0
  41. data/lib/java/poi-3.7/LICENSE +507 -0
  42. data/lib/java/poi-3.7/NOTICE +21 -0
  43. data/lib/java/poi-3.7/RELEASE_NOTES.txt +115 -0
  44. data/lib/java/poi-3.7/lib/commons-logging-1.1.jar +0 -0
  45. data/lib/java/poi-3.7/lib/junit-3.8.1.jar +0 -0
  46. data/lib/java/poi-3.7/lib/log4j-1.2.13.jar +0 -0
  47. data/lib/java/poi-3.7/ooxml-lib/dom4j-1.6.1.jar +0 -0
  48. data/lib/java/poi-3.7/ooxml-lib/geronimo-stax-api_1.0_spec-1.0.jar +0 -0
  49. data/lib/java/poi-3.7/ooxml-lib/xmlbeans-2.3.0.jar +0 -0
  50. data/lib/java/poi-3.7/poi-3.7-20101029.jar +0 -0
  51. data/lib/java/poi-3.7/poi-examples-3.7-20101029.jar +0 -0
  52. data/lib/java/poi-3.7/poi-ooxml-3.7-20101029.jar +0 -0
  53. data/lib/java/poi-3.7/poi-ooxml-schemas-3.7-20101029.jar +0 -0
  54. data/lib/java/poi-3.7/poi-scratchpad-3.7-20101029.jar +0 -0
  55. data/lib/loaders/csv_loader.rb +99 -0
  56. data/lib/loaders/excel_loader.rb +150 -0
  57. data/lib/loaders/loader_base.rb +332 -0
  58. data/lib/loaders/spreadsheet_loader.rb +137 -0
  59. data/lib/loaders/spree/image_loader.rb +46 -0
  60. data/lib/loaders/spree/product_loader.rb +225 -0
  61. data/spec/csv_loader_spec.rb +31 -0
  62. data/spec/datashift_spec.rb +27 -0
  63. data/spec/db/migrate/20110803201325_create_test_bed.rb +85 -0
  64. data/spec/excel_generator_spec.rb +79 -0
  65. data/spec/excel_loader_spec.rb +177 -0
  66. data/spec/file_definitions.rb +141 -0
  67. data/spec/fixtures/BadAssociationName.xls +0 -0
  68. data/spec/fixtures/DemoNegativeTesting.xls +0 -0
  69. data/spec/fixtures/ProjectsMultiCategories.xls +0 -0
  70. data/spec/fixtures/ProjectsSingleCategories.xls +0 -0
  71. data/spec/fixtures/SimpleProjects.xls +0 -0
  72. data/spec/fixtures/config/database.yml +25 -0
  73. data/spec/fixtures/interact_models_db.sqlite +0 -0
  74. data/spec/fixtures/interact_spree_db.sqlite +0 -0
  75. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.csv +4 -0
  76. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.xls +0 -0
  77. data/spec/fixtures/negative/SpreeProdMissManyMandatory.csv +4 -0
  78. data/spec/fixtures/negative/SpreeProdMissManyMandatory.xls +0 -0
  79. data/spec/fixtures/simple_export_spec.xls +0 -0
  80. data/spec/fixtures/simple_template_spec.xls +0 -0
  81. data/spec/fixtures/spree/SpreeProducts.csv +4 -0
  82. data/spec/fixtures/spree/SpreeProducts.xls +0 -0
  83. data/spec/fixtures/spree/SpreeProductsMultiColumn.csv +4 -0
  84. data/spec/fixtures/spree/SpreeProductsMultiColumn.xls +0 -0
  85. data/spec/fixtures/spree/SpreeProductsSimple.csv +4 -0
  86. data/spec/fixtures/spree/SpreeProductsSimple.xls +0 -0
  87. data/spec/fixtures/spree/SpreeZoneExample.csv +5 -0
  88. data/spec/fixtures/spree/SpreeZoneExample.xls +0 -0
  89. data/spec/fixtures/test_model_defs.rb +57 -0
  90. data/spec/loader_spec.rb +121 -0
  91. data/spec/method_mapper_spec.rb +238 -0
  92. data/spec/spec_helper.rb +116 -0
  93. data/spec/spree_generator_spec.rb +65 -0
  94. data/spec/spree_loader_spec.rb +311 -0
  95. data/spec/spree_method_mapping_spec.rb +215 -0
  96. data/tasks/config/seed_fu_product_template.erb +15 -0
  97. data/tasks/config/tidy_config.txt +13 -0
  98. data/tasks/db_tasks.rake +65 -0
  99. data/tasks/excel_generator.rake +79 -0
  100. data/tasks/file_tasks.rake +37 -0
  101. data/tasks/import/csv.rake +50 -0
  102. data/tasks/import/excel.rake +67 -0
  103. data/tasks/spree/image_load.rake +109 -0
  104. data/tasks/spree/product_loader.rake +44 -0
  105. data/tasks/word_to_seedfu.rake +167 -0
  106. data/test/helper.rb +18 -0
  107. data/test/test_interact.rb +7 -0
  108. metadata +301 -0
@@ -0,0 +1,21 @@
1
+ Apache POI
2
+ Copyright 2009 The Apache Software Foundation
3
+
4
+ This product includes software developed by
5
+ The Apache Software Foundation (http://www.apache.org/).
6
+
7
+ This product contains the DOM4J library (http://www.dom4j.org).
8
+ Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
9
+
10
+ This product contains parts that were originally based on software from BEA.
11
+ Copyright (c) 2000-2003, BEA Systems, <http://www.bea.com/>.
12
+
13
+ This product contains W3C XML Schema documents. Copyright 2001-2003 (c)
14
+ World Wide Web Consortium (Massachusetts Institute of Technology, European
15
+ Research Consortium for Informatics and Mathematics, Keio University)
16
+
17
+ This product contains the Piccolo XML Parser for Java
18
+ (http://piccolo.sourceforge.net/). Copyright 2002 Yuval Oren.
19
+
20
+ This product contains the chunks_parse_cmds.tbl file from the vsdump program.
21
+ Copyright (C) 2006-2007 Valek Filippov (frob@df.ru)
@@ -0,0 +1,115 @@
1
+ The Apache POI is pleased to announce the release of POI 3.7.
2
+
3
+ See the downloads page for binary and source distributions: http://poi.apache.org/download.html
4
+
5
+
6
+ Release Notes -- Apache POI -- Version 3.7
7
+
8
+ Apache POI is an open source Java library for working with Microsoft Office documents.
9
+ POI 3.7 is an incremental feature release based on the earlier 3.6 release.
10
+
11
+ The most notable changes since POI 3.6 are:
12
+ -------------------------------------------
13
+ OOXML
14
+ * support for reading aes-encrypted/write-protected ooxml files
15
+ * support Java 1.5 in auto-generated xmlbeans for ooxml schemas
16
+
17
+ Spreadsheet (Excel)
18
+ * initial support for autofilters
19
+ * support for data validation for ooxml format
20
+ * initial support for themes for ooxml format
21
+ * added implementation for new functions: RANDBETWEEN, POISSON, SUBTOTAL, TEXT, TRUNC
22
+ * support evaluation of indirect defined names in INDIRECT
23
+ * numerous fixes and performance optimizations in the Formula Evaluation module
24
+ * ability to add, modify and remove series from HSSF Charts
25
+ * numerous improvements in the cell data formatter (handling more formatting rules, better color detection, allow overriding of default locale)
26
+ * more examples including a rich "spreadsheet to HTML" converter
27
+
28
+ Document (Word)
29
+ * initial support for the HWPF revision marks authors list
30
+ * support for border codes in HWPF
31
+ * support for processing of symbols in HWPF
32
+ * support sections in Word 6 and Word 95 files
33
+ * improved reading of auto-saved ("complex") documents in HWPF
34
+ * improved support for manipulation of tables and paragraphs in XWPF
35
+
36
+ SlideShow (PowerPoint)
37
+ * allow editing workbooks embedded into HSLF slide shows
38
+
39
+ Text Extraction
40
+ * support for text extraction from XSLF tables
41
+ * add PublisherTextExtractor support to extractorfactory
42
+ * support attachments as embedded documents within the new OutlookTextExtactor
43
+ * new event based XSSF text extractor (XSSFEventBasedExcelExtractor)
44
+ * make it easier to tell which content types each POIXMLTextExtractor handles
45
+ * paragraph level as well as whole-file text extraction for word 6/95 files
46
+
47
+
48
+ * ...and much much more: code cleanup, many bug fixes and performance improvements
49
+
50
+ Contributors
51
+ ------------
52
+
53
+ The following people have contributed to this release by submitting bug
54
+ reports or by participating in the issue resolution process (in strict alphabetical order).
55
+
56
+ Alexey Butchik Jeff Lavezzo Philippe Laflamme
57
+ Andrew Shirley Jens Gatze Raiko Eckstein
58
+ Andrzej Bialecki Jerry Soung Rainer Schwarze
59
+ Antoni Mylka Jonathan Holloway Ranvijay Singh
60
+ Antony Bowesman Josh Micich Rick Cameron
61
+ Antti Koskimaki Jukka Zitting Robert Kish
62
+ Attila Kiraily Kai Zimmermann Robin Salkeld
63
+ Bob Smith Kalpesh Parmar Ryan Lauck
64
+ Brendan Nolan Kamil Soltys Ryan Skow
65
+ Charlie Chang Karl Eilebrecht Samuel Yung
66
+ Chris Barlock Ken Arnold Simon Kelly
67
+ Chris Lott Liu Yan Stefan Stern
68
+ Christiaan Fluit Lon Binder Steve Wolke
69
+ Dave Fisher Martin Studer TK Gospodinov
70
+ Dave Syer Martin W. Kirst Tao Jiang
71
+ David Agnew Maxim Valyanskiy Ted Schrader
72
+ David Lewis Michael Vilensky Thomas Herre
73
+ Dmitry Sviridov Michel Boudinot Tomas Prochazka
74
+ Domenico Napoletano Nick Burch Tony Harvey
75
+ Ed Beaty Paul Spencer Trejkaz (pen name)
76
+ Fabio Ebner Payam Hekmat Tsutomu YANO
77
+ Fred Ross Peter Kutak Viveck Shastri
78
+ Grzegorz Bloch Petr Udalau Vladimir Korenev
79
+ Henry Huang Phil Dunlea William J. Coleda
80
+ Immad Naseer Phil Varner Yegor Kozlov
81
+ Jan Stette Philipp Epp Zhang Zhang
82
+
83
+
84
+ Thank you all very much! Contributions are always welcome, come join the project.
85
+
86
+ Release Contents
87
+ ----------------
88
+
89
+ This release comes in two forms:
90
+ - pre-built binaries containing compiled versions of all Apache POI components and documentation
91
+ (poi-bin-3.7-20101029.zip or poi-bin-3.7-20101029.tar.gz)
92
+ - source archive you can build POI from (poi-src-3.7-20101029.zip or poi-src-3.7-20101029.tar.gz)
93
+
94
+ Pre-built versions of all POI components are also available in the central Maven repository
95
+ under Group ID "org.apache.poi" and Version "3.7"
96
+
97
+ All release artifacts are accompanied by MD5 checksums and a PGP signatures
98
+ that you can use to verify the authenticity of your download.
99
+ The public key used for the PGP signature can be found at
100
+ http://svn.apache.org/repos/asf/poi/tags/REL_3_7/KEYS
101
+
102
+
103
+ About Apache POI
104
+ -----------------------
105
+
106
+ Apache POI is well-known in the Java field as a library for reading and
107
+ writing Microsoft Office file formats, such as Excel, PowerPoint, Visio and
108
+ Word. Since POI 3.5, the new OOXML (Office Open XML) formats introduced in Office 2007 have been supported.
109
+
110
+ For more information, visit http://poi.apache.org/
111
+
112
+
113
+ For the Apache POI Team
114
+ Yegor Kozlov
115
+
@@ -0,0 +1,99 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2011
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specific loader to support CSV files.
7
+ #
8
+ #
9
+ require 'loaders/loader_base'
10
+ require 'datashift/exceptions'
11
+ require 'datashift/method_mapper'
12
+
13
+ module DataShift
14
+
15
+ module CsvLoading
16
+
17
+ def perform_csv_load(file_name, options = {})
18
+
19
+ require "csv"
20
+
21
+ # TODO - can we abstract out what a 'parsed file' is - so a common object can represent excel,csv etc
22
+ # then we can make load() more generic
23
+
24
+ @parsed_file = CSV.read(file_name)
25
+
26
+
27
+ @method_mapper = DataShift::MethodMapper.new
28
+
29
+ @mandatory = options[:mandatory] || []
30
+
31
+ # Create a method_mapper which maps list of headers into suitable calls on the Active Record class
32
+ map_headers_to_operators( @parsed_file.shift, options[:strict] , @mandatory )
33
+
34
+ unless(@method_mapper.missing_methods.empty?)
35
+ puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
36
+ raise MappingDefinitionError, "ERROR: Missing mappings for #{@method_mapper.missing_methods.size} column headings"
37
+ end
38
+
39
+ #if(options[:verbose])
40
+ puts "\n\n\nLoading from CSV file: #{file_name}"
41
+ puts "Processing #{@parsed_file.size} rows"
42
+ # end
43
+
44
+ load_object_class.transaction do
45
+ @loaded_objects = []
46
+
47
+ @parsed_file.each do |row|
48
+
49
+ # TODO - Smart sorting of column processing order ....
50
+ # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
51
+ # before associations can be processed so user should ensure mandatory columns are prior to associations
52
+
53
+ # as part of this we also attempt to save early, for example before assigning to
54
+ # has_and_belongs_to associations which require the load_object has an id for the join table
55
+
56
+ # Iterate over the columns method_mapper found in Excel,
57
+ # pulling data out of associated column
58
+ @method_mapper.method_details.each_with_index do |method_detail, col|
59
+
60
+ value = row[col]
61
+
62
+ prepare_data(method_detail, value)
63
+
64
+ process()
65
+ end
66
+
67
+ # TODO - handle when it's not valid ?
68
+ # Process rest and dump out an exception list of Products ??
69
+
70
+ puts "SAVING ROW #{row} : #{load_object.inspect}" #if options[:verbose]
71
+
72
+ save
73
+
74
+ # don't forget to reset the object or we'll update rather than create
75
+ new_load_object
76
+
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ class CsvLoader < LoaderBase
83
+
84
+ include DataShift::CsvLoading
85
+
86
+ def initialize(klass, object = nil, options = {})
87
+ super( klass, object, options )
88
+ raise "Cannot load - failed to create a #{klass}" unless @load_object
89
+ end
90
+
91
+ def perform_load( file_name, options = {} )
92
+ perform_csv_load( file_name, options )
93
+
94
+ puts "Excel loading stage complete - #{loaded_objects.size} rows added."
95
+ end
96
+
97
+ end
98
+
99
+ end
@@ -0,0 +1,150 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2011
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specific loader to support Excel files.
7
+ # Note this only requires JRuby, Excel not required, nor Win OLE.
8
+ #
9
+ # Maps column headings to operations on the model.
10
+ # Iterates over all the rows using mapped operations to assign row data to a database object,
11
+ # i.e pulls data from each column and sends to object.
12
+ #
13
+ require 'datashift/exceptions'
14
+
15
+
16
+ module DataShift
17
+
18
+ if(Guards::jruby?)
19
+
20
+ require 'loaders/loader_base'
21
+
22
+ require 'java'
23
+ require 'jexcel_file'
24
+
25
+ module ExcelLoading
26
+
27
+ # Options:
28
+ # [:header_row] : Default is 0. Use alternative row as header definition.
29
+ # [:mandatory] : Array of mandatory column names
30
+ # [:strict] : Raise exception when no mapping found for a column heading (non mandatory)
31
+ # [:sheet_number]
32
+
33
+ def perform_excel_load( file_name, options = {} )
34
+
35
+ @mandatory = options[:mandatory] || []
36
+
37
+ @excel = JExcelFile.new
38
+
39
+ @excel.open(file_name)
40
+
41
+ #if(options[:verbose])
42
+ puts "\n\n\nLoading from Excel file: #{file_name}"
43
+
44
+ sheet_number = options[:sheet_number] || 0
45
+
46
+ @sheet = @excel.sheet( sheet_number )
47
+
48
+ header_row_index = options[:header_row] || 0
49
+ @header_row = @sheet.getRow(header_row_index)
50
+
51
+ raise MissingHeadersError, "No headers found - Check Sheet #{@sheet} is complete and Row #{header_row_index} contains headers" unless(@header_row)
52
+
53
+ @headers = []
54
+
55
+ (0..JExcelFile::MAX_COLUMNS).each do |i|
56
+ cell = @header_row.getCell(i)
57
+ break unless cell
58
+ header = "#{@excel.cell_value(cell).to_s}".strip
59
+ break if header.empty?
60
+ @headers << header
61
+ end
62
+
63
+ raise MissingHeadersError, "No headers found - Check Sheet #{@sheet} is complete and Row #{header_row_index} contains headers" if(@headers.empty?)
64
+
65
+ # Create a method_mapper which maps list of headers into suitable calls on the Active Record class
66
+ map_headers_to_operators( @headers, options[:strict] , @mandatory )
67
+
68
+ load_object_class.transaction do
69
+ @loaded_objects = []
70
+
71
+ (1..@excel.num_rows).collect do |row|
72
+
73
+ # Excel num_rows seems to return all 'visible' rows, which appears to be greater than the actual data rows
74
+ # (TODO - write spec to process .xls with a huge number of rows)
75
+ #
76
+ # This is rubbish but currently manually detect when actual data ends, this isn't very smart but
77
+ # got no better idea than ending once we hit the first completely empty row
78
+ break if @excel.sheet.getRow(row).nil?
79
+
80
+ contains_data = false
81
+
82
+ # TODO - Smart sorting of column processing order ....
83
+ # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
84
+ # before associations can be processed so user should ensure mandatory columns are prior to associations
85
+
86
+ # as part of this we also attempt to save early, for example before assigning to
87
+ # has_and_belongs_to associations which require the load_object has an id for the join table
88
+
89
+ # Iterate over the columns method_mapper found in Excel,
90
+ # pulling data out of associated column
91
+ @method_mapper.method_details.each_with_index do |method_detail, col|
92
+
93
+ value = value_at(row, col)
94
+
95
+ contains_data = true unless(value.nil? || value.to_s.empty?)
96
+
97
+ #puts "DEBUG: Excel process METHOD :#{method_detail.inspect}", value.inspect
98
+ prepare_data(method_detail, value)
99
+
100
+ process()
101
+ end
102
+
103
+ break unless(contains_data == true)
104
+
105
+ # TODO - requirements to handle not valid ?
106
+ # all or nothing or carry on and dump out the exception list at end
107
+ #puts "DEBUG: FINAL SAVE #{load_object.inspect}"
108
+ save
109
+ #puts "DEBUG: SAVED #{load_object.inspect}"
110
+
111
+ # don't forget to reset the object or we'll update rather than create
112
+ new_load_object
113
+
114
+ end
115
+ end
116
+ puts "Excel loading stage complete - #{loaded_objects.size} rows added."
117
+ end
118
+
119
+ def value_at(row, column)
120
+ @excel.get_cell_value( @excel.sheet.getRow(row), column)
121
+ end
122
+ end
123
+
124
+
125
+ class ExcelLoader < LoaderBase
126
+
127
+ include ExcelLoading
128
+
129
+ def initialize(klass, object = nil, options = {})
130
+ super( klass, object, options )
131
+ raise "Cannot load - failed to create a #{klass}" unless @load_object
132
+ end
133
+
134
+
135
+ def perform_load( file_name, options = {} )
136
+ perform_excel_load( file_name, options )
137
+
138
+ puts "Excel loading stage complete - #{loaded_objects.size} rows added."
139
+ end
140
+
141
+ end
142
+
143
+ else
144
+
145
+ module ExcelLoading
146
+ end
147
+
148
+ end
149
+
150
+ end
@@ -0,0 +1,332 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2010
4
+ # License:: MIT
5
+ #
6
+ # Details:: Base class for loaders, providing a process hook which populates a model,
7
+ # based on a method map and supplied value from a file - i.e a single column/row's string value.
8
+ # Note that although a single column, the string can be formatted to contain multiple values.
9
+ #
10
+ # Tightly coupled with MethodMapper classes (in lib/engine) which contains full details of
11
+ # a file's column and it's correlated AR associations.
12
+ #
13
+ module DataShift
14
+
15
+ require 'datashift/method_mapper'
16
+
17
+ class LoaderBase
18
+
19
+ attr_reader :headers
20
+
21
+ attr_accessor :method_mapper
22
+
23
+ attr_accessor :load_object_class, :load_object
24
+ attr_accessor :current_value, :current_method_detail
25
+
26
+ attr_accessor :loaded_objects, :failed_objects
27
+
28
+ attr_accessor :options
29
+
30
+ # Support multiple associations being added to a base object to be specified in a single column.
31
+ #
32
+ # Entry represents the association to find via supplied name, value to use in the lookup.
33
+ # Can contain multiple lookup name/value pairs, separated by multi_assoc_delim ( | )
34
+ #
35
+ # Default syntax :
36
+ #
37
+ # Name1:value1, value2|Name2:value1, value2, value3|Name3:value1, value2
38
+ #
39
+ # E.G.
40
+ # Association Properties, has a column named Size, and another called Colour,
41
+ # and this combination could be used to lookup multiple associations to add to the main model Jumper
42
+ #
43
+ # Size:small # => generates find_by_size( 'small' )
44
+ # Size:large # => generates find_by_size( 'large' )
45
+ # Colour:red,green,blue # => generates find_all_by_colour( ['red','green','blue'] )
46
+ #
47
+ # Size:large|Size:medium|Size:large
48
+ # => Find 3 different associations, perform lookup via column called Size
49
+ # => Jumper.properties << [ small, medium, large ]
50
+ #
51
+ def self.name_value_delim
52
+ @name_value_delim ||= ':'
53
+ @name_value_delim
54
+ end
55
+
56
+ def self.set_name_value_delim(x) @name_value_delim = x; end
57
+ # TODO - support embedded object creation/update via hash (which hopefully we should be able to just forward to AR)
58
+ #
59
+ # |Category|
60
+ # name:new{ :date => '20110102', :owner = > 'blah'}
61
+ #
62
+
63
+
64
+ def self.multi_value_delim
65
+ @multi_value_delim ||= ','
66
+ @multi_value_delim
67
+ end
68
+
69
+ def self.set_multi_value_delim(x) @multi_value_delim = x; end
70
+
71
+ # TODO - support multi embedded object creation/update via hash (which hopefully we should be able to just forward to AR)
72
+ #
73
+ # |Category|
74
+ # name:new{ :a => 1, :b => 2}|name:medium{ :a => 6, :b => 34}|name:old{ :a => 12, :b => 67}
75
+ #
76
+ def self.multi_assoc_delim
77
+ @multi_assoc_delim ||= '|'
78
+ @multi_assoc_delim
79
+ end
80
+
81
+ def self.set_multi_assoc_delim(x) @multi_assoc_delim = x; end
82
+
83
+ # Options
84
+ # :instance_methods => true
85
+
86
+ def initialize(object_class, object = nil, options = {})
87
+ @load_object_class = object_class
88
+
89
+ # Gather list of all possible 'setter' methods on AR class (instance variables and associations)
90
+ DataShift::MethodMapper.find_operators( @load_object_class, :reload => true, :instance_methods => options[:instance_methods] )
91
+
92
+ @method_mapper = DataShift::MethodMapper.new
93
+ @options = options.clone
94
+ @headers = []
95
+
96
+ @default_values = {}
97
+ @prefixes = {}
98
+ @postfixes = {}
99
+
100
+ reset(object)
101
+ end
102
+
103
+
104
+ # kinda the derived classes interface - best way in Ruby ?
105
+ def perform_load( input, options = {} )
106
+ raise "WARNING- ABSTRACT METHOD CALLED - Please implement perform_load()"
107
+ end
108
+
109
+
110
+ # Core API - Given a list of free text column names from a file, map all headers to
111
+ # method mapper's operator list.
112
+ # Options:
113
+ # strict : report any header values that can't be mapped as an error
114
+ #
115
+ def map_headers_to_operators( headers, strict, mandatory = [])
116
+ @headers = headers
117
+
118
+ @method_mapper.populate_methods( load_object_class, @headers )
119
+
120
+ unless(@method_mapper.missing_methods.empty?)
121
+ puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
122
+ raise MappingDefinitionError, "Missing mappings for columns : #{@method_mapper.missing_methods.join(",")}" if(strict)
123
+ end
124
+
125
+ unless(@method_mapper.contains_mandatory?(mandatory) )
126
+ @method_mapper.missing_mandatory(mandatory).each { |e| puts "ERROR: Mandatory column missing - expected column '#{e}'" }
127
+ raise MissingMandatoryError, "Mandatory columns missing - please fix and retry."
128
+ end unless(mandatory.empty?)
129
+ end
130
+
131
+
132
+ # Core API - Given a free text column name from a file, search method mapper for
133
+ # associated operator on base object class.
134
+ #
135
+ # If suitable association found, process row data and then assign to current load_object
136
+ def find_and_process(column_name, data)
137
+ method_detail = MethodMapper.find_method_detail( load_object_class, column_name )
138
+
139
+ if(method_detail)
140
+ prepare_data(method_detail, data)
141
+ process()
142
+ else
143
+ @load_object.errors.add_base( "No matching method found for column #{column_name}")
144
+ end
145
+ end
146
+
147
+
148
+ # Set member variables to hold detsails and value.
149
+ #
150
+ # Check supplied value, validate it, and if required :
151
+ # set to any provided default value
152
+ # prepend or append with any provided extensions
153
+ def prepare_data(method_detail, value)
154
+
155
+ @current_value = value
156
+
157
+ @current_method_detail = method_detail
158
+
159
+ operator = method_detail.operator
160
+
161
+ if(default_value(operator) && (value.nil? || value.to_s.empty?))
162
+ @current_value = default_value(operator)
163
+ end
164
+
165
+ @current_value = "#{prefixes(operator)}#{@current_value}" if(prefixes(operator))
166
+ @current_value = "#{@current_value}#{postfixes(operator)}" if(postfixes(operator))
167
+
168
+ @current_value
169
+ end
170
+
171
+
172
+ # Process a value string from a column.
173
+ # Assigning value(s) to correct association on @load_object.
174
+ # Method detail represents a column from a file and it's correlated AR associations.
175
+ # Value string which may contain multiple values for a collection association.
176
+ #
177
+ def process()
178
+
179
+ if(@current_method_detail.operator_for(:has_many))
180
+
181
+ if(@current_method_detail.operator_class && @current_value)
182
+
183
+ # there are times when we need to save early, for example before assigning to
184
+ # has_and_belongs_to associations which require the load_object has an id for the join table
185
+
186
+ save_if_new
187
+
188
+ # A single column can contain multiple associations delimited by special char
189
+ columns = @current_value.to_s.split( LoaderBase::multi_assoc_delim)
190
+
191
+ # Size:large|Colour:red,green,blue => generates find_by_size( 'large' ) and find_all_by_colour( ['red','green','blue'] )
192
+
193
+ columns.each do |assoc|
194
+ operator, values = assoc.split(LoaderBase::name_value_delim)
195
+
196
+ lookups = values.split(LoaderBase::multi_value_delim)
197
+
198
+ if(lookups.size > 1)
199
+
200
+ @current_value = @current_method_detail.operator_class.send("find_all_by_#{operator}", lookups )
201
+
202
+ unless(lookups.size == @current_value.size)
203
+ found = @current_value.collect {|f| f.send(operator) }
204
+ @load_object.errors.add( method_detail.operator, "Association with key(s) #{(lookups - found).inspect} NOT found")
205
+ puts "WARNING: Association with key(s) #{(lookups - found).inspect} NOT found - Not added."
206
+ next if(@current_value.empty?)
207
+ end
208
+
209
+ else
210
+
211
+ @current_value = @current_method_detail.operator_class.send("find_by_#{operator}", lookups )
212
+
213
+ unless(@current_value)
214
+ @load_object.errors.add( @current_method_detail.operator, "Association with key #{lookups} NOT found")
215
+ puts "WARNING: Association with key #{lookups} NOT found - Not added."
216
+ next
217
+ end
218
+
219
+ end
220
+
221
+ # Lookup Assoc's Model done, now add the found value(s) to load model's collection
222
+ @current_method_detail.assign(@load_object, @current_value)
223
+ end
224
+ end
225
+ # END HAS_MANY
226
+ else
227
+ # Nice n simple straight assignment to a column variable
228
+ #puts "INFO: LOADER BASE processing #{method_detail.name}"
229
+ @current_method_detail.assign(@load_object, @current_value)
230
+ end
231
+ end
232
+
233
+ def save
234
+ #puts "DEBUG: SAVING #{load_object.class} : #{load_object.inspect}" #if(options[:verbose])
235
+ begin
236
+ result = @load_object.save
237
+ #puts "DEBUG: SAVED [#{result.inspect}]"
238
+ #puts "SAVED 2. #{load_object.errors.methods.inspect}"
239
+ #puts "SAVED 3. #{load_object.errors.full_messages.inspect}"
240
+ @loaded_objects << @load_object unless(@loaded_objects.include?(@load_object))
241
+
242
+ return result
243
+ rescue => e
244
+ @failed_objects << @load_object unless( !load_object.new_record? || @failed_objects.include?(@load_object))
245
+ puts "Error saving #{@load_object.class} : #{e.inspect}"
246
+ puts e.backtrace
247
+ raise "Error in save whilst processing column #{@current_method_detail.name}" if(@options[:strict])
248
+ end
249
+ end
250
+
251
+ def set_default_value( name, value )
252
+ @default_values[name] = value
253
+ end
254
+
255
+ def default_value(name)
256
+ @default_values[name]
257
+ end
258
+
259
+ def set_prefix( name, value )
260
+ @prefixes[name] = value
261
+ end
262
+
263
+ def prefixes(name)
264
+ @prefixes[name]
265
+ end
266
+
267
+ def set_postfix( name, value )
268
+ @postfixes[name] = value
269
+ end
270
+
271
+ def postfixes(name)
272
+ @postfixes[name]
273
+ end
274
+
275
+
276
+ # Reset the loader, including database object to be populated, and load counts
277
+ #
278
+ def reset(object = nil)
279
+ @load_object = object || new_load_object
280
+ @loaded_objects, @failed_objects = [],[]
281
+ @current_value = nil
282
+ end
283
+
284
+
285
+ def new_load_object
286
+ @load_object = @load_object_class.new
287
+ @load_object
288
+ end
289
+
290
+ def abort_on_failure?
291
+ @options[:abort_on_failure] == 'true'
292
+ end
293
+
294
+ def loaded_count
295
+ @loaded_objects.size
296
+ end
297
+
298
+ def failed_count
299
+ @failed_objects.size
300
+ end
301
+
302
+
303
+ # Check whether headers contains supplied list
304
+ def headers_contain_mandatory?( mandatory_list )
305
+ [ [*mandatory_list] - @headers].flatten.empty?
306
+ end
307
+
308
+
309
+ # Check whether headers contains supplied list
310
+ def missing_mandatory_headers( mandatory_list )
311
+ [ [*mandatory_list] - @headers].flatten
312
+ end
313
+
314
+ def find_or_new( klass, condition_hash = {} )
315
+ @records[klass] = klass.find(:all, :conditions => condition_hash)
316
+ if @records[klass].any?
317
+ return @records[klass].first
318
+ else
319
+ return klass.new
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ def save_if_new
326
+ #puts "SAVE", load_object.inspect
327
+ save if(load_object.valid? && load_object.new_record?)
328
+ end
329
+
330
+ end
331
+
332
+ end