drive_time 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +61 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +240 -0
- data/Rakefile +7 -0
- data/drive_time.gemspec +30 -0
- data/lib/drive_time/bi_directional_hash.rb +33 -0
- data/lib/drive_time/builders/join_builder.rb +39 -0
- data/lib/drive_time/builders/name_builder.rb +19 -0
- data/lib/drive_time/class_name_map.rb +41 -0
- data/lib/drive_time/converters/spreadsheets_converter.rb +125 -0
- data/lib/drive_time/converters/worksheet_converter.rb +262 -0
- data/lib/drive_time/field_expander.rb +73 -0
- data/lib/drive_time/loader.rb +100 -0
- data/lib/drive_time/logging.rb +19 -0
- data/lib/drive_time/model_store.rb +73 -0
- data/lib/drive_time/version.rb +3 -0
- data/lib/drive_time.rb +71 -0
- data/spec/drive_time/bi_directional_hash_spec.rb +54 -0
- data/spec/drive_time/builder/join_builder_spec.rb +48 -0
- data/spec/drive_time/builder/name_builder_spec.rb +26 -0
- data/spec/drive_time/class_name_map_spec.rb +77 -0
- data/spec/drive_time/converters/worksheet_converter_spec.rb +64 -0
- data/spec/drive_time/field_expander_spec.rb +130 -0
- data/spec/drive_time/loader_spec.rb +111 -0
- data/spec/drive_time/model_store_spec.rb +52 -0
- data/spec/drive_time_spec.rb +44 -0
- data/spec/fixtures/mapping.yml +55 -0
- data/spec/full_tests.rb +48 -0
- data/spec/spec_helper.rb +6 -0
- metadata +233 -0
data/.gitignore
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Also see Global .gitignore: ~/.gitignore_global
|
2
|
+
# https://github.com/stationkeeping/osx/blob/master/git/.gitignore_global
|
3
|
+
|
4
|
+
# Environmental Variables
|
5
|
+
#########################
|
6
|
+
|
7
|
+
.env
|
8
|
+
|
9
|
+
# Rails
|
10
|
+
#########################
|
11
|
+
|
12
|
+
*.rbc
|
13
|
+
*.sassc
|
14
|
+
.sass-cache
|
15
|
+
capybara-*.html
|
16
|
+
.rspec
|
17
|
+
.rvmrc
|
18
|
+
/.bundle
|
19
|
+
/vendor/bundle
|
20
|
+
/log/*
|
21
|
+
/tmp/*
|
22
|
+
/db/*.sqlite3
|
23
|
+
/public/system/*
|
24
|
+
/coverage/
|
25
|
+
/spec/tmp/*
|
26
|
+
**.orig
|
27
|
+
rerun.txt
|
28
|
+
pickle-email-*.html
|
29
|
+
.project
|
30
|
+
|
31
|
+
# Ruby
|
32
|
+
#########################
|
33
|
+
|
34
|
+
*.gem
|
35
|
+
*.rbc
|
36
|
+
.bundle
|
37
|
+
.config
|
38
|
+
coverage
|
39
|
+
InstalledFiles
|
40
|
+
lib/bundler/man
|
41
|
+
pkg
|
42
|
+
rdoc
|
43
|
+
spec/reports
|
44
|
+
test/tmp
|
45
|
+
test/version_tmp
|
46
|
+
tmp
|
47
|
+
|
48
|
+
# YARD artifacts
|
49
|
+
.yardoc
|
50
|
+
_yardoc
|
51
|
+
doc/
|
52
|
+
|
53
|
+
# Ruby Gems
|
54
|
+
#########################
|
55
|
+
|
56
|
+
Gemfile.lock
|
57
|
+
|
58
|
+
# Project Specific
|
59
|
+
#########################
|
60
|
+
|
61
|
+
spec/fixtures/cached/**
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Pedr Browne
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
# DriveTime
|
2
|
+
|
3
|
+
Drive Time allows you to transform a one or more Google Spredsheet into a Rails model graph.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'drive_time'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install drive_time
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Drive Time
|
22
|
+
|
23
|
+
Drive Time allows you to map a Google Spreadsheet to a your Rails database. Each worksheet represents a different Model class; its columns represent the model's attributes and each row represents a different instance. It is designed to make as many allowances for the person authoring the spreadsheet as possible so it can be used as a bridge between a non-technical user and yourself. Unless they are an idiot, in which case you're still screwed.
|
24
|
+
|
25
|
+
*I am using it sucessfully in two projects at the moment, however this is very much alpha and I am adding features as I need them. My main priority is to add more comprehansive tests.*
|
26
|
+
|
27
|
+
#### Installation
|
28
|
+
|
29
|
+
You know the drill. Add `gem drive_time` to your `Gemfile` and run `$ bundle`.
|
30
|
+
|
31
|
+
#### Prerequisits
|
32
|
+
|
33
|
+
Drive Time uses the [Google Drive](https://github.com/gimite/google-drive-ruby) gem, which handles the connection to Google Drive, the download and transforms the Spreadsheet and Worksheet into Ruby objects. It relies on the presence of two envs:
|
34
|
+
|
35
|
+
```
|
36
|
+
GOOGLE_USERNAME=your.email@gmail.com
|
37
|
+
GOOGLE_PASSWORD=YourPassword
|
38
|
+
```
|
39
|
+
|
40
|
+
You will also need a Rails project with a migrated datbase, ready to recieve the generated models.
|
41
|
+
|
42
|
+
#### Using Drive Time
|
43
|
+
|
44
|
+
Drive Time needs access to your Rails project. You can run it as part of the seeding process, or directly using `$ rails runner`.
|
45
|
+
|
46
|
+
An example of usage:
|
47
|
+
|
48
|
+
```
|
49
|
+
include DriveTime
|
50
|
+
|
51
|
+
mappings_path = File.join(File.dirname(__FILE__),'mappings.yml')
|
52
|
+
SpreadsheetsConverter.new.load mappings_path
|
53
|
+
```
|
54
|
+
|
55
|
+
The `mappings.yml` is crucial. It specifies the name of your spreadsheet and how you want to map your worksheets to your models.
|
56
|
+
|
57
|
+
##### Mappings
|
58
|
+
|
59
|
+
A bare-bones mapping might look like this:
|
60
|
+
|
61
|
+
```
|
62
|
+
spreadsheets:
|
63
|
+
- title: Business
|
64
|
+
worksheets:
|
65
|
+
- title: Company
|
66
|
+
key: name
|
67
|
+
fields:
|
68
|
+
- name: name
|
69
|
+
- name: description
|
70
|
+
associations:
|
71
|
+
- name: learning_group
|
72
|
+
```
|
73
|
+
|
74
|
+
Drive Time will look for a spreadsheet called 'Business' and download it. It will then find a spreadsheet within called 'Company' and run through each row, generating a new model of class `Company` with attributes of `name` and `description`.
|
75
|
+
|
76
|
+
The `key` is a unique identifier which is used internally to identify the models and is used within the spreadsheets to declare associations. In this case we are declaring that we want to use the name field as the key. The crucial thing is that the key is unique amongst all models of that type. If no attributes are guarenteed to be unique, one option is to add an explicit `key` column, containing a unique identifier to the spreadsheet, however this is unwealdy and easy to lose track of. A better option is to use multiple attributes to generate the key. This can be done using a builder:
|
77
|
+
|
78
|
+
```
|
79
|
+
worksheets:
|
80
|
+
- title: Company
|
81
|
+
key:
|
82
|
+
builder: join
|
83
|
+
from_fields: [name, city]
|
84
|
+
```
|
85
|
+
|
86
|
+
This will result in a key made from the company name and city combined. This is great for giving models of the same type a unique key, but requires more thought if the id will be used by other models to declare associations.
|
87
|
+
|
88
|
+
*Note: Internally, keys are just downcased, underscored and whitespace-stripped versions of whater value is ussed for the key.*
|
89
|
+
|
90
|
+
*All fields values are run through a markdown parser before they are added to the model.*
|
91
|
+
|
92
|
+
##### Associations
|
93
|
+
|
94
|
+
Below an association is declared between the `Company` and the `Product`. It is declared as a singular relationship using the `singular` mapping attribute.
|
95
|
+
|
96
|
+
Drive Time assumes that if a worksheet declares a singular attribute mapping, it will contain a column named after the model. This field should contain the key of the instance of the model that it will use to satisfy its dependency. In the example below it would contain the sku of the `Product` model.
|
97
|
+
|
98
|
+
```
|
99
|
+
spreadsheets:
|
100
|
+
- title: Business
|
101
|
+
worksheets:
|
102
|
+
- title: Company
|
103
|
+
key: name
|
104
|
+
fields:
|
105
|
+
- name: name
|
106
|
+
- name: description
|
107
|
+
associations:
|
108
|
+
- name: Product
|
109
|
+
- singular: true
|
110
|
+
- title: Product
|
111
|
+
key: sku
|
112
|
+
fields:
|
113
|
+
- name: title
|
114
|
+
- name: sku
|
115
|
+
- name: price
|
116
|
+
```
|
117
|
+
|
118
|
+
One-to-many relationships can be declared in two ways.
|
119
|
+
|
120
|
+
If there are only a few possible options, a column can be assigned to each option and named after the option. For example if a company had one or more Regions from either Europe, or Asia or America, and we were using the name field for the Region key, we could add three columns to the Company spreadsheet named 'Europe', 'Asia', and 'America'. If we want to add that region we would add a value of 'Yes' to the field:
|
121
|
+
|
122
|
+
```
|
123
|
+
associations:
|
124
|
+
- name: region
|
125
|
+
builder: use_fields
|
126
|
+
field_names: [europe, asia, america]
|
127
|
+
```
|
128
|
+
|
129
|
+
Another option, appropriate for situations where there are many possible values, is to declare an inverted relationship. for example, if you had a 'Team' that had many 'Players', rather than declaring the team's players in the team Spreadsheet, you could declare each Player's team in a column in the Player Spreadsheet. For this to work, you must declare the relationship inverse. So in the Player worksheet mapping:
|
130
|
+
|
131
|
+
```
|
132
|
+
associations:
|
133
|
+
- name: team
|
134
|
+
inverse: true
|
135
|
+
```
|
136
|
+
|
137
|
+
A final option is to add a comma separated list of keys and use the 'join' builder. A model wil be added for each key:
|
138
|
+
|
139
|
+
```
|
140
|
+
associations:
|
141
|
+
- name: member
|
142
|
+
builder: multi
|
143
|
+
```
|
144
|
+
|
145
|
+
A polymorphic association can be declared using:
|
146
|
+
|
147
|
+
```
|
148
|
+
associations:
|
149
|
+
- name: team
|
150
|
+
polymorphic:
|
151
|
+
association: contactable
|
152
|
+
```
|
153
|
+
|
154
|
+
It is OK to mix 'polymorphic' and 'singular' for the same association.
|
155
|
+
|
156
|
+
Using Google Spreadsheet's data validations can make things much easier on the Spreadsheet end. You can effectively add dropdowns containing values from a fixed list or from another Worksheet column, making sure that only valid values can be added.
|
157
|
+
|
158
|
+
##### Caching
|
159
|
+
|
160
|
+
By setting an env called `CACHED_DIR` to a directory path, Drive Time will store downloaded spreadsheets there and use them on subsequent occasions. Just delete the contents of the folder or remove the env to reload new versions the next time it runs.
|
161
|
+
|
162
|
+
|
163
|
+
```
|
164
|
+
CACHED_DIR=/Users/me/path/to/cached/directory
|
165
|
+
```
|
166
|
+
|
167
|
+
##### File Expansion and Text Docs
|
168
|
+
|
169
|
+
If Drive Time encounters a value surrounded by `{{` and `}}` within a database field, it will use this value to load another file from Google Drive and use its content in the place of the field value. Possible values are `expand_spreadsheet` and `expand_file` which can be used to load the content of a spreadsheet or a `.txt` file respectively:
|
170
|
+
|
171
|
+
```
|
172
|
+
{{expand_spreadsheet}}
|
173
|
+
```
|
174
|
+
|
175
|
+
And:
|
176
|
+
|
177
|
+
```
|
178
|
+
{{expand_file}}
|
179
|
+
```
|
180
|
+
|
181
|
+
In both cases, Drive Time will try to load a file named identically to the key for the model on which the field will be added. To specify a different filename, add it in hard brackets and without an extension:
|
182
|
+
|
183
|
+
```
|
184
|
+
{{expand_spreadsheet[some_spreadsheet_file_name]}}
|
185
|
+
```
|
186
|
+
|
187
|
+
And:
|
188
|
+
|
189
|
+
```
|
190
|
+
{{expand_spreadsheet[some_txt_file_name]}}
|
191
|
+
```
|
192
|
+
|
193
|
+
In the case of the text file, its contents will simply be added. In the case of a spreadsheet, it will be converted to a JSON object which will be used as the field value.
|
194
|
+
|
195
|
+
##### Debugging
|
196
|
+
|
197
|
+
By default, Drive Time outputs a minimal set of messages. To enable a much more verbose output, set the log level to DEBUG:
|
198
|
+
|
199
|
+
```
|
200
|
+
require "log4r"
|
201
|
+
DriveTime::log_level = Log4r::DEBUG
|
202
|
+
```
|
203
|
+
|
204
|
+
##### Other features
|
205
|
+
|
206
|
+
If you want to map a database field to a differently named field on a model you can use the following:
|
207
|
+
|
208
|
+
```
|
209
|
+
fields:
|
210
|
+
- name: name
|
211
|
+
map_to: title
|
212
|
+
```
|
213
|
+
|
214
|
+
This will map a Worksheet field named 'name' to a model attribute named 'title'.
|
215
|
+
|
216
|
+
You can also map the Worksheet title to a differently named model:
|
217
|
+
|
218
|
+
```
|
219
|
+
worksheets:
|
220
|
+
- title: Staff
|
221
|
+
map_to_class: Employee
|
222
|
+
```
|
223
|
+
|
224
|
+
This will use the Worksheet named 'Staff' to a model called 'Employee'.
|
225
|
+
|
226
|
+
It is sometimes useful to generate a UID for models to use in linked to an image or icon resource. You can map the model's key to a model attribute using:
|
227
|
+
|
228
|
+
```
|
229
|
+
- title: Example
|
230
|
+
key: title
|
231
|
+
key_to: uid
|
232
|
+
```
|
233
|
+
|
234
|
+
## Contributing
|
235
|
+
|
236
|
+
1. Fork it
|
237
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
238
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
239
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
240
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/drive_time.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'drive_time/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "drive_time"
|
8
|
+
gem.version = DriveTime::VERSION
|
9
|
+
gem.authors = ["Pedr Browne"]
|
10
|
+
gem.email = ["pedr.browne@gmail.com"]
|
11
|
+
gem.description = %q{Convert Google Spreadsheets to Rails Models}
|
12
|
+
gem.summary = %q{Map Worksheets to Models and their columns to model attributes. Seed your database directly from Google Drive.}
|
13
|
+
gem.homepage = "https://github.com/stationkeeping/drive_time"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "log4r"
|
21
|
+
gem.add_dependency "deep_end"
|
22
|
+
gem.add_dependency "google_drive"
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rake'
|
25
|
+
gem.add_development_dependency 'rspec'
|
26
|
+
gem.add_development_dependency 'dotenv'
|
27
|
+
gem.add_development_dependency "activemodel", "3.2.13"
|
28
|
+
gem.add_development_dependency "activerecord", "3.2.13"
|
29
|
+
gem.add_development_dependency "activesupport", "3.2.13"
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module DriveTime
|
2
|
+
|
3
|
+
class BiDirectionalHash
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@key_to_value = {}
|
7
|
+
@value_to_key = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def insert(key, value)
|
11
|
+
@key_to_value[key] = value;
|
12
|
+
@value_to_key[value] = key;
|
13
|
+
end
|
14
|
+
|
15
|
+
def value_for_key(key)
|
16
|
+
return @key_to_value[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def key_for_value(value)
|
20
|
+
return @value_to_key[value]
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_key_for_value(value)
|
24
|
+
return !key_for_value(value).nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_value_for_key(key)
|
28
|
+
return !value_for_key(key).nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module DriveTime
|
2
|
+
|
3
|
+
# Take a series of name fields. Look up their values and assemble them into a single id
|
4
|
+
# For example it might build a name from a model's title and its amount
|
5
|
+
class JoinBuilder
|
6
|
+
|
7
|
+
class MissingFieldError < StandardError; end
|
8
|
+
class NoFieldsError < StandardError; end
|
9
|
+
|
10
|
+
# Fields to use for names
|
11
|
+
def build(field_keys, row_map)
|
12
|
+
values = []
|
13
|
+
|
14
|
+
field_keys.each do |field_key|
|
15
|
+
raise MissingFieldError, "No field for key #{field_key}" if !row_map.has_key? field_key
|
16
|
+
values << DriveTime.underscore_from_text(row_map[field_key]) unless row_map[field_key].empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
raise NoFieldsError, 'No fields matched' if values.empty?
|
20
|
+
|
21
|
+
values.each_with_index do |value, index|
|
22
|
+
|
23
|
+
result = self.process_value value
|
24
|
+
if result
|
25
|
+
values[index] = result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
values.join('_').downcase
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def process_value(value)
|
34
|
+
return value
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DriveTime
|
2
|
+
|
3
|
+
# Take a series of name fields. Look up their values and assemble them into a single id
|
4
|
+
class NameBuilder < JoinBuilder
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def process_value(value)
|
9
|
+
|
10
|
+
# Add a period to initials
|
11
|
+
if value.length == 1
|
12
|
+
return "#{value}."
|
13
|
+
end
|
14
|
+
return value
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module DriveTime
|
2
|
+
|
3
|
+
# This is a fluid two-way map. In both directions it will return a mapping if it exists, otherwise
|
4
|
+
# it will return the value passed to it. It does not raise an exception if a mapping is not found.
|
5
|
+
class ClassNameMap
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@map = BiDirectionalHash.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# Check for mapped class
|
12
|
+
def resolve_original_from_mapped(className)
|
13
|
+
if @map.has_key_for_value className
|
14
|
+
@map.key_for_value className
|
15
|
+
else
|
16
|
+
className
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def resolve_mapped_from_original(className)
|
21
|
+
if @map.has_value_for_key className
|
22
|
+
@map.value_for_key className
|
23
|
+
else
|
24
|
+
className
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Accepts String versions of class names eg. ExampleClass
|
29
|
+
def save_mapping(class_name, mapped_class_name=nil)
|
30
|
+
if mapped_class_name
|
31
|
+
# Save mapping so we can look it up from both directions
|
32
|
+
@map.insert(class_name, mapped_class_name)
|
33
|
+
mapped_class_name
|
34
|
+
else
|
35
|
+
class_name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module DriveTime
|
2
|
+
|
3
|
+
class SpreadsheetsConverter
|
4
|
+
|
5
|
+
def initialize()
|
6
|
+
@dependency_graph = DeepEnd::Graph.new
|
7
|
+
@loader = DriveTime::Loader.new
|
8
|
+
@model_store = ModelStore.new(DriveTime::log_level)
|
9
|
+
@class_name_map = ClassNameMap.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Load mappings YML file
|
13
|
+
def load(mappings_path)
|
14
|
+
@mappings = ActiveSupport::HashWithIndifferentAccess.new(YAML::load File.open(mappings_path))
|
15
|
+
@namespace = @mappings[:namespace]
|
16
|
+
spreadsheets = download_spreadsheets
|
17
|
+
|
18
|
+
worksheets = []
|
19
|
+
spreadsheets.each do |spreadsheet|
|
20
|
+
# Create a map containing any class mappings
|
21
|
+
build_class_map spreadsheet
|
22
|
+
# Download and combine worksheets into single Array
|
23
|
+
worksheets.concat download_worksheets(spreadsheet)
|
24
|
+
end
|
25
|
+
|
26
|
+
worksheets = order_worksheets_by_dependencies( worksheets )
|
27
|
+
|
28
|
+
# Convert the worksheets
|
29
|
+
worksheets.each do |worksheet|
|
30
|
+
WorksheetConverter.new(@model_store, @class_name_map, @loader, @namespace).convert(worksheet)
|
31
|
+
end
|
32
|
+
|
33
|
+
@model_store.save_all
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def download_spreadsheets
|
39
|
+
spreadsheets = []
|
40
|
+
# First download the spreadsheets
|
41
|
+
@mappings[:spreadsheets].each do |spreadsheet_mapping|
|
42
|
+
spreadsheet = @loader.load_spreadsheet(spreadsheet_mapping[:title])
|
43
|
+
raise "No spreadsheet with a title: #{spreadsheet_mapping['title']} available" if spreadsheet.nil?
|
44
|
+
# Store mapping on the spreadsheet
|
45
|
+
spreadsheet.mapping = spreadsheet_mapping
|
46
|
+
spreadsheets << spreadsheet
|
47
|
+
end
|
48
|
+
return spreadsheets
|
49
|
+
end
|
50
|
+
|
51
|
+
def download_worksheets(spreadsheet)
|
52
|
+
worksheet_mappings = spreadsheet.mapping[:worksheets]
|
53
|
+
worksheets = []
|
54
|
+
if worksheet_mappings
|
55
|
+
worksheet_mappings.each do |worksheet_mapping|
|
56
|
+
title = worksheet_mapping[:title]
|
57
|
+
worksheet = @loader.load_worksheet_from_spreadsheet spreadsheet, title
|
58
|
+
raise "No worksheet with a title: #{worksheet_mapping['title']} available" if worksheet.nil?
|
59
|
+
worksheet.mapping = worksheet_mapping
|
60
|
+
worksheets << worksheet
|
61
|
+
end
|
62
|
+
return worksheets
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def order_worksheets_by_dependencies(worksheets)
|
67
|
+
Logger.log_as_header "Calculating worksheet dependencies"
|
68
|
+
# Now sort their dependencies before converting them
|
69
|
+
worksheets.each do |worksheet|
|
70
|
+
Logger.debug "Worsheet named #{worksheet.title}"
|
71
|
+
associations = find_associations worksheet, worksheets
|
72
|
+
associations.each { |association| Logger.debug " - #{association.title}" }
|
73
|
+
@dependency_graph.add_dependency worksheet, associations
|
74
|
+
end
|
75
|
+
return @dependency_graph.resolved_dependencies
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_associations(dependent_worksheet, worksheets)
|
79
|
+
|
80
|
+
dependent_associations_mapping = dependent_worksheet.mapping[:associations]
|
81
|
+
|
82
|
+
associations = []
|
83
|
+
if dependent_associations_mapping
|
84
|
+
# Run through each association
|
85
|
+
dependent_associations_mapping.each do |association_mapping|
|
86
|
+
# Handle possible polymorphic association
|
87
|
+
# If name is an array, we need to add each possibility as a dependency
|
88
|
+
names = []
|
89
|
+
if association_mapping[:name].is_a? Array
|
90
|
+
names = association_mapping[:name]
|
91
|
+
else
|
92
|
+
names << association_mapping[:name]
|
93
|
+
end
|
94
|
+
names.each do |name|
|
95
|
+
associations << worksheet_for_association(name, worksheets)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
return associations
|
100
|
+
end
|
101
|
+
|
102
|
+
def worksheet_for_association(name, worksheets)
|
103
|
+
# And find the worksheet that satisfies it
|
104
|
+
worksheets.each do |worksheet|
|
105
|
+
class_name = DriveTime.class_name_from_title(worksheet.title)
|
106
|
+
resolved = @class_name_map.resolve_mapped_from_original class_name
|
107
|
+
# If the name matches we have a dependent relationship
|
108
|
+
if resolved.underscore == name
|
109
|
+
return worksheet
|
110
|
+
end
|
111
|
+
end
|
112
|
+
raise MissingAssociationError, "No worksheet #{name} to satisfy multi association"
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_class_map(spreadsheet)
|
116
|
+
worksheets_mapping = spreadsheet.mapping[:worksheets]
|
117
|
+
worksheets_mapping.each do |worksheet_mapping|
|
118
|
+
class_name = DriveTime.class_name_from_title(worksheet_mapping[:title])
|
119
|
+
mapped_class_name = worksheet_mapping[:map_to_class]
|
120
|
+
@class_name_map.save_mapping(class_name, mapped_class_name)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|