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.
- data/.document +5 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +211 -0
- data/LICENSE.txt +27 -0
- data/README.markdown +286 -0
- data/README.rdoc +19 -0
- data/Rakefile +96 -0
- data/VERSION +5 -0
- data/bin/autospec +16 -0
- data/bin/convert_to_should_syntax +16 -0
- data/bin/erubis +16 -0
- data/bin/htmldiff +16 -0
- data/bin/jeweler +16 -0
- data/bin/ldiff +16 -0
- data/bin/nokogiri +16 -0
- data/bin/rackup +16 -0
- data/bin/rails +16 -0
- data/bin/rake +16 -0
- data/bin/rake2thor +16 -0
- data/bin/ri +16 -0
- data/bin/rspec +16 -0
- data/bin/spree +16 -0
- data/bin/thor +16 -0
- data/bin/tilt +16 -0
- data/bin/tt +16 -0
- data/datashift.gemspec +178 -0
- data/lib/applications/jruby/jexcel_file.rb +397 -0
- data/lib/applications/jruby/word.rb +79 -0
- data/lib/datashift.rb +114 -0
- data/lib/datashift/exceptions.rb +12 -0
- data/lib/datashift/file_definitions.rb +353 -0
- data/lib/datashift/mapping_file_definitions.rb +88 -0
- data/lib/datashift/method_detail.rb +237 -0
- data/lib/datashift/method_mapper.rb +257 -0
- data/lib/generators/csv_generator.rb +36 -0
- data/lib/generators/excel_generator.rb +122 -0
- data/lib/generators/generator_base.rb +14 -0
- data/lib/helpers/core_ext/to_b.rb +24 -0
- data/lib/helpers/spree_helper.rb +131 -0
- data/lib/java/poi-3.7/._poi-3.7-20101029.jar5645100390082102460.tmp +0 -0
- data/lib/java/poi-3.7/LICENSE +507 -0
- data/lib/java/poi-3.7/NOTICE +21 -0
- data/lib/java/poi-3.7/RELEASE_NOTES.txt +115 -0
- data/lib/java/poi-3.7/lib/commons-logging-1.1.jar +0 -0
- data/lib/java/poi-3.7/lib/junit-3.8.1.jar +0 -0
- data/lib/java/poi-3.7/lib/log4j-1.2.13.jar +0 -0
- data/lib/java/poi-3.7/ooxml-lib/dom4j-1.6.1.jar +0 -0
- data/lib/java/poi-3.7/ooxml-lib/geronimo-stax-api_1.0_spec-1.0.jar +0 -0
- data/lib/java/poi-3.7/ooxml-lib/xmlbeans-2.3.0.jar +0 -0
- data/lib/java/poi-3.7/poi-3.7-20101029.jar +0 -0
- data/lib/java/poi-3.7/poi-examples-3.7-20101029.jar +0 -0
- data/lib/java/poi-3.7/poi-ooxml-3.7-20101029.jar +0 -0
- data/lib/java/poi-3.7/poi-ooxml-schemas-3.7-20101029.jar +0 -0
- data/lib/java/poi-3.7/poi-scratchpad-3.7-20101029.jar +0 -0
- data/lib/loaders/csv_loader.rb +99 -0
- data/lib/loaders/excel_loader.rb +150 -0
- data/lib/loaders/loader_base.rb +332 -0
- data/lib/loaders/spreadsheet_loader.rb +137 -0
- data/lib/loaders/spree/image_loader.rb +46 -0
- data/lib/loaders/spree/product_loader.rb +225 -0
- data/spec/csv_loader_spec.rb +31 -0
- data/spec/datashift_spec.rb +27 -0
- data/spec/db/migrate/20110803201325_create_test_bed.rb +85 -0
- data/spec/excel_generator_spec.rb +79 -0
- data/spec/excel_loader_spec.rb +177 -0
- data/spec/file_definitions.rb +141 -0
- data/spec/fixtures/BadAssociationName.xls +0 -0
- data/spec/fixtures/DemoNegativeTesting.xls +0 -0
- data/spec/fixtures/ProjectsMultiCategories.xls +0 -0
- data/spec/fixtures/ProjectsSingleCategories.xls +0 -0
- data/spec/fixtures/SimpleProjects.xls +0 -0
- data/spec/fixtures/config/database.yml +25 -0
- data/spec/fixtures/interact_models_db.sqlite +0 -0
- data/spec/fixtures/interact_spree_db.sqlite +0 -0
- data/spec/fixtures/negative/SpreeProdMiss1Mandatory.csv +4 -0
- data/spec/fixtures/negative/SpreeProdMiss1Mandatory.xls +0 -0
- data/spec/fixtures/negative/SpreeProdMissManyMandatory.csv +4 -0
- data/spec/fixtures/negative/SpreeProdMissManyMandatory.xls +0 -0
- data/spec/fixtures/simple_export_spec.xls +0 -0
- data/spec/fixtures/simple_template_spec.xls +0 -0
- data/spec/fixtures/spree/SpreeProducts.csv +4 -0
- data/spec/fixtures/spree/SpreeProducts.xls +0 -0
- data/spec/fixtures/spree/SpreeProductsMultiColumn.csv +4 -0
- data/spec/fixtures/spree/SpreeProductsMultiColumn.xls +0 -0
- data/spec/fixtures/spree/SpreeProductsSimple.csv +4 -0
- data/spec/fixtures/spree/SpreeProductsSimple.xls +0 -0
- data/spec/fixtures/spree/SpreeZoneExample.csv +5 -0
- data/spec/fixtures/spree/SpreeZoneExample.xls +0 -0
- data/spec/fixtures/test_model_defs.rb +57 -0
- data/spec/loader_spec.rb +121 -0
- data/spec/method_mapper_spec.rb +238 -0
- data/spec/spec_helper.rb +116 -0
- data/spec/spree_generator_spec.rb +65 -0
- data/spec/spree_loader_spec.rb +311 -0
- data/spec/spree_method_mapping_spec.rb +215 -0
- data/tasks/config/seed_fu_product_template.erb +15 -0
- data/tasks/config/tidy_config.txt +13 -0
- data/tasks/db_tasks.rake +65 -0
- data/tasks/excel_generator.rake +79 -0
- data/tasks/file_tasks.rake +37 -0
- data/tasks/import/csv.rake +50 -0
- data/tasks/import/excel.rake +67 -0
- data/tasks/spree/image_load.rake +109 -0
- data/tasks/spree/product_loader.rake +44 -0
- data/tasks/word_to_seedfu.rake +167 -0
- data/test/helper.rb +18 -0
- data/test/test_interact.rb +7 -0
- 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
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|