abstract_importer 1.1.0 → 1.2.0.rc1
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.
- checksums.yaml +4 -4
- data/README.md +18 -11
- data/lib/abstract_importer/base.rb +35 -3
- data/lib/abstract_importer/collection_importer.rb +44 -25
- data/lib/abstract_importer/import_options.rb +8 -3
- data/lib/abstract_importer/reporter.rb +3 -2
- data/lib/abstract_importer/summary.rb +7 -2
- data/lib/abstract_importer/version.rb +1 -1
- data/test/callback_test.rb +33 -2
- data/test/importer_test.rb +57 -1
- data/test/support/mock_data_source.rb +38 -14
- data/test/support/mock_objects.rb +11 -0
- data/test/support/schema.rb +14 -0
- data/test/test_helper.rb +5 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df7b7a0811459f30502cf09d03741312919b9f54
|
4
|
+
data.tar.gz: 116840924377d0562121169b715b9fd64f49fde1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9c6b45e74533ba5a1ef07432d677834057649b66656c07c61594ea34e91bb449e5346167fb69cad7d7c464a1c48899e420f8f73a051a22b731afeecfab1db49
|
7
|
+
data.tar.gz: b64e4b346bbc2f566c5686b95edde07f3f49f86be9a971916ac870a3d583038ef31da07cad2ed4e7d58f9f976c6f4de92343dfd93c7449841523cafd1488176d
|
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# AbstractImporter
|
2
2
|
|
3
3
|
[](https://travis-ci.org/concordia-publishing-house/abstract_importer)
|
4
|
-
|
5
4
|
[](https://codeclimate.com/github/concordia-publishing-house/abstract_importer)
|
6
5
|
|
7
6
|
AbstractImporter provides services for importing complex data from an arbitrary data source. It:
|
@@ -54,7 +53,7 @@ end
|
|
54
53
|
`MyImporter`'s initializer takes two arguments: `parent` and `data_source`:
|
55
54
|
|
56
55
|
* `parent` is any object that will respond to the names of your collections with an `ActiveRecord::Relation`.
|
57
|
-
* `data_source` is any object that will respond to the names of your collections
|
56
|
+
* `data_source` is any object that will respond to the names of your collections with an `Enumerator`.
|
58
57
|
|
59
58
|
Here are reasonable classes for `parent` and `data_source`:
|
60
59
|
|
@@ -70,15 +69,19 @@ end
|
|
70
69
|
# data source
|
71
70
|
class Database
|
72
71
|
def students
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
72
|
+
Enumerator.new do |e|
|
73
|
+
e.yield id: 457, name: "Ron"
|
74
|
+
e.yield id: 458, name: "Ginny"
|
75
|
+
e.yield id: 459, name: "Fred"
|
76
|
+
e.yield id: 460, name: "George"
|
77
|
+
end
|
77
78
|
end
|
78
79
|
|
79
80
|
def parents
|
80
|
-
|
81
|
-
|
81
|
+
Enumerator.new do |e|
|
82
|
+
e.yield id: 88, name: "Arthur"
|
83
|
+
e.yield id: 89, name: "Molly"
|
84
|
+
end
|
82
85
|
end
|
83
86
|
end
|
84
87
|
```
|
@@ -118,7 +121,7 @@ class MyImporter < AbstractImporter::Base
|
|
118
121
|
import.students do |options|
|
119
122
|
options.finder :find_student
|
120
123
|
options.before_build { |attrs| attrs.merge(name: attrs[:name].capitalize) }
|
121
|
-
options.
|
124
|
+
options.after_all :students_completed
|
122
125
|
end
|
123
126
|
import.parents
|
124
127
|
end
|
@@ -156,9 +159,13 @@ The complete list of callbacks is below.
|
|
156
159
|
|
157
160
|
`after_create` is called with the original hash of attributes and the newly-saved record right after it is successfully saved.
|
158
161
|
|
159
|
-
#####
|
162
|
+
##### before_all
|
163
|
+
|
164
|
+
`before_all` is called just before the records in a collection are been processed.
|
165
|
+
|
166
|
+
##### after_all
|
160
167
|
|
161
|
-
`
|
168
|
+
`after_all` is called when all of the records in a collection have been processed.
|
162
169
|
|
163
170
|
|
164
171
|
|
@@ -15,7 +15,11 @@ module AbstractImporter
|
|
15
15
|
yield @import_plan = ImportPlan.new
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
def depends_on(*dependencies)
|
19
|
+
@dependencies = dependencies
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :import_plan, :dependencies
|
19
23
|
end
|
20
24
|
|
21
25
|
|
@@ -31,11 +35,16 @@ module AbstractImporter
|
|
31
35
|
@id_map = IdMap.new
|
32
36
|
@results = {}
|
33
37
|
@import_plan = self.class.import_plan.to_h
|
38
|
+
@atomic = options.fetch(:atomic, false)
|
34
39
|
@collections = []
|
35
40
|
end
|
36
41
|
|
37
42
|
attr_reader :source, :parent, :reporter, :id_map, :results
|
38
43
|
|
44
|
+
def atomic?
|
45
|
+
@atomic
|
46
|
+
end
|
47
|
+
|
39
48
|
def dry_run?
|
40
49
|
@dry_run
|
41
50
|
end
|
@@ -53,7 +62,9 @@ module AbstractImporter
|
|
53
62
|
reporter.finish_setup(ms)
|
54
63
|
|
55
64
|
ms = Benchmark.ms do
|
56
|
-
|
65
|
+
with_transaction do
|
66
|
+
collections.each &method(:import_collection)
|
67
|
+
end
|
57
68
|
end
|
58
69
|
|
59
70
|
teardown
|
@@ -142,8 +153,19 @@ module AbstractImporter
|
|
142
153
|
end
|
143
154
|
end
|
144
155
|
|
156
|
+
def dependencies
|
157
|
+
@dependencies ||= Array(self.class.dependencies).map do |name|
|
158
|
+
reflection = parent.class.reflect_on_association(name)
|
159
|
+
model = reflection.klass
|
160
|
+
table_name = model.table_name
|
161
|
+
scope = parent.public_send(name)
|
162
|
+
|
163
|
+
Collection.new(name, model, table_name, scope, nil)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
145
167
|
def prepopulate_id_map!
|
146
|
-
collections.each do |collection|
|
168
|
+
(collections + dependencies).each do |collection|
|
147
169
|
query = collection.scope.where("#{collection.table_name}.legacy_id IS NOT NULL")
|
148
170
|
map = values_of(query, :id, :legacy_id) \
|
149
171
|
.each_with_object({}) { |(id, legacy_id), map| map[legacy_id] = id }
|
@@ -167,5 +189,15 @@ module AbstractImporter
|
|
167
189
|
reporter.count_notice "#{plural}.#{foreign_key} will be nil: a #{depends_on.to_s.singularize} with the legacy id #{legacy_id} was not mapped."
|
168
190
|
end
|
169
191
|
|
192
|
+
|
193
|
+
|
194
|
+
def with_transaction(&block)
|
195
|
+
if atomic?
|
196
|
+
ActiveRecord::Base.transaction(requires_new: true, &block)
|
197
|
+
else
|
198
|
+
block.call
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
170
202
|
end
|
171
203
|
end
|
@@ -30,11 +30,12 @@ module AbstractImporter
|
|
30
30
|
reporter.start_collection(self)
|
31
31
|
prepare!
|
32
32
|
|
33
|
+
invoke_callback(:before_all)
|
33
34
|
summary.ms = Benchmark.ms do
|
34
35
|
each_new_record &method(:process_record)
|
35
36
|
end
|
37
|
+
invoke_callback(:after_all)
|
36
38
|
|
37
|
-
invoke_callback(:on_complete)
|
38
39
|
reporter.finish_collection(self, summary)
|
39
40
|
summary
|
40
41
|
end
|
@@ -54,43 +55,56 @@ module AbstractImporter
|
|
54
55
|
# has foreign keys that refer to another
|
55
56
|
next unless association.macro == :belongs_to
|
56
57
|
|
57
|
-
# We don't at this time support polymorphic associations
|
58
|
-
# which would require extending id_map to take the foreign
|
59
|
-
# type fields into account.
|
60
|
-
#
|
61
|
-
# Rails can't return `association.table_name` so easily
|
62
|
-
# because `table_name` comes from `klass` and `klass`
|
63
|
-
# isn't predetermined.
|
64
|
-
next if association.options[:polymorphic]
|
65
|
-
|
66
|
-
depends_on = association.table_name.to_sym
|
67
|
-
foreign_key = association.foreign_key.to_sym
|
68
|
-
|
69
58
|
# We support skipping some mappings entirely. I believe
|
70
59
|
# this is largely to cut down on verbosity in the log
|
71
60
|
# files and should be refactored to another place in time.
|
61
|
+
foreign_key = association.foreign_key.to_sym
|
72
62
|
next unless remap_foreign_key?(name, foreign_key)
|
73
63
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
|
79
|
-
end
|
64
|
+
if association.options[:polymorphic]
|
65
|
+
mappings << prepare_polymorphic_mapping!(association)
|
66
|
+
else
|
67
|
+
mappings << prepare_mapping!(association)
|
80
68
|
end
|
81
69
|
end
|
82
70
|
mappings
|
83
71
|
end
|
84
72
|
|
73
|
+
def prepare_mapping!(association)
|
74
|
+
depends_on = association.table_name.to_sym
|
75
|
+
foreign_key = association.foreign_key.to_sym
|
76
|
+
|
77
|
+
Proc.new do |attrs|
|
78
|
+
if attrs.key?(foreign_key)
|
79
|
+
attrs[foreign_key] = map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
|
80
|
+
else
|
81
|
+
reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def prepare_polymorphic_mapping!(association)
|
87
|
+
foreign_key = association.foreign_key.to_sym
|
88
|
+
foreign_type = association.foreign_key.gsub(/_id$/, "_type").to_sym
|
89
|
+
|
90
|
+
Proc.new do |attrs|
|
91
|
+
if attrs.key?(foreign_key) && attrs.key?(foreign_type)
|
92
|
+
foreign_model = attrs[foreign_type]
|
93
|
+
depends_on = foreign_model.tableize.to_sym if foreign_model
|
94
|
+
attrs[foreign_key] = depends_on && map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
|
95
|
+
else
|
96
|
+
reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
85
101
|
|
86
102
|
|
87
103
|
|
88
104
|
|
89
105
|
def each_new_record
|
90
|
-
source.public_send(name) do |
|
91
|
-
|
92
|
-
yield hash.dup
|
93
|
-
end
|
106
|
+
source.public_send(name).each do |attrs|
|
107
|
+
yield attrs.dup
|
94
108
|
end
|
95
109
|
end
|
96
110
|
|
@@ -114,6 +128,8 @@ module AbstractImporter
|
|
114
128
|
else
|
115
129
|
summary.invalid += 1
|
116
130
|
end
|
131
|
+
rescue ::AbstractImporter::Skip
|
132
|
+
summary.skipped += 1
|
117
133
|
end
|
118
134
|
|
119
135
|
|
@@ -154,7 +170,7 @@ module AbstractImporter
|
|
154
170
|
# rescue_callback has one shot to fix things
|
155
171
|
invoke_callback(:rescue, record) unless record.valid?
|
156
172
|
|
157
|
-
if record.save
|
173
|
+
if record.valid? && record.save
|
158
174
|
invoke_callback(:after_create, hash, record)
|
159
175
|
id_map << record
|
160
176
|
|
@@ -162,7 +178,7 @@ module AbstractImporter
|
|
162
178
|
true
|
163
179
|
else
|
164
180
|
|
165
|
-
reporter.record_failed(record)
|
181
|
+
reporter.record_failed(record, hash)
|
166
182
|
false
|
167
183
|
end
|
168
184
|
end
|
@@ -186,4 +202,7 @@ module AbstractImporter
|
|
186
202
|
end
|
187
203
|
|
188
204
|
end
|
205
|
+
|
206
|
+
class Skip < StandardError; end
|
207
|
+
|
189
208
|
end
|
@@ -5,7 +5,8 @@ module AbstractImporter
|
|
5
5
|
:before_build_callback,
|
6
6
|
:before_create_callback,
|
7
7
|
:after_create_callback,
|
8
|
-
:
|
8
|
+
:before_all_callback,
|
9
|
+
:after_all_callback
|
9
10
|
|
10
11
|
def finder(sym=nil, &block)
|
11
12
|
@finder_callback = sym || block
|
@@ -27,8 +28,12 @@ module AbstractImporter
|
|
27
28
|
@rescue_callback = sym || block
|
28
29
|
end
|
29
30
|
|
30
|
-
def
|
31
|
-
@
|
31
|
+
def before_all(sym=nil, &block)
|
32
|
+
@before_all_callback = sym || block
|
33
|
+
end
|
34
|
+
|
35
|
+
def after_all(sym=nil, &block)
|
36
|
+
@after_all_callback = sym || block
|
32
37
|
end
|
33
38
|
|
34
39
|
end
|
@@ -54,7 +54,7 @@ module AbstractImporter
|
|
54
54
|
io.print "." unless production?
|
55
55
|
end
|
56
56
|
|
57
|
-
def record_failed(record)
|
57
|
+
def record_failed(record, hash)
|
58
58
|
io.print "×" unless production?
|
59
59
|
|
60
60
|
error_messages = invalid_params[record.class.name] ||= {}
|
@@ -111,8 +111,9 @@ module AbstractImporter
|
|
111
111
|
stat "#{summary.already_imported} #{plural} were imported previously"
|
112
112
|
stat "#{summary.redundant} #{plural} would create duplicates and will not be imported"
|
113
113
|
stat "#{summary.invalid} #{plural} were invalid"
|
114
|
+
stat "#{summary.skipped} #{plural} were skipped"
|
114
115
|
stat "#{summary.created} #{plural} were imported"
|
115
|
-
stat "#{distance_of_time(summary.ms)} elapsed (#{
|
116
|
+
stat "#{distance_of_time(summary.ms)} elapsed (#{summary.average_ms.to_i}ms each)"
|
116
117
|
else
|
117
118
|
stat "#{distance_of_time(summary.ms)} elapsed"
|
118
119
|
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
module AbstractImporter
|
2
|
-
class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms)
|
2
|
+
class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms, :skipped)
|
3
3
|
|
4
4
|
def initialize
|
5
|
-
super(0,0,0,0,0,0)
|
5
|
+
super(0,0,0,0,0,0,0)
|
6
|
+
end
|
7
|
+
|
8
|
+
def average_ms
|
9
|
+
return nil if total == 0
|
10
|
+
ms / total
|
6
11
|
end
|
7
12
|
|
8
13
|
end
|
data/test/callback_test.rb
CHANGED
@@ -18,6 +18,20 @@ class CallbackTest < ActiveSupport::TestCase
|
|
18
18
|
import!
|
19
19
|
assert_equal ["Harry", "Ron", "Hermione"], account.students.pluck(:name)
|
20
20
|
end
|
21
|
+
|
22
|
+
should "allow you to skip certain records" do
|
23
|
+
plan do |import|
|
24
|
+
import.students do |options|
|
25
|
+
options.before_build do |attrs|
|
26
|
+
raise AbstractImporter::Skip if attrs[:name] == "Harry Potter"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
import!
|
32
|
+
assert_equal ["Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
|
33
|
+
assert_equal 1, results[:students].skipped
|
34
|
+
end
|
21
35
|
end
|
22
36
|
|
23
37
|
|
@@ -75,11 +89,28 @@ class CallbackTest < ActiveSupport::TestCase
|
|
75
89
|
|
76
90
|
|
77
91
|
|
78
|
-
context "
|
92
|
+
context "before_all" do
|
93
|
+
setup do
|
94
|
+
plan do |import|
|
95
|
+
import.students do |options|
|
96
|
+
options.before_all :callback
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
should "should be invoked before the collection has been imported" do
|
102
|
+
mock(importer).callback.once
|
103
|
+
import!
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
context "after_all" do
|
79
110
|
setup do
|
80
111
|
plan do |import|
|
81
112
|
import.students do |options|
|
82
|
-
options.
|
113
|
+
options.after_all :callback
|
83
114
|
end
|
84
115
|
end
|
85
116
|
end
|
data/test/importer_test.rb
CHANGED
@@ -45,7 +45,44 @@ class ImporterTest < ActiveSupport::TestCase
|
|
45
45
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
46
46
|
end
|
47
47
|
|
48
|
-
should "preserve mappings
|
48
|
+
should "preserve mappings even when a record was previously imported" do
|
49
|
+
harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
|
50
|
+
import!
|
51
|
+
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when {atomic: true}" do
|
55
|
+
should "rollback the whole import if an part fails" do
|
56
|
+
mock(importer).atomic? { true }
|
57
|
+
mock.instance_of(Parent).save { raise "hell" }
|
58
|
+
import! rescue
|
59
|
+
assert_equal 0, account.parents.count, "No parents should have been imported with the exception"
|
60
|
+
assert_equal 0, account.students.count, "Expected students to have been rolled back"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when {atomic: false}" do
|
65
|
+
should "not rollback the whole import if an part fails" do
|
66
|
+
mock(importer).atomic? { false }
|
67
|
+
mock.instance_of(Parent).save { raise "hell" }
|
68
|
+
import! rescue
|
69
|
+
assert_equal 0, account.parents.count, "No parents should have been imported with the exception"
|
70
|
+
assert_equal 3, account.students.count, "Expected students not to have been rolled back"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
context "with a dependency" do
|
78
|
+
setup do
|
79
|
+
depends_on :students
|
80
|
+
plan do |import|
|
81
|
+
import.parents
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
should "preserve mappings when a dependency was imported by another importer" do
|
49
86
|
harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
|
50
87
|
import!
|
51
88
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
@@ -103,4 +140,23 @@ class ImporterTest < ActiveSupport::TestCase
|
|
103
140
|
|
104
141
|
|
105
142
|
|
143
|
+
context "with polymorphic associations" do
|
144
|
+
setup do
|
145
|
+
plan do |import|
|
146
|
+
import.cats
|
147
|
+
import.owls
|
148
|
+
import.students
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
should "preserve mappings" do
|
153
|
+
import!
|
154
|
+
assert_equal 2, account.students.map(&:pet).compact.count, "Expected two students to still be linked to their pets upon import"
|
155
|
+
assert_kind_of Owl, account.students.find_by_name("Harry Potter").pet, "Expected Harry's pet to be an Owl"
|
156
|
+
assert_kind_of Cat, account.students.find_by_name("Hermione Granger").pet, "Expected Harry's pet to be a Cat"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
|
106
162
|
end
|
@@ -1,32 +1,56 @@
|
|
1
1
|
class MockDataSource
|
2
2
|
|
3
|
+
|
3
4
|
def students
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
Enumerator.new do |e|
|
6
|
+
e.yield id: 456, name: "Harry Potter", pet_type: "Owl", pet_id: 901
|
7
|
+
e.yield id: 457, name: "Ron Weasley", pet_type: nil, pet_id: nil
|
8
|
+
e.yield id: 458, name: "Hermione Granger", pet_type: "Cat", pet_id: 901
|
9
|
+
end
|
7
10
|
end
|
8
11
|
|
9
12
|
def parents
|
10
|
-
|
11
|
-
|
13
|
+
Enumerator.new do |e|
|
14
|
+
e.yield id: 88, name: "James Potter", student_id: 456
|
15
|
+
e.yield id: 89, name: "Lily Potter", student_id: 456
|
16
|
+
end
|
12
17
|
end
|
13
18
|
|
14
19
|
def locations
|
15
|
-
|
16
|
-
|
20
|
+
Enumerator.new do |e|
|
21
|
+
e.yield id: 5, slug: "godric's-hollow" # <-- invalid
|
22
|
+
e.yield id: 6, slug: "azkaban"
|
23
|
+
end
|
17
24
|
end
|
18
25
|
|
19
26
|
def subjects
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
27
|
+
Enumerator.new do |e|
|
28
|
+
e.yield id: 49, name: "Care of Magical Creatures", student_ids: [456]
|
29
|
+
e.yield id: 50, name: "Advanced Potions", student_ids: [456, 457]
|
30
|
+
e.yield id: 51, name: "History of Magic", student_ids: [457]
|
31
|
+
e.yield id: 52, name: "Arithmancy", student_ids: [458]
|
32
|
+
e.yield id: 53, name: "Study of Ancient Runes", student_ids: [458]
|
33
|
+
end
|
25
34
|
end
|
26
35
|
|
27
36
|
def grades
|
28
|
-
|
29
|
-
|
37
|
+
Enumerator.new do |e|
|
38
|
+
e.yield id: 500, subject_id: 50, student_id: 457, value: "Acceptable"
|
39
|
+
e.yield id: 501, subject_id: 51, student_id: 457, value: "Troll"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def cats
|
44
|
+
Enumerator.new do |e|
|
45
|
+
e.yield id: 901, name: "Crookshanks"
|
46
|
+
end
|
30
47
|
end
|
31
48
|
|
49
|
+
def owls
|
50
|
+
Enumerator.new do |e|
|
51
|
+
e.yield id: 901, name: "Hedwig"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
32
56
|
end
|
@@ -2,6 +2,7 @@ class Student < ActiveRecord::Base
|
|
2
2
|
has_and_belongs_to_many :subjects
|
3
3
|
has_many :grades
|
4
4
|
has_many :parents
|
5
|
+
belongs_to :pet, polymorphic: true
|
5
6
|
|
6
7
|
def report_card
|
7
8
|
subjects.map do |subject|
|
@@ -34,4 +35,14 @@ class Account < ActiveRecord::Base
|
|
34
35
|
has_many :subjects
|
35
36
|
has_many :grades
|
36
37
|
has_many :locations
|
38
|
+
has_many :cats
|
39
|
+
has_many :owls
|
40
|
+
end
|
41
|
+
|
42
|
+
class Cat < ActiveRecord::Base
|
43
|
+
has_one :student, as: :pet
|
44
|
+
end
|
45
|
+
|
46
|
+
class Owl < ActiveRecord::Base
|
47
|
+
has_one :student, as: :pet
|
37
48
|
end
|
data/test/support/schema.rb
CHANGED
@@ -8,6 +8,8 @@ ActiveRecord::Schema.define(:version => 1) do
|
|
8
8
|
t.integer "legacy_id"
|
9
9
|
t.string "name"
|
10
10
|
t.string "house"
|
11
|
+
t.string "pet_type"
|
12
|
+
t.integer "pet_id"
|
11
13
|
end
|
12
14
|
|
13
15
|
create_table "parents", :force => true do |t|
|
@@ -42,4 +44,16 @@ ActiveRecord::Schema.define(:version => 1) do
|
|
42
44
|
t.string "value"
|
43
45
|
end
|
44
46
|
|
47
|
+
create_table "owls", :force => true do |t|
|
48
|
+
t.integer "account_id"
|
49
|
+
t.integer "legacy_id"
|
50
|
+
t.string "name"
|
51
|
+
end
|
52
|
+
|
53
|
+
create_table "cats", :force => true do |t|
|
54
|
+
t.integer "account_id"
|
55
|
+
t.integer "legacy_id"
|
56
|
+
t.string "name"
|
57
|
+
end
|
58
|
+
|
45
59
|
end
|
data/test/test_helper.rb
CHANGED
@@ -50,12 +50,16 @@ class ActiveSupport::TestCase
|
|
50
50
|
|
51
51
|
protected
|
52
52
|
|
53
|
-
attr_reader :account, :results
|
53
|
+
attr_reader :account, :results, :data_source
|
54
54
|
|
55
55
|
def plan(&block)
|
56
56
|
@klass.import(&block)
|
57
57
|
end
|
58
58
|
|
59
|
+
def depends_on(*args)
|
60
|
+
@klass.depends_on(*args)
|
61
|
+
end
|
62
|
+
|
59
63
|
def import!
|
60
64
|
@results = importer.perform!
|
61
65
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abstract_importer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Lail
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -209,12 +209,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
209
209
|
version: '0'
|
210
210
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
211
211
|
requirements:
|
212
|
-
- - '
|
212
|
+
- - '>'
|
213
213
|
- !ruby/object:Gem::Version
|
214
|
-
version:
|
214
|
+
version: 1.3.1
|
215
215
|
requirements: []
|
216
216
|
rubyforge_project:
|
217
|
-
rubygems_version: 2.
|
217
|
+
rubygems_version: 2.2.1
|
218
218
|
signing_key:
|
219
219
|
specification_version: 4
|
220
220
|
summary: Provides services for the mass-import of complex relational data
|