abstract_importer 1.1.0 → 1.2.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/concordia-publishing-house/abstract_importer.png?branch=master)](https://travis-ci.org/concordia-publishing-house/abstract_importer)
|
4
|
-
|
5
4
|
[![Code Climate](https://codeclimate.com/github/concordia-publishing-house/abstract_importer.png)](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
|