config_scripts 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +124 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +1 -0
- data/TODO.md +8 -0
- data/config_scripts.gemspec +31 -0
- data/lib/config_scripts/generators/config_script.rb +24 -0
- data/lib/config_scripts/generators/migrations.rb +36 -0
- data/lib/config_scripts/generators.rb +2 -0
- data/lib/config_scripts/scripts/script.rb +135 -0
- data/lib/config_scripts/scripts/script_history.rb +39 -0
- data/lib/config_scripts/scripts.rb +9 -0
- data/lib/config_scripts/seeds/seed_set.rb +321 -0
- data/lib/config_scripts/seeds/seed_type.rb +361 -0
- data/lib/config_scripts/seeds.rb +8 -0
- data/lib/config_scripts/tasks/pending_migrations.rake +11 -0
- data/lib/config_scripts/tasks/seeds.rake +18 -0
- data/lib/config_scripts/tasks.rb +12 -0
- data/lib/config_scripts/version.rb +4 -0
- data/lib/config_scripts.rb +9 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/hair_color.rb +2 -0
- data/spec/dummy/app/models/person.rb +4 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +23 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20140208014550_create_config_scripts.rb +7 -0
- data/spec/dummy/db/migrate/20140208161829_create_people.rb +9 -0
- data/spec/dummy/db/migrate/20140208182050_create_hair_colors.rb +9 -0
- data/spec/dummy/db/migrate/20140208182101_add_hair_color_to_person.rb +6 -0
- data/spec/dummy/db/migrate/20140208225801_add_scope_to_people.rb +6 -0
- data/spec/dummy/db/migrate/20140209132911_add_hex_value_to_hair_color.rb +5 -0
- data/spec/dummy/db/schema.rb +38 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/generators/config_script_spec.rb +23 -0
- data/spec/generators/migrations_spec.rb +23 -0
- data/spec/scripts/script_history_spec.rb +53 -0
- data/spec/scripts/script_spec.rb +282 -0
- data/spec/seeds/seed_set_spec.rb +371 -0
- data/spec/seeds/seed_type_spec.rb +439 -0
- data/spec/spec_helper.rb +38 -0
- data/templates/config_script.rb +9 -0
- data/templates/create_config_scripts_migration.rb +7 -0
- metadata +321 -0
@@ -0,0 +1,321 @@
|
|
1
|
+
module ConfigScripts
|
2
|
+
module Seeds
|
3
|
+
# This class represents a set of related seeds.
|
4
|
+
#
|
5
|
+
# These seeds will be stored as CSV files in a folder together.
|
6
|
+
class SeedSet
|
7
|
+
# @return [String]
|
8
|
+
# The name of the folder for this seed set.
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# @return [Integer]
|
12
|
+
# A number identifying this set. Seed sets will be run from the one with
|
13
|
+
# the lowest number to the highest.
|
14
|
+
attr_accessor :set_number
|
15
|
+
|
16
|
+
# @return [String]
|
17
|
+
# The name of the folder to which we will write the seeds.
|
18
|
+
attr_reader :folder
|
19
|
+
|
20
|
+
# @return [Hash]
|
21
|
+
# Arbitrary extra data passed in when defining the seed set.
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# @return [Hash]
|
25
|
+
# A hash mapping class names to the {SeedType} instances describing how
|
26
|
+
# to handle seeds for that class within this set.
|
27
|
+
attr_reader :seed_types
|
28
|
+
|
29
|
+
# @return [Proc]
|
30
|
+
# The block that will be run when resetting the records during a load.
|
31
|
+
attr_reader :reset_block
|
32
|
+
|
33
|
+
class << self
|
34
|
+
# @!group Registration
|
35
|
+
|
36
|
+
# @return [Hash<Integer, SeedSet>]
|
37
|
+
# The seed sets that have been defined.
|
38
|
+
attr_reader :registered_sets
|
39
|
+
|
40
|
+
# This method adds a new seed set to our registry.
|
41
|
+
#
|
42
|
+
# If there is already a registered seed set with this set's set_number,
|
43
|
+
# the number will be incremented until it is available.
|
44
|
+
#
|
45
|
+
# @param [SeedSet] set
|
46
|
+
# The new seed set.
|
47
|
+
#
|
48
|
+
# @return [SeedSet]
|
49
|
+
def register_seed_set(set)
|
50
|
+
@registered_sets ||= {}
|
51
|
+
while @registered_sets[set.set_number]
|
52
|
+
return if @registered_sets[set.set_number] == set
|
53
|
+
set.set_number += 1
|
54
|
+
end
|
55
|
+
@registered_sets[set.set_number] = set
|
56
|
+
end
|
57
|
+
|
58
|
+
# This method wipes out our records of registered seed sets.
|
59
|
+
#
|
60
|
+
# @return [Hash]
|
61
|
+
# The new list of seed sets.
|
62
|
+
def clear_registered_sets
|
63
|
+
@registered_sets = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# This method loads all of the seed definitions from the files in the
|
67
|
+
# +db/seeds/definitions+ directory.
|
68
|
+
def load_seed_sets
|
69
|
+
Dir[Rails.root.join('db', 'seeds', 'definitions', '*')].each do |file|
|
70
|
+
require file
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!group Batch Operations
|
75
|
+
|
76
|
+
# This method writes the data for every seed set to its seed data
|
77
|
+
# folder.
|
78
|
+
#
|
79
|
+
# @param [Integer] set_number
|
80
|
+
# The number of the set to write.
|
81
|
+
#
|
82
|
+
# @return [Array]
|
83
|
+
def write(set_number=nil)
|
84
|
+
self.each_set(set_number, &:write)
|
85
|
+
end
|
86
|
+
|
87
|
+
# This method loads the data from every seed set into the database.
|
88
|
+
#
|
89
|
+
# @param [Integer] set_number
|
90
|
+
# The number of the set to read.
|
91
|
+
#
|
92
|
+
# @return [Array]
|
93
|
+
def read(set_number=nil)
|
94
|
+
self.each_set set_number do |set|
|
95
|
+
set.read(set_number.present?)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# This method lists every seed set, with its set number.
|
100
|
+
# @return [Array]
|
101
|
+
def list
|
102
|
+
self.each_set do |set|
|
103
|
+
puts "#{set.set_number}: #{set.name}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# This method runs a block on each set that the app has defined.
|
108
|
+
#
|
109
|
+
# The block will be given one parameter, which is the seed set.
|
110
|
+
#
|
111
|
+
# @param [String] set_number
|
112
|
+
# The number of the set that we should run.
|
113
|
+
#
|
114
|
+
# @return [Array]
|
115
|
+
def each_set(set_number=nil, &block)
|
116
|
+
@registered_sets ||= {}
|
117
|
+
self.load_seed_sets
|
118
|
+
if set_number
|
119
|
+
if self.registered_sets[set_number]
|
120
|
+
block.call(self.registered_sets[set_number])
|
121
|
+
end
|
122
|
+
else
|
123
|
+
self.registered_sets.keys.sort.each do |set_number|
|
124
|
+
block.call(self.registered_sets[set_number])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# @!group Creation
|
131
|
+
|
132
|
+
# This method creates a new seed set.
|
133
|
+
#
|
134
|
+
# It should be given a block, which will be run on the instance, and
|
135
|
+
# which should use the {#seeds_for} method to define seed types.
|
136
|
+
#
|
137
|
+
# @param [String] name
|
138
|
+
# The name for the folder for the seeds.
|
139
|
+
#
|
140
|
+
# @param [Integer] set_number
|
141
|
+
# The set_number in which this seed set should be run.
|
142
|
+
#
|
143
|
+
# @param [String] folder
|
144
|
+
# The folder that we should use for this seed set. If this is not
|
145
|
+
# provided, we will use the name.
|
146
|
+
#
|
147
|
+
# @param [Hash] options
|
148
|
+
# Additional information that can be made accessible to the seed type
|
149
|
+
# definitions.
|
150
|
+
def initialize(name, set_number=1, folder=nil, options = {}, &block)
|
151
|
+
@name = name.to_s
|
152
|
+
@set_number = set_number
|
153
|
+
@folder = folder || @name
|
154
|
+
@options = options
|
155
|
+
@seed_types = {}
|
156
|
+
self.instance_eval(&block) if block_given?
|
157
|
+
ConfigScripts::Seeds::SeedSet.register_seed_set(self)
|
158
|
+
end
|
159
|
+
|
160
|
+
# @!group Reading and Writing
|
161
|
+
|
162
|
+
# This method writes the data for this seed set to its seed folder.
|
163
|
+
#
|
164
|
+
# It will create the folder and then write the file for each seed type
|
165
|
+
# that has been defined.
|
166
|
+
def write
|
167
|
+
folder_path = Rails.root.join('db', 'seeds', 'data', self.folder)
|
168
|
+
FileUtils.mkdir_p(folder_path)
|
169
|
+
puts "Writing seeds for #{self.name} to #{folder_path}"
|
170
|
+
self.seed_types.each do |klass, seed_type|
|
171
|
+
seed_type.write_to_folder(folder_path)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# This method reads the data for this seed set from its seed folder.
|
176
|
+
#
|
177
|
+
# @param [Boolean] reset
|
178
|
+
# Whether we should reset the existing records before loading the seeds.
|
179
|
+
#
|
180
|
+
# It will load the data for each seed type's file, enclosing all the
|
181
|
+
# seed types in a transaction block.
|
182
|
+
def read(reset=false)
|
183
|
+
folder_path = Rails.root.join('db', 'seeds', 'data', self.folder)
|
184
|
+
FileUtils.mkdir_p(folder_path)
|
185
|
+
puts "Reading seeds for #{self.name} from #{folder_path}"
|
186
|
+
ActiveRecord::Base.transaction do
|
187
|
+
self.reset_records if reset
|
188
|
+
self.seed_types.each do |klass, seed_type|
|
189
|
+
seed_type.read_from_folder(folder_path)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# This method resets all the existing records that could be populated by
|
195
|
+
# this seed set.
|
196
|
+
def reset_records
|
197
|
+
self.reset_block.call if self.reset_block
|
198
|
+
end
|
199
|
+
|
200
|
+
# @!group DSL
|
201
|
+
|
202
|
+
# This method defines a new seed type within this seed set.
|
203
|
+
#
|
204
|
+
# This method should be given a block, which will be passed to the
|
205
|
+
# initializer for the new seed type.
|
206
|
+
#
|
207
|
+
# @param [Class] klass
|
208
|
+
# The model class whose seed data this stores.
|
209
|
+
#
|
210
|
+
# @param [String] filename
|
211
|
+
# The name of the file in which the seed data should be stored.
|
212
|
+
# If this is not provided, it will use the name of the class.
|
213
|
+
# This should not include the file extension.
|
214
|
+
#
|
215
|
+
# @return [SeedType]
|
216
|
+
def seeds_for(klass, filename=nil, &block)
|
217
|
+
filename ||= klass.name.underscore.pluralize
|
218
|
+
@seed_types[klass] = SeedType.new(self, klass, filename, &block)
|
219
|
+
end
|
220
|
+
|
221
|
+
# This method defines a block that will be run when resetting existing
|
222
|
+
# records.
|
223
|
+
#
|
224
|
+
# This block will be run when loading a seed set as a one-off, but not
|
225
|
+
# when loading all the seed sets.
|
226
|
+
#
|
227
|
+
# @return [Proc]
|
228
|
+
def when_resetting(&block)
|
229
|
+
@reset_block = block
|
230
|
+
end
|
231
|
+
|
232
|
+
# @!group Seed Identifiers
|
233
|
+
|
234
|
+
# This method gets a seed type that we have on file for a class.
|
235
|
+
#
|
236
|
+
# @param [Class] klass
|
237
|
+
# The class whose seeds we are dealing with.
|
238
|
+
#
|
239
|
+
# @return [SeedType]
|
240
|
+
def seed_type_for_class(klass)
|
241
|
+
while klass && klass != ActiveRecord::Base
|
242
|
+
seed_type = self.seed_types[klass]
|
243
|
+
if seed_type
|
244
|
+
return seed_type
|
245
|
+
end
|
246
|
+
klass = klass.superclass
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
|
251
|
+
# This method gets a unique identifier for a record when writing seeds
|
252
|
+
# that refer to it.
|
253
|
+
#
|
254
|
+
# It will look for a seed type that handles seeds for a class in the
|
255
|
+
# record's class heirarchy, and use that seed type's
|
256
|
+
# {#seed_identifier_for_record} method. If it cannot find a seed type,
|
257
|
+
# it will use the record's ID.
|
258
|
+
#
|
259
|
+
# @param [ActiveRecord::Base] record
|
260
|
+
# The record whose identifier we are generating.
|
261
|
+
#
|
262
|
+
# @return [String]
|
263
|
+
def seed_identifier_for_record(record)
|
264
|
+
seed_type = self.seed_type_for_class(record.class)
|
265
|
+
if seed_type
|
266
|
+
seed_type.seed_identifier_for_record(record)
|
267
|
+
else
|
268
|
+
record.id rescue nil
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# This method finds a record based on a unique identifier in the seed
|
273
|
+
# data.
|
274
|
+
#
|
275
|
+
# This will look for a seed type for the class, and use its
|
276
|
+
# {#record_for_seed_identifier} method to get the record.
|
277
|
+
#
|
278
|
+
# The result of this will be memoized so that we do not have to keep
|
279
|
+
# looking up the records.
|
280
|
+
#
|
281
|
+
# @param [Class] klass
|
282
|
+
# The model class for the record we are finding.
|
283
|
+
#
|
284
|
+
# @param [Array<String>] identifier
|
285
|
+
# The identifier components from the seed data.
|
286
|
+
#
|
287
|
+
# @return [ActiveRecord::Base]
|
288
|
+
# The model record.
|
289
|
+
def record_for_seed_identifier(klass, identifier)
|
290
|
+
seed_type = self.seed_type_for_class(klass)
|
291
|
+
return nil unless seed_type
|
292
|
+
|
293
|
+
@record_cache ||= {}
|
294
|
+
@record_cache[klass] ||= {}
|
295
|
+
|
296
|
+
cache_identifier = identifier.dup
|
297
|
+
|
298
|
+
while cache_identifier.any?
|
299
|
+
record = @record_cache[klass][cache_identifier]
|
300
|
+
if record
|
301
|
+
cache_identifier.count.times { identifier.shift }
|
302
|
+
return record
|
303
|
+
end
|
304
|
+
cache_identifier.pop
|
305
|
+
end
|
306
|
+
|
307
|
+
if seed_type
|
308
|
+
cache_identifier = identifier.dup
|
309
|
+
record = seed_type.record_for_seed_identifier(identifier)
|
310
|
+
|
311
|
+
if identifier.any?
|
312
|
+
cache_identifier = cache_identifier[0...-1*identifier.count]
|
313
|
+
end
|
314
|
+
|
315
|
+
@record_cache[klass][cache_identifier] = record
|
316
|
+
end
|
317
|
+
record
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,361 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module ConfigScripts
|
4
|
+
module Seeds
|
5
|
+
# This class encapsulates information about how to write seeds for a class
|
6
|
+
# to a seed file.
|
7
|
+
class SeedType
|
8
|
+
# @!group Attributes
|
9
|
+
|
10
|
+
# @return [SeedSet]
|
11
|
+
# The seed set that this type has been defined within.
|
12
|
+
attr_reader :seed_set
|
13
|
+
|
14
|
+
# @return [Class]
|
15
|
+
# The model class whose records we are storing in this seed file.
|
16
|
+
attr_reader :klass
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
# The name of the file that we will store these records in.
|
20
|
+
attr_reader :filename
|
21
|
+
|
22
|
+
# @return [Array<Symbol>]
|
23
|
+
# The names of the attributes on the model object that we store in the
|
24
|
+
# seed file.
|
25
|
+
attr_reader :attributes
|
26
|
+
|
27
|
+
# @return [Array<Symbol>]
|
28
|
+
# The names of the attributes used to compose a unique identifier for
|
29
|
+
# a record.
|
30
|
+
attr_reader :identifier_attributes
|
31
|
+
|
32
|
+
# @return [Array]
|
33
|
+
# The active record associations for the model class for this seed file.
|
34
|
+
attr_reader :associations
|
35
|
+
|
36
|
+
# @return [Hash<Symbol, Proc>]
|
37
|
+
# The attributes that we generate dynamically after loading the ones from
|
38
|
+
# the seed file.
|
39
|
+
attr_reader :dynamic_readers
|
40
|
+
|
41
|
+
# @return [Hash<Symbol, Proc>]
|
42
|
+
# The attributes that we generate dynamically when writing things to the
|
43
|
+
# seed file.
|
44
|
+
attr_reader :dynamic_writers
|
45
|
+
|
46
|
+
# @return [Array<Array>]
|
47
|
+
# The scopes that we apply when fetching items.
|
48
|
+
#
|
49
|
+
# Each entry will be an array. The first entry in the inner arrays will
|
50
|
+
# be a symbol, the name of a method that can be run on a relation. The
|
51
|
+
# result of the array will be passed in when running the method on the
|
52
|
+
# scope.
|
53
|
+
attr_reader :scopes
|
54
|
+
|
55
|
+
# @!group Creation
|
56
|
+
|
57
|
+
# This method creates a new seed type.
|
58
|
+
#
|
59
|
+
# This method should be given a block, which will be run in the instance
|
60
|
+
# context of the new seed type. That block should use the DSL methods to
|
61
|
+
# fill in the details for the seed type.
|
62
|
+
#
|
63
|
+
# @param [SeedSet] seed_set
|
64
|
+
# The seed set that this seed type is defined within.
|
65
|
+
#
|
66
|
+
# @param [Class] klass
|
67
|
+
# The model class whose data we are running.
|
68
|
+
#
|
69
|
+
# @param [String] filename
|
70
|
+
# The name of the file in which the seed data will be stored.
|
71
|
+
def initialize(seed_set, klass, filename, &block)
|
72
|
+
@seed_set = seed_set
|
73
|
+
@klass = klass
|
74
|
+
@filename = filename
|
75
|
+
@attributes = []
|
76
|
+
@identifier_attributes = [:id]
|
77
|
+
@scopes = []
|
78
|
+
@dynamic_writers = {}
|
79
|
+
@dynamic_readers = {}
|
80
|
+
|
81
|
+
@associations = {}
|
82
|
+
@klass.reflect_on_all_associations.each do |association|
|
83
|
+
@associations[association.name] = association.klass rescue nil
|
84
|
+
end
|
85
|
+
|
86
|
+
self.instance_eval(&block) if block_given?
|
87
|
+
end
|
88
|
+
|
89
|
+
# @!group DSL
|
90
|
+
|
91
|
+
# This method adds new attributes to the ones written in this seed type.
|
92
|
+
#
|
93
|
+
# @param [Array<Symbol>] new_attributes
|
94
|
+
# The attributes to add.
|
95
|
+
#
|
96
|
+
# @return [Array<Symbol>]
|
97
|
+
# The full list of attributes after the new ones are added.
|
98
|
+
def has_attributes(*new_attributes)
|
99
|
+
@attributes += new_attributes
|
100
|
+
end
|
101
|
+
|
102
|
+
# This method defines the attributes used to generate a unique identifier
|
103
|
+
# for a record.
|
104
|
+
#
|
105
|
+
# @param [Array<Symbol>] attributes
|
106
|
+
# The attributes that form the unique identifier.
|
107
|
+
#
|
108
|
+
# @return [Array<Symbol>]
|
109
|
+
# The attributes.
|
110
|
+
def has_identifier_attributes(*attributes)
|
111
|
+
@identifier_attributes = attributes
|
112
|
+
end
|
113
|
+
|
114
|
+
# This method adds a scope to the list used to filter the records for
|
115
|
+
# writing.
|
116
|
+
#
|
117
|
+
# @param [Symbol] method
|
118
|
+
# The name of the method to call on the relation.
|
119
|
+
#
|
120
|
+
# @param [Array] args
|
121
|
+
# The arguments that will be passed into the scope method on the
|
122
|
+
# relation.
|
123
|
+
#
|
124
|
+
# @return [Array]
|
125
|
+
# The full list of scopes.
|
126
|
+
def has_scope(method, *args)
|
127
|
+
@scopes << [method, args]
|
128
|
+
end
|
129
|
+
|
130
|
+
# This method registers a custom block that will be run when reading a
|
131
|
+
# value from the seed file.
|
132
|
+
#
|
133
|
+
# This method takes a block that will be run on the value from the seed
|
134
|
+
# file. The return value of the block will be used in place of the
|
135
|
+
# original value from the seed file.
|
136
|
+
#
|
137
|
+
# @param [Symbol] attribute
|
138
|
+
# The attribute that we are reading.
|
139
|
+
def when_reading(attribute, &block)
|
140
|
+
@dynamic_readers[attribute] = block
|
141
|
+
end
|
142
|
+
|
143
|
+
# This method registers a custom block that will be run when writing a
|
144
|
+
# value to the seed file.
|
145
|
+
#
|
146
|
+
# This method takes a block that will be run on the item whose values we
|
147
|
+
# are writing. The return value of the block will be used instead of
|
148
|
+
# running the method on the record.
|
149
|
+
#
|
150
|
+
# @param [Symbol] attribute
|
151
|
+
# The attribute we are writing.
|
152
|
+
def when_writing(attribute, &block)
|
153
|
+
@dynamic_writers[attribute] = block
|
154
|
+
end
|
155
|
+
|
156
|
+
# @!group Reading and Writing
|
157
|
+
|
158
|
+
# This method writes the seed data file to a folder.
|
159
|
+
#
|
160
|
+
# This will write a header row with the names of the attributes, and then
|
161
|
+
# write a row for each item from the {#items} method. It will use the
|
162
|
+
# {#write_value_for_attribute} method to get the values for the CSV file.
|
163
|
+
#
|
164
|
+
# If this seed type has no attributes, this method will not write
|
165
|
+
# anything.
|
166
|
+
#
|
167
|
+
# @param [String] folder
|
168
|
+
# The full path to the folder to write to.
|
169
|
+
def write_to_folder(folder)
|
170
|
+
return unless attributes.any?
|
171
|
+
CSV.open(File.join(folder, "#{self.filename}.csv"), 'w') do |csv|
|
172
|
+
csv << self.attributes
|
173
|
+
self.items.each do |item|
|
174
|
+
data = self.attributes.collect { |attribute| self.write_value_for_attribute(item, attribute) }
|
175
|
+
csv << data
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# This method reads the seed data from a file, and creates new records
|
181
|
+
# from it.
|
182
|
+
#
|
183
|
+
# This will extract all the rows from the CSV file, and use
|
184
|
+
# {#read_value_for_attribute} to get the attributes for the record from
|
185
|
+
# each cell in the CSV file.
|
186
|
+
#
|
187
|
+
# If this seed type has no attributes, this method will not try to read
|
188
|
+
# the file.
|
189
|
+
#
|
190
|
+
# @param [String] folder
|
191
|
+
# The full path to the folder with the seed files.
|
192
|
+
def read_from_folder(folder)
|
193
|
+
return unless attributes.any?
|
194
|
+
CSV.open(File.join(folder, "#{self.filename}.csv"), headers: true) do |csv|
|
195
|
+
csv.each do |row|
|
196
|
+
record = self.klass.new
|
197
|
+
row.each do |attribute, value|
|
198
|
+
attribute = attribute.to_sym
|
199
|
+
value = self.read_value_for_attribute(value, attribute)
|
200
|
+
record.send("#{attribute}=", value)
|
201
|
+
end
|
202
|
+
record.save!
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# This method gets the value that we should write into the CSV file for
|
208
|
+
# an attribute.
|
209
|
+
#
|
210
|
+
# If the value for that attribute is another model record, this will get
|
211
|
+
# its seed identifier from the seed set. Otherwise, it will just use the
|
212
|
+
# value.
|
213
|
+
#
|
214
|
+
# If the attribute is a polymorphic foreign key, this will prefix the seed
|
215
|
+
# identifier with the class name.
|
216
|
+
#
|
217
|
+
# @param [ActiveRecord::Base] item
|
218
|
+
# The record whose value we are getting.
|
219
|
+
#
|
220
|
+
# @param [Symbol] attribute
|
221
|
+
# The attribute we are getting.
|
222
|
+
#
|
223
|
+
# @return [String]
|
224
|
+
# The value to write.
|
225
|
+
def write_value_for_attribute(item, attribute)
|
226
|
+
if @dynamic_writers[attribute]
|
227
|
+
value = @dynamic_writers[attribute].call(item)
|
228
|
+
else
|
229
|
+
value = item.send(attribute)
|
230
|
+
end
|
231
|
+
|
232
|
+
if value.is_a?(ActiveRecord::Base)
|
233
|
+
identifier = self.seed_set.seed_identifier_for_record(value)
|
234
|
+
if !self.associations[attribute]
|
235
|
+
identifier = "#{value.class.name}::#{identifier}"
|
236
|
+
end
|
237
|
+
value = identifier
|
238
|
+
end
|
239
|
+
value
|
240
|
+
end
|
241
|
+
|
242
|
+
# This method takes a value from the CSV file and gives back the value
|
243
|
+
# that should be set on the record.
|
244
|
+
#
|
245
|
+
# If the attribute is an association, this will pass it to the seed set
|
246
|
+
# as a seed identifier. If it is a polymorphic association, it will use
|
247
|
+
# the first part of the seed identifier as a class name.
|
248
|
+
#
|
249
|
+
# @param [String] value
|
250
|
+
# The value from the CSV file.
|
251
|
+
#
|
252
|
+
# @param [Symbol] attribute
|
253
|
+
# The name of the attribute that we are formatting.
|
254
|
+
#
|
255
|
+
# @return [Object]
|
256
|
+
# The value to set on the record.
|
257
|
+
def read_value_for_attribute(value, attribute)
|
258
|
+
if @dynamic_readers[attribute]
|
259
|
+
value = @dynamic_readers[attribute].call(value)
|
260
|
+
end
|
261
|
+
|
262
|
+
if @associations.has_key?(attribute)
|
263
|
+
return nil if value.blank?
|
264
|
+
value = self.read_value_for_association(attribute, value.split("::"))
|
265
|
+
end
|
266
|
+
value
|
267
|
+
end
|
268
|
+
|
269
|
+
# This method gets the value that we will assign to an attribute for an
|
270
|
+
# association.
|
271
|
+
#
|
272
|
+
# This will remove as many entries from the identifier as it needs to get
|
273
|
+
# the value.
|
274
|
+
#
|
275
|
+
# @param [Symbol] attribute
|
276
|
+
# The name of the association.
|
277
|
+
#
|
278
|
+
# @param [Array<String>] identifier
|
279
|
+
# The components of the seed identifier.
|
280
|
+
#
|
281
|
+
# @return [ActiveRecord::Base]
|
282
|
+
def read_value_for_association(attribute, identifier)
|
283
|
+
klass = @associations[attribute]
|
284
|
+
unless klass
|
285
|
+
class_name = identifier.shift
|
286
|
+
klass = class_name.constantize
|
287
|
+
end
|
288
|
+
value = self.seed_set.record_for_seed_identifier(klass, identifier)
|
289
|
+
end
|
290
|
+
|
291
|
+
# @!group Fetching
|
292
|
+
|
293
|
+
# This method gets a relation encompassing all the records in the class.
|
294
|
+
#
|
295
|
+
# We encapsulate this here so that we can use different calls in Rails 3
|
296
|
+
# and Rails 4.
|
297
|
+
#
|
298
|
+
# @return [Relation]
|
299
|
+
def all
|
300
|
+
version = Rails.version[0]
|
301
|
+
records = version == '3' ? self.klass.scoped : self.klass.all
|
302
|
+
end
|
303
|
+
# This method gets the items that we should write to our seed file.
|
304
|
+
#
|
305
|
+
# It will start with the scoped list for the model class, and then apply
|
306
|
+
# all the scopes in our {#scopes} list.
|
307
|
+
#
|
308
|
+
# @return [Relation]
|
309
|
+
def items
|
310
|
+
records = self.all
|
311
|
+
self.scopes.each { |method, args| records = records.send(method, *args) }
|
312
|
+
records
|
313
|
+
end
|
314
|
+
|
315
|
+
# This method gets the additional information passed in when defining our
|
316
|
+
# seed set.
|
317
|
+
#
|
318
|
+
# @return [Hash]
|
319
|
+
def options
|
320
|
+
self.seed_set.options
|
321
|
+
end
|
322
|
+
|
323
|
+
# @!group Seed Identifiers
|
324
|
+
|
325
|
+
# This method gets the unique identifier for a record of the class that
|
326
|
+
# this seed type handles.
|
327
|
+
#
|
328
|
+
# @param [ActiveRecord::Base] record
|
329
|
+
# The record
|
330
|
+
#
|
331
|
+
# @return [String]
|
332
|
+
# The identifier for the seed files.
|
333
|
+
def seed_identifier_for_record(record)
|
334
|
+
self.identifier_attributes.collect { |param| self.write_value_for_attribute(record, param) }.join("::")
|
335
|
+
end
|
336
|
+
|
337
|
+
# This method finds a record for our model class based on the unique seed
|
338
|
+
# identifier.
|
339
|
+
#
|
340
|
+
# @param [Array<String>] identifier
|
341
|
+
# The identifier from the CSV file.
|
342
|
+
#
|
343
|
+
# @return [ActiveRecord::Base]
|
344
|
+
# The record
|
345
|
+
def record_for_seed_identifier(identifier)
|
346
|
+
return nil if identifier.blank?
|
347
|
+
records = self.all
|
348
|
+
self.identifier_attributes.each_with_index do |attribute, index|
|
349
|
+
if self.associations.has_key?(attribute)
|
350
|
+
value = self.read_value_for_association(attribute, identifier)
|
351
|
+
records = records.where("#{attribute}_id" => value.try(:id))
|
352
|
+
else
|
353
|
+
value = self.read_value_for_attribute(identifier.shift, attribute)
|
354
|
+
records = records.where(attribute => value)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
records.first
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
namespace :config_scripts do
|
2
|
+
desc "Run pending config scripts"
|
3
|
+
task :run_pending => :environment do
|
4
|
+
ConfigScripts::Scripts::Script.run_pending_scripts
|
5
|
+
end
|
6
|
+
|
7
|
+
desc "List pending config scripts"
|
8
|
+
task :list_pending => :environment do
|
9
|
+
ConfigScripts::Scripts::Script.list_pending_scripts
|
10
|
+
end
|
11
|
+
end
|