from_excel 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +168 -0
- data/lib/excel_import.rb +166 -0
- data/lib/from_excel.rb +5 -0
- data/test/helper.rb +10 -0
- data/test/model/adress.rb +17 -0
- data/test/model/role.rb +13 -0
- data/test/model/user.rb +21 -0
- data/test/test_bounds.rb +68 -0
- data/test/test_from_excel.rb +84 -0
- data/test/test_mapping.rb +55 -0
- metadata +82 -0
data/README.rdoc
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
== Overview
|
2
|
+
|
3
|
+
Import excel spreadsheet rows to ruby objects.
|
4
|
+
For how to use it, see examples below or see unit tests
|
5
|
+
|
6
|
+
== Install
|
7
|
+
|
8
|
+
gem install from_excel
|
9
|
+
|
10
|
+
== Dependencies
|
11
|
+
|
12
|
+
gem 'nokogiri'
|
13
|
+
gem 'roo'
|
14
|
+
gem 'rubyzip'
|
15
|
+
gem 'spreadsheet'
|
16
|
+
gem 'google-spreadsheet-ruby'
|
17
|
+
|
18
|
+
(also require 'active_support/inflector')
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
data = Model.from_excel(spreadsheet_file, options)
|
23
|
+
|
24
|
+
Include module in your model : include ExcelImport
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
spreadsheet_file : File object as spreadsheet (ie :File.new ("path_to_spreadsheet"))
|
29
|
+
|
30
|
+
options :
|
31
|
+
:mapping => hash mapping attribute <=> <column name or column index>
|
32
|
+
:title => true|false . Pass false to indicate that columns have no title. True by default.
|
33
|
+
:limits => :start => [first_row, first_column], :end => [last_row, last_column]
|
34
|
+
:rules => Procs rules to convert data in cell sheet to desired data for object attribute.
|
35
|
+
For example, transform a string to association id (see example below)
|
36
|
+
|
37
|
+
|
38
|
+
<b>Note: If columns have title, by default the row of columns titles is supposed to be the first row before the first row
|
39
|
+
of data which will be convert to ruby object.</b>
|
40
|
+
|
41
|
+
== Basic spreadsheet
|
42
|
+
|
43
|
+
Suppose you have this kind of Excel sheet:
|
44
|
+
A B C
|
45
|
+
1 First name Last name Age
|
46
|
+
2 Albert Einstein 131
|
47
|
+
3 Leonard De Vinci 558
|
48
|
+
|
49
|
+
and a class:
|
50
|
+
|
51
|
+
User (:first_name, last_name, :age)
|
52
|
+
|
53
|
+
You can retrieve users, the simple way like that :
|
54
|
+
|
55
|
+
users = User.from_excel(File.new ("path_to_spreadsheet"))
|
56
|
+
|
57
|
+
You get theses users :
|
58
|
+
|
59
|
+
[User.new(:first_name => 'Albert', :last_name => 'Einstein', :age => 131),
|
60
|
+
User.new(:first_name => 'Leonard', :last_name => 'De Vinci', :age => 558),
|
61
|
+
User.new(:first_name => 'Martin', :last_name => 'Heidegger', :age => 121)]
|
62
|
+
|
63
|
+
By default, attributes model are retrieved from columns title with the following simple rule :
|
64
|
+
o spaces are converted to "_"
|
65
|
+
o words are downcased
|
66
|
+
|
67
|
+
First Name => :first_name
|
68
|
+
Last Name => :last_name
|
69
|
+
|
70
|
+
|
71
|
+
== Mapping attribute - column names
|
72
|
+
|
73
|
+
If a column name can't be simply mapped to attribute model, you can pass a map to.
|
74
|
+
If, with the last spreadsheet example, "First name" column had "Prenom" instead as title and you want this column
|
75
|
+
to map :first_name attribute of User model, do like this :
|
76
|
+
|
77
|
+
users = User.from_excel sheet, :mapping => {:first_name => 'Prenom'}
|
78
|
+
|
79
|
+
== Spreadsheet whithout columns title
|
80
|
+
|
81
|
+
Suppose your spreadsheet has no columns title.
|
82
|
+
You can then map columns <=> model attribute via index of column.
|
83
|
+
You can also pass the starting cell of data (the first cell of data (excluded title when there is one))
|
84
|
+
|
85
|
+
users = User.from_excel sheet, :mapping => {:first_name => 1, :last_name => 2, :age => 3}, :limits => {:start => [2, 1]}
|
86
|
+
|
87
|
+
== Limits : Extraction of part of the spreadsheet
|
88
|
+
|
89
|
+
If you want to extract just a part of the spreadsheet, just pass start and end limits.
|
90
|
+
users = User.from_excel(sheet, :limits => {:start => [3, 3], :end => [5, 5] })
|
91
|
+
|
92
|
+
<b>Note</b> : The row which contains the columns titles (for default attributes mapping) is, here, supposed to be begining
|
93
|
+
at [2,3] and end at [2,5] (one row before the first row of data).
|
94
|
+
If not, you must pass a mapping with :attribute => column_index
|
95
|
+
|
96
|
+
== Object associations handling
|
97
|
+
|
98
|
+
Suppose user has one adress of class Adress(:street, :town, :zip_code)
|
99
|
+
And you have this spreadsheet :
|
100
|
+
A B C D E F
|
101
|
+
1 First name Last name Age Street Town Zip Code
|
102
|
+
2 Albert Einstein 131 17 rue de Brest Quimper 29000
|
103
|
+
So, you want user with user.adress = Adress.new(:stret => ..., :town => ... , :zip_code => ....)
|
104
|
+
Just pass the following mapping :
|
105
|
+
|
106
|
+
mapping = {[:adress, :street] => 'Street', [:adress, :town] => 'Town', [:adress, :zip_code] => 'Zip Code'}
|
107
|
+
|
108
|
+
users = User.from_excel sheet, :mapping => mapping
|
109
|
+
|
110
|
+
And you will get :
|
111
|
+
|
112
|
+
users.first.first_name => 'Albert'
|
113
|
+
users.first.adress.street => '17 rue de Brest'
|
114
|
+
|
115
|
+
users.first.adress => Adress.new(:street => '17 rue de Brest', :town => 'Quimper' .....)
|
116
|
+
|
117
|
+
== Rules
|
118
|
+
|
119
|
+
|
120
|
+
Suppose you have to transform value of cells column to value for your object attribute, make a rule!
|
121
|
+
|
122
|
+
For example, in the sheet you have a value that represents an association attribute id:
|
123
|
+
You have roles predifined in your database :
|
124
|
+
Roles :
|
125
|
+
id : 1 , name => 'Admin'
|
126
|
+
id : 2 , name => 'Reader' ....
|
127
|
+
|
128
|
+
Sheet :
|
129
|
+
|
130
|
+
First name Last name Rôle
|
131
|
+
Bobby Lapointe Admin
|
132
|
+
Gaston Lagaffe Reader
|
133
|
+
|
134
|
+
|
135
|
+
'Admin' value must be retrieved as a Role with id 1
|
136
|
+
Make the rules like that :
|
137
|
+
|
138
|
+
rules = {[:role, :id] => rule}
|
139
|
+
|
140
|
+
Where rule is a Proc.
|
141
|
+
|
142
|
+
mapping = {[:role, :id] => 'Rôle'}
|
143
|
+
rules = {[:role, :id] => rule}
|
144
|
+
users = User.from_excel sheet, :mapping => mapping, :rules => rules
|
145
|
+
|
146
|
+
|
147
|
+
def rule
|
148
|
+
lambda do |cell_value|
|
149
|
+
case cell_value
|
150
|
+
when 'Admin' then 1
|
151
|
+
when 'Reader' then 2
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
That's it !
|
158
|
+
You get user.first.role => Role(:id => 1)
|
159
|
+
|
160
|
+
You may use rules for other purpose. Rule proc aim to transform value sheet cell to value you need for
|
161
|
+
your model.
|
162
|
+
|
163
|
+
|
164
|
+
|
165
|
+
== TO DO
|
166
|
+
|
167
|
+
Option for other spreadsheet than Excel (LibreOffice ...)
|
168
|
+
Handle has_many associations
|
data/lib/excel_import.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
module ExcelImport
|
2
|
+
|
3
|
+
class Bounds
|
4
|
+
DEFAULT_OFFSET_FROM_TITLE = 1
|
5
|
+
|
6
|
+
attr_accessor :first_row, :last_row, :first_column, :last_column, :offset_from_title
|
7
|
+
|
8
|
+
def initialize sheet, options={}
|
9
|
+
@options = options
|
10
|
+
@offset_from_title = title_offset
|
11
|
+
bounds(sheet)
|
12
|
+
end
|
13
|
+
|
14
|
+
def bounds sheet
|
15
|
+
limits = @options[:limits] || limits_from(sheet)
|
16
|
+
@first_column = limits[:start] ? limits[:start][1] : sheet.first_column
|
17
|
+
@first_row = limits[:start] ? limits[:start][0] : sheet.first_row + offset_from_title
|
18
|
+
@last_column = limits[:end] ? limits[:end][1] : sheet.last_column
|
19
|
+
@last_row = limits[:end] ? limits[:end][0] : sheet.last_row
|
20
|
+
end
|
21
|
+
|
22
|
+
def limits_from sheet
|
23
|
+
{:start => [sheet.first_row + offset_from_title, sheet.first_column], :end => [sheet.last_row, sheet.last_column]}
|
24
|
+
end
|
25
|
+
|
26
|
+
def title_offset
|
27
|
+
@options[:title] = true if @options[:title].nil?
|
28
|
+
@options[:title] ? @options[:offset_from_title] || DEFAULT_OFFSET_FROM_TITLE : 0
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class Mapping
|
34
|
+
|
35
|
+
attr_accessor :map, :has_one_associations
|
36
|
+
|
37
|
+
def initialize sheet, map, bounds
|
38
|
+
@map = map
|
39
|
+
@sheet = sheet
|
40
|
+
@bounds = bounds
|
41
|
+
@map ? indexation : default
|
42
|
+
@has_one_associations = extract_has_one_associations
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_has_one_associations
|
46
|
+
@map.inject({}) do |associations, (attribute, index)|
|
47
|
+
next(associations) unless attribute.is_a?(Array)
|
48
|
+
association = attribute.first
|
49
|
+
next(associations) if associations.has_key?(association)
|
50
|
+
associations.merge({association => klass_of(association)})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def attributes_of association
|
55
|
+
@map.select {|attribute,| attribute.is_a?(Array) && attribute.first == association}
|
56
|
+
end
|
57
|
+
|
58
|
+
def indexation
|
59
|
+
return self if @map.all? {|key, value| value.is_a?(Integer)}
|
60
|
+
@bounds.first_column.upto(@bounds.last_column) do |index|
|
61
|
+
@map[attribute column_name_at(index)] = index
|
62
|
+
end
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def default
|
67
|
+
@map = {}
|
68
|
+
@bounds.first_column.upto(@bounds.last_column) do |index|
|
69
|
+
@map[Mapping.default_attribute_for column_name_at(index)] = index
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def attribute column
|
74
|
+
@map.each_pair {|key, value| return key if value == column}
|
75
|
+
Mapping.default_attribute_for(column)
|
76
|
+
end
|
77
|
+
|
78
|
+
def column_name_at column_index
|
79
|
+
@sheet.cell(@bounds.first_row - @bounds.offset_from_title, column_index)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.default_attribute_for column_name
|
83
|
+
column_name.downcase.gsub(' ', '_').to_sym
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def klass_of attribute
|
89
|
+
attribute.to_s.camelize.constantize
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
class Rules
|
95
|
+
|
96
|
+
def initialize rules
|
97
|
+
@rules = rules || {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def for attribute
|
101
|
+
@rules[attribute] || default
|
102
|
+
end
|
103
|
+
|
104
|
+
def default
|
105
|
+
lambda {|value| value}
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
class Integrator
|
111
|
+
|
112
|
+
def initialize sheet, klass, options
|
113
|
+
@sheet = sheet
|
114
|
+
@klass = klass
|
115
|
+
@bounds = Bounds.new(sheet, options)
|
116
|
+
@rules = Rules.new(options[:rules])
|
117
|
+
@mapping = Mapping.new(sheet, options[:mapping], @bounds)
|
118
|
+
end
|
119
|
+
|
120
|
+
def cell_value row_index, column_index
|
121
|
+
@rules.for(@mapping.attribute(column_index)).call(@sheet.cell(row_index, column_index))
|
122
|
+
end
|
123
|
+
|
124
|
+
def primary_attributes_hash row_index
|
125
|
+
@mapping.map.select{|attribute,| attribute.is_a? Symbol}.inject({}) do |hash, (attribute, index)|
|
126
|
+
hash[attribute] = cell_value(row_index, index)
|
127
|
+
hash
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def object_from_columns_for_association association, row_index, klass
|
132
|
+
attributes = @mapping.attributes_of(association)
|
133
|
+
attributes.inject(klass.new) do |object, ((association, attribute), index)|
|
134
|
+
object.send(attribute.to_s + '=', cell_value(row_index, index))
|
135
|
+
object
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def has_one_associations_attributes_hash row_index
|
140
|
+
@mapping.has_one_associations.inject({}) do |hash, (association, klass)|
|
141
|
+
hash[association] = object_from_columns_for_association(association, row_index, klass)
|
142
|
+
hash
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def row_to_hash row_index
|
147
|
+
hash = primary_attributes_hash(row_index)
|
148
|
+
hash.merge(has_one_associations_attributes_hash row_index)
|
149
|
+
end
|
150
|
+
|
151
|
+
def rows_to_objects
|
152
|
+
@bounds.first_row.upto(@bounds.last_row).inject([]) {|objects, row_index| objects << @klass.new(row_to_hash(row_index)); objects}
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.included(base)
|
158
|
+
class << base
|
159
|
+
def from_excel spreadsheet, opt={}
|
160
|
+
integrator = Integrator.new(Excel.new(spreadsheet.path), self, opt)
|
161
|
+
integrator.rows_to_objects
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
data/lib/from_excel.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Adress
|
2
|
+
attr_accessor :street, :town, :zip_code
|
3
|
+
|
4
|
+
def initialize args={}
|
5
|
+
@street = args[:street]
|
6
|
+
@town = args[:town]
|
7
|
+
@zip_code = args[:zip_code]
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
return false unless other
|
12
|
+
street == other.street &&
|
13
|
+
town == other.town &&
|
14
|
+
zip_code == other.zip_code
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/test/model/role.rb
ADDED
data/test/model/user.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class User
|
2
|
+
include ExcelImport
|
3
|
+
attr_accessor :first_name, :last_name, :age, :adress, :role
|
4
|
+
|
5
|
+
def initialize args={}
|
6
|
+
@first_name = args[:first_name]
|
7
|
+
@last_name = args[:last_name]
|
8
|
+
@age = args[:age]
|
9
|
+
@adress = args[:adress]
|
10
|
+
@role = args[:role]
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
first_name == other.first_name &&
|
15
|
+
last_name == other.last_name &&
|
16
|
+
age == other.age &&
|
17
|
+
adress == other.adress &&
|
18
|
+
role == other.role
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/test/test_bounds.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestBounds < Test::Unit::TestCase
|
4
|
+
|
5
|
+
|
6
|
+
def test_default_bounds
|
7
|
+
sheet = stubbed_sheet_with_bounds
|
8
|
+
bounds = ExcelImport::Bounds.new(sheet)
|
9
|
+
assert_equal 1, bounds.first_column
|
10
|
+
assert_equal 10, bounds.last_column
|
11
|
+
assert_equal 2, bounds.first_row #have a title line by default so first row is after title line
|
12
|
+
assert_equal 10, bounds.last_row
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_bounds_with_limits_changed
|
16
|
+
sheet = stub
|
17
|
+
bounds = ExcelImport::Bounds.new(sheet, :limits => {:start => [3, 4], :end => [5, 20]})
|
18
|
+
assert_equal 4, bounds.first_column
|
19
|
+
assert_equal 20, bounds.last_column
|
20
|
+
assert_equal 3, bounds.first_row
|
21
|
+
assert_equal 5, bounds.last_row
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_bounds_without_title_for_columns
|
25
|
+
sheet = stubbed_sheet_with_bounds
|
26
|
+
bounds = ExcelImport::Bounds.new(sheet, :title => false)
|
27
|
+
assert_equal 1, bounds.first_column
|
28
|
+
assert_equal 10, bounds.last_column
|
29
|
+
assert_equal 1, bounds.first_row # No title so first row is the first row of sheet
|
30
|
+
assert_equal 10, bounds.last_row
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_bounds_with_only_start_key_should_take_sheet_end_cell_coordinates
|
34
|
+
sheet = stubbed_sheet_with_bounds
|
35
|
+
bounds = ExcelImport::Bounds.new(sheet, :limits => {:start => [3, 4]})
|
36
|
+
assert_equal 4, bounds.first_column
|
37
|
+
assert_equal 10, bounds.last_column
|
38
|
+
assert_equal 3, bounds.first_row
|
39
|
+
assert_equal 10, bounds.last_row
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_bounds_with_only_end_key_should_take_sheet_start_cell_coordinates_with_1_added_to_first_row_if_title
|
43
|
+
sheet = stubbed_sheet_with_bounds
|
44
|
+
bounds = ExcelImport::Bounds.new(sheet, :limits => {:end => [10, 5]})
|
45
|
+
assert_equal 1, bounds.first_column
|
46
|
+
assert_equal 5, bounds.last_column
|
47
|
+
assert_equal 2, bounds.first_row
|
48
|
+
assert_equal 10, bounds.last_row
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_bounds_with_offset_from_title
|
52
|
+
sheet = stubbed_sheet_with_bounds
|
53
|
+
bounds = ExcelImport::Bounds.new(sheet, :offset_from_title => 4)
|
54
|
+
assert_equal 1, bounds.first_column
|
55
|
+
assert_equal 10, bounds.last_column
|
56
|
+
assert_equal 5, bounds.first_row
|
57
|
+
assert_equal 10, bounds.last_row
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def stubbed_sheet_with_bounds
|
65
|
+
stub(:first_column => 1, :last_column => 10, :first_row => 1, :last_row => 10)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestFromExcel < Test::Unit::TestCase
|
4
|
+
SIMPLE_SHEET= File.join(File.dirname(__FILE__), 'spreadsheets/simple_sheet.xls')
|
5
|
+
SHEET_WITH_COLUMN_NAME_DIFFERENT_FROM_ATTRIBUTE = File.join(File.dirname(__FILE__), 'spreadsheets/sheet_with_column_name_different_from_attribute.xls')
|
6
|
+
SHEET_WITHOUT_COLUMNS_NAMES = File.join(File.dirname(__FILE__), 'spreadsheets/sheet_without_columns_names.xls')
|
7
|
+
SHEET_WITH_START_END_COLUMNS_ROWS = File.join(File.dirname(__FILE__), 'spreadsheets/sheet_with_start_end_columns_rows.xls')
|
8
|
+
SHEET_WITH_COLUMNS_FROM_ASSOCIATION = File.join(File.dirname(__FILE__), 'spreadsheets/sheet_with_columns_from_association.xls')
|
9
|
+
SHEET_WITH_COLUMN_VALUE_TO_ASSOCIATION_ID = File.join(File.dirname(__FILE__), 'spreadsheets/sheet_with_column_value_to_association_id.xls')
|
10
|
+
|
11
|
+
def test_import_with_simple_sheet
|
12
|
+
assert users = User.from_excel(File.new SIMPLE_SHEET)
|
13
|
+
assert_equal sheet_users, users
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_import_with_simple_sheet_with_some_attributes_mapped_with_columns_names
|
17
|
+
sheet = File.new(SHEET_WITH_COLUMN_NAME_DIFFERENT_FROM_ATTRIBUTE)
|
18
|
+
users = User.from_excel sheet, :mapping => {:first_name => 'Prenom'}
|
19
|
+
assert_equal sheet_users, users
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_should_be_able_to_import_with_attributes_mapped_with_columns_index
|
23
|
+
sheet = File.new(SHEET_WITHOUT_COLUMNS_NAMES)
|
24
|
+
users = User.from_excel sheet, :mapping => {:first_name => 1, :last_name => 2, :age => 3},
|
25
|
+
:limits => {:start => [2, 1]}
|
26
|
+
assert_equal sheet_users, users
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_should_be_able_to_import_simple_sheet_with_given_start_and_end_columns_and_rows
|
30
|
+
assert users = User.from_excel(File.new(SHEET_WITH_START_END_COLUMNS_ROWS), :limits => {:start => [3, 3], :end => [5, 5] })
|
31
|
+
assert_equal sheet_users, users
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_should_import_columns_on_associations_attributes_with_mapping_on_columns_names
|
35
|
+
sheet = File.new(SHEET_WITH_COLUMNS_FROM_ASSOCIATION)
|
36
|
+
mapping = {[:adress, :street] => 'Street', [:adress, :town] => 'Town', [:adress, :zip_code] => 'Zip Code'}
|
37
|
+
users = User.from_excel sheet, :mapping => mapping
|
38
|
+
|
39
|
+
assert_equal users_with_adresses, users
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_should_be_able_to_apply_rule_on_column_cells_to_transform_data_from_sheet_to_object_attribute_value
|
43
|
+
sheet = File.new(SHEET_WITH_COLUMN_VALUE_TO_ASSOCIATION_ID)
|
44
|
+
mapping = {[:role, :id] => 'Rôle'}
|
45
|
+
rules = {[:role, :id] => rule_to_retrieve_id}
|
46
|
+
users = User.from_excel sheet, :mapping => mapping, :rules => rules
|
47
|
+
|
48
|
+
assert_equal users_with_role, users
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def rule_to_retrieve_id
|
54
|
+
lambda do |cell_value|
|
55
|
+
case cell_value
|
56
|
+
when 'Admin' then 1
|
57
|
+
when 'Reader' then 2
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def sheet_users
|
63
|
+
[User.new(:first_name => 'Albert', :last_name => 'Einstein', :age => 131),
|
64
|
+
User.new(:first_name => 'Leonard', :last_name => 'De Vinci', :age => 558),
|
65
|
+
User.new(:first_name => 'Martin', :last_name => 'Heidegger', :age => 121)]
|
66
|
+
end
|
67
|
+
|
68
|
+
def users_with_adresses
|
69
|
+
adresses = [ Adress.new(:street => '17 rue de Brest', :town => 'Quimper', :zip_code => 29000),
|
70
|
+
Adress.new(:street => '26 rue des Lilas', :town => 'Paris', :zip_code => 75017),
|
71
|
+
Adress.new(:street => '13 rue du port', :town => 'Vannes', :zip_code => 56000)]
|
72
|
+
users = sheet_users
|
73
|
+
users.each_with_index {|user, index| user.adress = adresses[index]}
|
74
|
+
users
|
75
|
+
end
|
76
|
+
|
77
|
+
def users_with_role
|
78
|
+
[ User.new(:first_name => 'Bobby', :last_name => 'Lapointe', :role => Role.new(:id => 1)),
|
79
|
+
User.new(:first_name => 'Gaston', :last_name => 'Lagaffe', :role => Role.new(:id => 2)) ]
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestMapping < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_mapping_should_make_default_attribute_name_from_columns_title
|
6
|
+
sheet = stubbed_sheet
|
7
|
+
bounds = ExcelImport::Bounds.new(sheet)
|
8
|
+
mapping = ExcelImport::Mapping.new(sheet, {}, bounds )
|
9
|
+
|
10
|
+
assert_equal :first_name, mapping.attribute('First Name')
|
11
|
+
assert_equal :last_name, mapping.attribute('Last Name')
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_mapping_should__attribute_name
|
15
|
+
sheet = stubbed_sheet
|
16
|
+
bounds = ExcelImport::Bounds.new(sheet)
|
17
|
+
mapping = ExcelImport::Mapping.new(sheet, {:name => 'First Name'}, bounds )
|
18
|
+
|
19
|
+
assert_equal :name, mapping.attribute(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_should_handle_association_attributes
|
23
|
+
sheet = stubbed_sheet_with_adress_columns
|
24
|
+
mapping = {[:adress, :street] => 'Street', [:adress, :town] => 'Town'}
|
25
|
+
bounds = ExcelImport::Bounds.new(sheet)
|
26
|
+
mapping = ExcelImport::Mapping.new(sheet, mapping, bounds )
|
27
|
+
|
28
|
+
assert_equal [:adress, :street], mapping.attribute(4)
|
29
|
+
assert_equal [:adress, :town], mapping.attribute(5)
|
30
|
+
assert_equal 5, mapping.map[[:adress, :town]]
|
31
|
+
assert_equal ({:adress => Adress}), mapping.has_one_associations
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def stubbed_sheet
|
38
|
+
sheet = stub(:first_column => 1, :last_column => 3, :first_row => 1, :last_row => 2)
|
39
|
+
sheet.stubs(:cell).with(1,1).returns('First Name')
|
40
|
+
sheet.stubs(:cell).with(1,2).returns('Last Name')
|
41
|
+
sheet.stubs(:cell).with(1,3).returns('Age')
|
42
|
+
sheet
|
43
|
+
end
|
44
|
+
|
45
|
+
def stubbed_sheet_with_adress_columns
|
46
|
+
sheet = stub(:first_column => 1, :last_column => 5, :first_row => 1, :last_row => 2)
|
47
|
+
sheet.stubs(:cell).with(1,1).returns('First Name')
|
48
|
+
sheet.stubs(:cell).with(1,2).returns('Last Name')
|
49
|
+
sheet.stubs(:cell).with(1,3).returns('Age')
|
50
|
+
sheet.stubs(:cell).with(1,4).returns('Street')
|
51
|
+
sheet.stubs(:cell).with(1,5).returns('Town')
|
52
|
+
sheet
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: from_excel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Philippe Cantin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-11-08 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Import Excel spreadsheet rows to ruby objects
|
23
|
+
email:
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.rdoc
|
30
|
+
files:
|
31
|
+
- lib/excel_import.rb
|
32
|
+
- lib/from_excel.rb
|
33
|
+
- README.rdoc
|
34
|
+
- test/helper.rb
|
35
|
+
- test/model/adress.rb
|
36
|
+
- test/model/role.rb
|
37
|
+
- test/model/user.rb
|
38
|
+
- test/test_bounds.rb
|
39
|
+
- test/test_from_excel.rb
|
40
|
+
- test/test_mapping.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/anoiaque/from_excel
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.7
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Import Excel spreadsheet rows to ruby objects
|
75
|
+
test_files:
|
76
|
+
- test/helper.rb
|
77
|
+
- test/model/adress.rb
|
78
|
+
- test/model/role.rb
|
79
|
+
- test/model/user.rb
|
80
|
+
- test/test_bounds.rb
|
81
|
+
- test/test_from_excel.rb
|
82
|
+
- test/test_mapping.rb
|