datashift 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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