bullet_train-super_scaffolding 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1365 @@
1
+ require "indefinite_article"
2
+ require "yaml"
3
+
4
+ class Scaffolding::Transformer
5
+ attr_accessor :child, :parent, :parents, :class_names_transformer, :cli_options, :additional_steps, :namespace, :suppress_could_not_find
6
+
7
+ def initialize(child, parents, cli_options = {})
8
+ self.child = child
9
+ self.parent = parents.first
10
+ self.parents = parents
11
+ self.namespace = cli_options["namespace"] || "account"
12
+ self.class_names_transformer = Scaffolding::ClassNamesTransformer.new(child, parent, namespace)
13
+ self.cli_options = cli_options
14
+ self.additional_steps = []
15
+ end
16
+
17
+ RUBY_NEW_FIELDS_PROCESSING_HOOK = "# 🚅 super scaffolding will insert processing for new fields above this line."
18
+ RUBY_NEW_ARRAYS_HOOK = "# 🚅 super scaffolding will insert new arrays above this line."
19
+ RUBY_NEW_FIELDS_HOOK = "# 🚅 super scaffolding will insert new fields above this line."
20
+ RUBY_ADDITIONAL_NEW_FIELDS_HOOK = "# 🚅 super scaffolding will also insert new fields above this line."
21
+ RUBY_EVEN_MORE_NEW_FIELDS_HOOK = "# 🚅 super scaffolding will additionally insert new fields above this line."
22
+ ENDPOINTS_HOOK = "# 🚅 super scaffolding will mount new endpoints above this line."
23
+ ERB_NEW_FIELDS_HOOK = "<%#{RUBY_NEW_FIELDS_HOOK} %>"
24
+ CONCERNS_HOOK = "# 🚅 add concerns above."
25
+ BELONGS_TO_HOOK = "# 🚅 add belongs_to associations above."
26
+ HAS_MANY_HOOK = "# 🚅 add has_many associations above."
27
+ OAUTH_PROVIDERS_HOOK = "# 🚅 add oauth providers above."
28
+ HAS_ONE_HOOK = "# 🚅 add has_one associations above."
29
+ SCOPES_HOOK = "# 🚅 add scopes above."
30
+ VALIDATIONS_HOOK = "# 🚅 add validations above."
31
+ CALLBACKS_HOOK = "# 🚅 add callbacks above."
32
+ DELEGATIONS_HOOK = "# 🚅 add delegations above."
33
+ METHODS_HOOK = "# 🚅 add methods above."
34
+
35
+ def encode_double_replacement_fix(string)
36
+ string.chars.join("~!@BT@!~")
37
+ end
38
+
39
+ def decode_double_replacement_fix(string)
40
+ string.gsub("~!@BT@!~", "")
41
+ end
42
+
43
+ def transform_string(string)
44
+ [
45
+
46
+ # full class name plural.
47
+ "Scaffolding::AbsolutelyAbstract::CreativeConcepts",
48
+ "Scaffolding::CompletelyConcrete::TangibleThings",
49
+ "scaffolding/absolutely_abstract/creative_concepts",
50
+ "scaffolding/completely_concrete/tangible_things",
51
+ "scaffolding/completely_concrete/_tangible_things",
52
+ "scaffolding_absolutely_abstract_creative_concepts",
53
+ "scaffolding_completely_concrete_tangible_things",
54
+ "scaffolding-absolutely-abstract-creative-concepts",
55
+ "scaffolding-completely-concrete-tangible-things",
56
+
57
+ # full class name singular.
58
+ "Scaffolding::AbsolutelyAbstract::CreativeConcept",
59
+ "Scaffolding::CompletelyConcrete::TangibleThing",
60
+ "scaffolding/absolutely_abstract/creative_concept",
61
+ "scaffolding/completely_concrete/tangible_thing",
62
+ "scaffolding_absolutely_abstract_creative_concept",
63
+ "scaffolding_completely_concrete_tangible_thing",
64
+ "scaffolding-absolutely-abstract-creative-concept",
65
+ "scaffolding-completely-concrete-tangible-thing",
66
+
67
+ # class name in context plural.
68
+ "absolutely_abstract_creative_concepts",
69
+ "completely_concrete_tangible_things",
70
+ "absolutely_abstract/creative_concepts",
71
+ "completely_concrete/tangible_things",
72
+ "absolutely-abstract-creative-concepts",
73
+ "completely-concrete-tangible-things",
74
+
75
+ # class name in context singular.
76
+ "absolutely_abstract_creative_concept",
77
+ "completely_concrete_tangible_thing",
78
+ "absolutely_abstract/creative_concept",
79
+ "completely_concrete/tangible_thing",
80
+ "absolutely-abstract-creative-concept",
81
+ "completely-concrete-tangible-thing",
82
+
83
+ # just class name singular.
84
+ "creative_concepts",
85
+ "tangible_things",
86
+ "creative-concepts",
87
+ "tangible-things",
88
+ "Creative Concepts",
89
+ "Tangible Things",
90
+
91
+ # just class name plural.
92
+ "creative_concept",
93
+ "tangible_thing",
94
+ "creative-concept",
95
+ "tangible-thing",
96
+ "Creative Concept",
97
+ "Tangible Thing",
98
+
99
+ # Account namespace vs. others.
100
+ ":account",
101
+ "/account/"
102
+
103
+ ].each do |needle|
104
+ string = string.gsub(needle, encode_double_replacement_fix(class_names_transformer.replacement_for(needle)))
105
+ end
106
+ decode_double_replacement_fix(string)
107
+ end
108
+
109
+ def get_transformed_file_content(file)
110
+ transformed_file_content = []
111
+
112
+ skipping = false
113
+ gathering_lines_to_repeat = false
114
+
115
+ parents_to_repeat_for = []
116
+ gathered_lines_for_repeating = nil
117
+
118
+ File.open(file).each_line do |line|
119
+ if line.include?("# 🚅 skip when scaffolding.")
120
+ next
121
+ end
122
+
123
+ if line.include?("# 🚅 skip this section if resource is nested directly under team.")
124
+ skipping = true if parent == "Team"
125
+ next
126
+ end
127
+
128
+ if line.include?("# 🚅 skip this section when scaffolding.")
129
+ skipping = true
130
+ next
131
+ end
132
+
133
+ if line.include?("# 🚅 stop any skipping we're doing now.")
134
+ skipping = false
135
+ next
136
+ end
137
+
138
+ if line.include?("# 🚅 for each child resource from team down to the resource we're scaffolding, repeat the following:")
139
+ gathering_lines_to_repeat = true
140
+ parents_to_repeat_for = ([child] + parents.dup).reverse
141
+ gathered_lines_for_repeating = []
142
+ next
143
+ end
144
+
145
+ if line.include?("# 🚅 stop repeating.")
146
+ gathering_lines_to_repeat = false
147
+
148
+ while parents_to_repeat_for.count > 1
149
+ current_parent = parents_to_repeat_for[0]
150
+ current_child = parents_to_repeat_for[1]
151
+ current_transformer = self.class.new(current_child, current_parent)
152
+ transformed_file_content << current_transformer.transform_string(gathered_lines_for_repeating.join)
153
+ parents_to_repeat_for.shift
154
+ end
155
+
156
+ next
157
+ end
158
+
159
+ if gathering_lines_to_repeat
160
+ gathered_lines_for_repeating << line
161
+ next
162
+ end
163
+
164
+ if skipping
165
+ next
166
+ end
167
+
168
+ # remove lines with 'remove in scaffolded files.'
169
+ unless line.include?("remove in scaffolded files.")
170
+
171
+ # only transform it if it doesn't have the lock emoji.
172
+ if line.include?("🔒")
173
+ # remove any comments that start with a lock.
174
+ line.gsub!(/\s+?#\s+🔒.*/, "")
175
+ else
176
+ line = transform_string(line)
177
+ end
178
+
179
+ transformed_file_content << line
180
+
181
+ end
182
+ end
183
+
184
+ transformed_file_content.join
185
+ end
186
+
187
+ def scaffold_file(file)
188
+ transformed_file_content = get_transformed_file_content(file)
189
+ transformed_file_name = transform_string(file)
190
+
191
+ transformed_directory_name = File.dirname(transformed_file_name)
192
+ unless File.directory?(transformed_directory_name)
193
+ FileUtils.mkdir_p(transformed_directory_name)
194
+ end
195
+
196
+ puts "Writing '#{transformed_file_name}'."
197
+
198
+ File.write(transformed_file_name, transformed_file_content.strip + "\n")
199
+
200
+ if transformed_file_name.split(".").last == "rb"
201
+ puts "Fixing Standard Ruby on '#{transformed_file_name}'."
202
+ # `standardrb --fix #{transformed_file_name} 2> /dev/null`
203
+ end
204
+ end
205
+
206
+ def scaffold_directory(directory)
207
+ transformed_directory_name = transform_string(directory)
208
+ begin
209
+ Dir.mkdir(transformed_directory_name)
210
+ rescue Errno::EEXIST => _
211
+ puts "The directory #{transformed_directory_name} already exists, skipping generation.".yellow
212
+ rescue Errno::ENOENT => _
213
+ puts "Proceeding to generate '#{transformed_directory_name}'."
214
+ end
215
+
216
+ Dir.foreach(directory) do |file|
217
+ file = "#{directory}/#{file}"
218
+ unless File.directory?(file)
219
+ scaffold_file(file)
220
+ end
221
+ end
222
+ end
223
+
224
+ # pass in an array where this content should be inserted within the yml file. For example, to add content
225
+ # to admin.models pass in [:admin, :models]
226
+ def add_line_to_yml_file(file, content, location_array)
227
+ # First check that the given location array actually exists in the yml file:
228
+ yml = YAML.safe_load(File.read(file))
229
+ location_array.map!(&:to_s)
230
+ return nil if yml.dig(*location_array).nil? # Should we raise an error?
231
+ content += "\n" unless content[-1] == "\n"
232
+ # Find the location in the file where the location_array is
233
+ lines = File.readlines(file)
234
+ current_needle = location_array.shift.to_s
235
+ current_space = ""
236
+ insert_after = 1
237
+ lines.each_with_index do |line, index|
238
+ break if current_needle.nil?
239
+ if line.strip == current_needle + ":"
240
+ current_needle = location_array.shift.to_s
241
+ insert_after = index
242
+ current_space = line.match(/\s+/).to_s
243
+ end
244
+ end
245
+ new_lines = []
246
+ current_space += " "
247
+ lines.each_with_index do |line, index|
248
+ new_lines << line
249
+ new_lines << current_space + content if index == insert_after
250
+ end
251
+ File.write(file, new_lines.join)
252
+ end
253
+
254
+ def add_line_to_file(file, content, hook, options = {})
255
+ increase_indent = options[:increase_indent]
256
+ add_before = options[:add_before]
257
+ add_after = options[:add_after]
258
+
259
+ transformed_file_name = file
260
+ transformed_content = content
261
+ transform_hook = hook
262
+
263
+ begin
264
+ target_file_content = File.read(transformed_file_name)
265
+ rescue Errno::ENOENT => _
266
+ puts "Couldn't find '#{transformed_file_name}'".red unless suppress_could_not_find
267
+ return false
268
+ end
269
+
270
+ if target_file_content.include?(transformed_content)
271
+ puts "No need to update '#{transformed_file_name}'. It already has '#{transformed_content}'."
272
+
273
+ else
274
+
275
+ new_target_file_content = []
276
+
277
+ target_file_content.split("\n").each do |line|
278
+ if options[:exact_match] ? line == transform_hook : line.match(/#{Regexp.escape(transform_hook)}\s*$/)
279
+
280
+ if add_before
281
+ new_target_file_content << "#{line} #{add_before}"
282
+ else
283
+ unless options[:prepend]
284
+ new_target_file_content << line
285
+ end
286
+ end
287
+
288
+ line =~ /^(\s*).*#{Regexp.escape(transform_hook)}.*/
289
+ leading_whitespace = $1
290
+
291
+ incoming_leading_whitespace = nil
292
+ transformed_content.lines.each do |content_line|
293
+ content_line.rstrip
294
+ content_line =~ /^(\s*).*/
295
+ # this ignores empty lines.
296
+ # it accepts any amount of whitespace if we haven't seen any whitespace yet.
297
+ if content_line.present? && $1 && (incoming_leading_whitespace.nil? || $1.length < incoming_leading_whitespace.length)
298
+ incoming_leading_whitespace = $1
299
+ end
300
+ end
301
+
302
+ incoming_leading_whitespace ||= ""
303
+
304
+ transformed_content.lines.each do |content_line|
305
+ new_target_file_content << "#{leading_whitespace}#{" " if increase_indent}#{content_line.gsub(/^#{incoming_leading_whitespace}/, "").rstrip}".presence
306
+ end
307
+
308
+ new_target_file_content << "#{leading_whitespace}#{add_after}" if add_after
309
+
310
+ if options[:prepend]
311
+ new_target_file_content << line
312
+ end
313
+
314
+ else
315
+
316
+ new_target_file_content << line
317
+
318
+ end
319
+ end
320
+
321
+ puts "Updating '#{transformed_file_name}'."
322
+
323
+ File.write(transformed_file_name, new_target_file_content.join("\n").strip + "\n")
324
+
325
+ end
326
+ end
327
+
328
+ def scaffold_add_line_to_file(file, content, hook, options = {})
329
+ file = transform_string(file)
330
+ content = transform_string(content)
331
+ hook = transform_string(hook)
332
+ add_line_to_file(file, content, hook, options)
333
+ end
334
+
335
+ def replace_line_in_file(file, content, in_place_of)
336
+ target_file_content = File.read(file)
337
+
338
+ if target_file_content.include?(content)
339
+ puts "No need to update '#{file}'. It already has '#{content}'."
340
+ else
341
+ puts "Updating '#{file}'."
342
+ target_file_content.gsub!(in_place_of, content)
343
+ File.write(file, target_file_content)
344
+ end
345
+ end
346
+
347
+ def scaffold_replace_line_in_file(file, content, in_place_of)
348
+ file = transform_string(file)
349
+ # we specifically don't transform the content, we assume a builder function created this content.
350
+ in_place_of = transform_string(in_place_of)
351
+ replace_line_in_file(file, content, in_place_of)
352
+ end
353
+
354
+ # if class_name isn't specified, we use `child`.
355
+ # if class_name is specified, then `child` is assumed to be a parent of `class_name`.
356
+ # returns an array with the ability line and a boolean indicating whether the ability line should be inserted among
357
+ # the abilities for admins only. (this happens when building an ability line for a resources that doesn't ultimately
358
+ # belong to a Team or a User.)
359
+ def build_ability_line(class_names = nil)
360
+ # e.g. ['Conversations::Message', 'Conversation']
361
+ if class_names
362
+ # e.g. 'Conversations::Message'
363
+ class_name = class_names.shift
364
+ # e.g. ['Conversation', 'Deliverable', 'Phase', 'Project', 'Team']
365
+ working_parents = class_names + [child] + parents
366
+ else
367
+ # e.g. 'Deliverable'
368
+ class_name = child
369
+ # e.g. ['Phase', 'Project', 'Team']
370
+ working_parents = parents.dup
371
+ end
372
+
373
+ case working_parents.last
374
+ when "User"
375
+ working_parents.pop
376
+ ability_line = "user_id: user.id"
377
+ when "Team"
378
+ working_parents.pop
379
+ ability_line = "team_id: user.team_ids"
380
+ else
381
+ # if a resources is specified that isn't ultimately owned by a team or a user, then only admins can manage it.
382
+ return ["can :manage, #{class_name}", true]
383
+ end
384
+
385
+ # e.g. ['Phase', 'Project']
386
+ while working_parents.any?
387
+ current_parent = working_parents.pop
388
+ current_transformer = Scaffolding::ClassNamesTransformer.new(working_parents.last || class_name, current_parent, namespace)
389
+ ability_line = "#{current_transformer.parent_variable_name_in_context}: {#{ability_line}}"
390
+ end
391
+
392
+ # e.g. "can :manage, Deliverable, phase: {project: {team_id: user.team_ids}}"
393
+ ["can :manage, #{class_name}, #{ability_line}", false]
394
+ end
395
+
396
+ def build_conversation_ability_line
397
+ build_ability_line(["Conversations::Message", "Conversation"])
398
+ end
399
+
400
+ def add_scaffolding_hooks_to_model
401
+ before_scaffolding_hooks = <<~RUBY
402
+ #{CONCERNS_HOOK}
403
+
404
+ RUBY
405
+
406
+ after_scaffolding_hooks = <<-RUBY
407
+ #{BELONGS_TO_HOOK}
408
+
409
+ #{HAS_MANY_HOOK}
410
+
411
+ #{HAS_ONE_HOOK}
412
+
413
+ #{SCOPES_HOOK}
414
+
415
+ #{VALIDATIONS_HOOK}
416
+
417
+ #{CALLBACKS_HOOK}
418
+
419
+ #{DELEGATIONS_HOOK}
420
+
421
+ #{METHODS_HOOK}
422
+ RUBY
423
+
424
+ # add scaffolding hooks to the model.
425
+ unless File.readlines(transform_string("./app/models/scaffolding/completely_concrete/tangible_thing.rb")).join.include?(CONCERNS_HOOK)
426
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", before_scaffolding_hooks, "ApplicationRecord", increase_indent: true)
427
+ end
428
+
429
+ unless File.readlines(transform_string("./app/models/scaffolding/completely_concrete/tangible_thing.rb")).join.include?(BELONGS_TO_HOOK)
430
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", after_scaffolding_hooks, "end", prepend: true, increase_indent: true, exact_match: true)
431
+ end
432
+ end
433
+
434
+ def add_ability_line_to_roles_yml(class_names = nil)
435
+ model_names = class_names || [child]
436
+ role_file = "./config/models/roles.yml"
437
+ model_names.each do |model_name|
438
+ add_line_to_yml_file(role_file, "#{model_name}: read", [:default, :models])
439
+ add_line_to_yml_file(role_file, "#{model_name}: manage", [:admin, :models])
440
+ end
441
+ end
442
+
443
+ def build_factory_setup
444
+ class_name = child
445
+ working_parents = parents.dup
446
+ current_parent = working_parents.pop
447
+ current_transformer = Scaffolding::Transformer.new(working_parents.last || class_name, [current_parent])
448
+
449
+ setup_lines = []
450
+
451
+ unless current_parent == "Team" || current_parent == "User"
452
+ setup_lines << current_transformer.transform_string("@absolutely_abstract_creative_concept = create(:scaffolding_absolutely_abstract_creative_concept)")
453
+ end
454
+
455
+ previous_assignment = current_transformer.transform_string("absolutely_abstract_creative_concept: @absolutely_abstract_creative_concept")
456
+
457
+ current_parent = working_parents.pop
458
+
459
+ while current_parent
460
+ current_transformer = Scaffolding::Transformer.new(working_parents.last || class_name, [current_parent])
461
+ setup_lines << current_transformer.transform_string("@absolutely_abstract_creative_concept = create(:scaffolding_absolutely_abstract_creative_concept, #{previous_assignment})")
462
+ previous_assignment = current_transformer.transform_string("absolutely_abstract_creative_concept: @absolutely_abstract_creative_concept")
463
+
464
+ current_parent = working_parents.pop
465
+ end
466
+
467
+ setup_lines << current_transformer.transform_string("@tangible_thing = create(:scaffolding_completely_concrete_tangible_thing, #{previous_assignment})")
468
+
469
+ setup_lines
470
+ end
471
+
472
+ def replace_in_file(file, before, after, target_regexp = nil)
473
+ puts "Replacing in '#{file}'."
474
+ if target_regexp.present?
475
+ target_file_content = ""
476
+ File.open(file).each_line do |l|
477
+ l.gsub!(before, after) if !!l.match(target_regexp)
478
+ target_file_content += l
479
+ end
480
+ else
481
+ target_file_content = File.read(file)
482
+ target_file_content.gsub!(before, after)
483
+ end
484
+ File.write(file, target_file_content)
485
+ end
486
+
487
+ def restart_server
488
+ # restart the server.
489
+ puts "Restarting the server so it picks up the new localization .yml file."
490
+ `./bin/rails restart`
491
+ end
492
+
493
+ def add_locale_helper_export_fix
494
+ namespaced_locale_export_hook = "# 🚅 super scaffolding will insert the export for the locale view helper here."
495
+
496
+ spacer = " "
497
+ indentation = spacer * 3
498
+ namespace_elements = child.underscore.pluralize.split("/")
499
+ last_element = namespace_elements.shift
500
+ lines_to_add = [last_element + ":"]
501
+ namespace_elements.map do |namespace_element|
502
+ lines_to_add << indentation + namespace_element + ":"
503
+ last_element = namespace_element
504
+ indentation += spacer
505
+ end
506
+ lines_to_add << lines_to_add.pop + " *#{last_element}"
507
+
508
+ scaffold_replace_line_in_file("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml", lines_to_add.join("\n"), namespaced_locale_export_hook)
509
+ end
510
+
511
+ def scaffold_new_breadcrumbs(child, parents)
512
+ scaffold_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_breadcrumbs.html.erb")
513
+ puts
514
+ puts "Heads up! We're only able to generate the new breadcrumb views, so you'll have to edit `#{transform_string("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml")}` and add the label. You can look at `./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml` for an example of how to do this, but here's an example of what it should look like:".yellow
515
+ puts
516
+ puts transform_string("en:\n scaffolding/completely_concrete/tangible_things: &tangible_things\n label: &label Things\n breadcrumbs:\n label: *label").yellow
517
+ puts
518
+ end
519
+
520
+ def add_has_many_association
521
+ has_many_line = ["has_many :completely_concrete_tangible_things"]
522
+
523
+ # TODO I _think_ this is the right way to check for whether we need `class_name` to specify the name of the model.
524
+ unless transform_string("completely_concrete_tangible_things").classify == child
525
+ has_many_line << "class_name: \"Scaffolding::CompletelyConcrete::TangibleThing\""
526
+ end
527
+
528
+ has_many_line << "dependent: :destroy"
529
+
530
+ # TODO I _think_ this is the right way to check for whether we need `foreign_key` to specify the name of the model.
531
+ unless transform_string("absolutely_abstract_creative_concept_id") == "#{parent.underscore}_id"
532
+ has_many_line << "foreign_key: :absolutely_abstract_creative_concept_id"
533
+
534
+ # And if we need `foreign_key`, we should also specify `inverse_of`.
535
+ has_many_line << "inverse_of: :absolutely_abstract_creative_concept"
536
+ end
537
+
538
+ has_many_string = transform_string(has_many_line.join(", "))
539
+ add_line_to_file(transform_string("./app/models/scaffolding/absolutely_abstract/creative_concept.rb"), has_many_string, HAS_MANY_HOOK, prepend: true)
540
+
541
+ # Return the name of the has_many association.
542
+ has_many_string.split(",").first.split(":").last
543
+ end
544
+
545
+ def add_has_many_through_associations(has_many_through_transformer)
546
+ has_many_association = add_has_many_association
547
+ has_many_through_string = has_many_through_transformer.transform_string("has_many :completely_concrete_tangible_things, through: :$HAS_MANY_ASSOCIATION")
548
+ has_many_through_string.gsub!("$HAS_MANY_ASSOCIATION", has_many_association)
549
+ add_line_to_file(transform_string("./app/models/scaffolding/absolutely_abstract/creative_concept.rb"), has_many_through_string, HAS_MANY_HOOK, prepend: true)
550
+ end
551
+
552
+ def add_attributes_to_various_views(attributes, scaffolding_options = {})
553
+ sql_type_to_field_type_mapping = {
554
+ # 'binary' => '',
555
+ "boolean" => "buttons",
556
+ "date" => "date_field",
557
+ "datetime" => "date_and_time_field",
558
+ "decimal" => "text_field",
559
+ "float" => "text_field",
560
+ "integer" => "text_field",
561
+ "bigint" => "text_field",
562
+ # 'primary_key' => '',
563
+ # 'references' => '',
564
+ "string" => "text_field",
565
+ "text" => "text_area"
566
+ # 'time' => '',
567
+ # 'timestamp' => '',
568
+ }
569
+
570
+ # add attributes to various views.
571
+ attributes.each_with_index do |attribute, index|
572
+ first_table_cell = index == 0 && scaffolding_options[:type] == :crud
573
+
574
+ parts = attribute.split(":")
575
+ name = parts.shift
576
+ type = parts.join(":")
577
+ boolean_buttons = type == "boolean"
578
+
579
+ # extract any options they passed in with the field.
580
+ # will extract options declared with either [] or {}.
581
+ type, attribute_options = type.scan(/^(.*)[\[|{](.*)[\]|}]/).first || type
582
+
583
+ # create a hash of the options.
584
+ attribute_options = if attribute_options
585
+ attribute_options.split(",").map { |s|
586
+ option_name, option_value = s.split("=")
587
+ [option_name.to_sym, option_value || true]
588
+ }.to_h
589
+ else
590
+ {}
591
+ end
592
+
593
+ attribute_options[:label] ||= "label_string"
594
+
595
+ if sql_type_to_field_type_mapping[type]
596
+ type = sql_type_to_field_type_mapping[type]
597
+ end
598
+
599
+ is_id = name.match?(/_id$/)
600
+ is_ids = name.match?(/_ids$/)
601
+ # if this is the first attribute of a newly scaffolded model, that field is required.
602
+ is_required = attribute_options[:required] || (scaffolding_options[:type] == :crud && index == 0)
603
+ is_vanilla = attribute_options&.key?(:vanilla)
604
+ is_belongs_to = is_id && !is_vanilla
605
+ is_has_many = is_ids && !is_vanilla
606
+ is_multiple = attribute_options&.key?(:multiple) || is_has_many
607
+ is_association = is_belongs_to || is_has_many
608
+
609
+ # Sometimes we need all the magic of a `*_id` field, but without the scoping stuff.
610
+ # Possibly only ever used internally by `join-model`.
611
+ is_unscoped = attribute_options[:unscoped]
612
+
613
+ name_without_id = name.gsub(/_id$/, "")
614
+ name_without_ids = name.gsub(/_ids$/, "").pluralize
615
+ collection_name = is_ids ? name_without_ids : name_without_id.pluralize
616
+
617
+ # field on the show view.
618
+ attribute_partial ||= attribute_options[:attribute] || case type
619
+ when "trix_editor", "ckeditor"
620
+ "html"
621
+ when "buttons", "super_select", "options"
622
+ if boolean_buttons
623
+ "boolean"
624
+ elsif is_ids
625
+ "has_many"
626
+ elsif is_id
627
+ "belongs_to"
628
+ else
629
+ "option"
630
+ end
631
+ when "cloudinary_image"
632
+ attribute_options[:height] = 200
633
+ "image"
634
+ when "phone_field"
635
+ "phone_number"
636
+ when "date_field"
637
+ "date"
638
+ when "date_and_time_field"
639
+ "date_and_time"
640
+ when "email_field"
641
+ "email"
642
+ when "color_picker"
643
+ "code"
644
+ else
645
+ "text"
646
+ end
647
+
648
+ cell_attributes = if boolean_buttons
649
+ ' class="text-center"'
650
+ end
651
+
652
+ # e.g. from `person_id` to `person` or `person_ids` to `people`.
653
+ attribute_name = if is_ids
654
+ name_without_ids
655
+ elsif is_id
656
+ name_without_id
657
+ else
658
+ name
659
+ end
660
+
661
+ title_case = if is_ids
662
+ # user_ids should be 'Users'
663
+ name_without_ids.humanize.titlecase
664
+ elsif is_id
665
+ name_without_id.humanize.titlecase
666
+ else
667
+ name.humanize.titlecase
668
+ end
669
+
670
+ attribute_assignment = case type
671
+ when "text_field", "password_field", "text_area"
672
+ "'Alternative String Value'"
673
+ when "email_field"
674
+ "'another.email@test.com'"
675
+ when "phone_field"
676
+ "'+19053871234'"
677
+ end
678
+
679
+ # don't do table columns for certain types of fields and attribute partials
680
+ if ["trix_editor", "ckeditor", "text_area"].include?(type) || ["html", "has_many"].include?(attribute_partial)
681
+ cli_options["skip-table"] = true
682
+ end
683
+
684
+ if type == "none"
685
+ cli_options["skip-form"] = true
686
+ end
687
+
688
+ if attribute_partial == "none"
689
+ cli_options["skip-show"] = true
690
+ cli_options["skip-table"] = true
691
+ end
692
+
693
+ #
694
+ # MODEL VALIDATIONS
695
+ #
696
+
697
+ unless cli_options["skip-form"] || is_unscoped
698
+
699
+ file_name = "./app/models/scaffolding/completely_concrete/tangible_thing.rb"
700
+
701
+ if is_association
702
+ field_content = if attribute_options[:source]
703
+ <<~RUBY
704
+ def valid_#{collection_name}
705
+ #{attribute_options[:source]}
706
+ end
707
+
708
+ RUBY
709
+ else
710
+ add_additional_step :yellow, transform_string("You'll need to implement the `valid_#{collection_name}` method of `Scaffolding::CompletelyConcrete::TangibleThing` in `./app/models/scaffolding/completely_concrete/tangible_thing.rb`. This is the method that will be used to populate the `#{type}` field and also validate that users aren't trying to exploit multitenancy.")
711
+
712
+ <<~RUBY
713
+ def valid_#{collection_name}
714
+ raise "please review and implement `valid_#{collection_name}` in `app/models/scaffolding/completely_concrete/tangible_thing.rb`."
715
+ # please specify what objects should be considered valid for assigning to `#{name_without_id}`.
716
+ # the resulting code should probably look something like `team.#{collection_name}`.
717
+ end
718
+
719
+ RUBY
720
+ end
721
+
722
+ scaffold_add_line_to_file(file_name, field_content, METHODS_HOOK, prepend: true)
723
+
724
+ if is_belongs_to
725
+ scaffold_add_line_to_file(file_name, "validates :#{name_without_id}, scope: true", VALIDATIONS_HOOK, prepend: true)
726
+ end
727
+
728
+ # TODO we need to add a multitenancy check for has many associations.
729
+ end
730
+
731
+ end
732
+
733
+ #
734
+ # FORM FIELD
735
+ #
736
+
737
+ unless cli_options["skip-form"]
738
+
739
+ # add `has_rich_text` for trix editor fields.
740
+ if type == "trix_editor"
741
+ file_name = "./app/models/scaffolding/completely_concrete/tangible_thing.rb"
742
+ scaffold_add_line_to_file(file_name, "has_rich_text :#{name}", HAS_ONE_HOOK, prepend: true)
743
+ end
744
+
745
+ # field on the form.
746
+ file_name = "./app/views/account/scaffolding/completely_concrete/tangible_things/_form.html.erb"
747
+ field_attributes = {method: ":#{name}"}
748
+ field_options = {}
749
+
750
+ if scaffolding_options[:type] == :crud && index == 0
751
+ field_options[:autofocus] = "true"
752
+ end
753
+
754
+ if is_id && type == "super_select"
755
+ field_options[:include_blank] = "t('.fields.#{name}.placeholder')"
756
+ # add_additional_step :yellow, transform_string("We've added a reference to a `placeholder` to the form for the select or super_select field, but unfortunately earlier versions of the scaffolded locales Yaml don't include a reference to `fields: *fields` under `form`. Please add it, otherwise your form won't be able to locate the appropriate placeholder label.")
757
+ end
758
+
759
+ if is_multiple
760
+ field_options[:multiple] = "true"
761
+ end
762
+
763
+ valid_values = if is_id
764
+ "valid_#{name_without_id.pluralize}"
765
+ elsif is_ids
766
+ "valid_#{collection_name}"
767
+ end
768
+
769
+ # https://stackoverflow.com/questions/21582464/is-there-a-ruby-hashto-s-equivalent-for-the-new-hash-syntax
770
+ if field_options.any?
771
+ field_options_key = if ["buttons", "super_select", "options"].include?(type)
772
+ :html_options
773
+ else
774
+ :options
775
+ end
776
+ field_attributes[field_options_key] = "{" + field_options.map { |key, value| "#{key}: #{value}" }.join(", ") + "}"
777
+ end
778
+
779
+ if is_association
780
+ short = attribute_options[:class_name].underscore.split("/").last
781
+ case type
782
+ when "buttons", "options"
783
+ field_attributes["\n options"] = "@tangible_thing.#{valid_values}.map { |#{short}| [#{short}.id, #{short}.#{attribute_options[:label]}] }"
784
+ when "super_select"
785
+ field_attributes["\n choices"] = "@tangible_thing.#{valid_values}.map { |#{short}| [#{short}.#{attribute_options[:label]}, #{short}.id] }"
786
+ end
787
+ end
788
+
789
+ field_content = "<%= render 'shared/fields/#{type}'#{", " if field_attributes.any?}#{field_attributes.map { |key, value| "#{key}: #{value}" }.join(", ")} %>"
790
+ scaffold_add_line_to_file(file_name, field_content, ERB_NEW_FIELDS_HOOK, prepend: true)
791
+ end
792
+
793
+ #
794
+ # SHOW VIEW
795
+ #
796
+
797
+ unless cli_options["skip-show"]
798
+
799
+ if is_id
800
+ <<~ERB
801
+ <% if @tangible_thing.#{name_without_id} %>
802
+ <div class="form-group">
803
+ <label class="col-form-label"><%= t('.fields.#{name}.heading') %></label>
804
+ <div>
805
+ <%= link_to @tangible_thing.#{name_without_id}.#{attribute_options[:label]}, [:account, @tangible_thing.#{name_without_id}] %>
806
+ </div>
807
+ </div>
808
+ <% end %>
809
+ ERB
810
+ elsif is_ids
811
+ <<~ERB
812
+ <% if @tangible_thing.#{collection_name}.any? %>
813
+ <div class="form-group">
814
+ <label class="col-form-label"><%= t('.fields.#{name}.heading') %></label>
815
+ <div>
816
+ <%= @tangible_thing.#{collection_name}.map { |#{name_without_ids}| link_to #{name_without_ids}.#{attribute_options[:label]}, [:account, #{name_without_ids}] }.to_sentence.html_safe %>
817
+ </div>
818
+ </div>
819
+ <% end %>
820
+ ERB
821
+ end
822
+
823
+ # this gets stripped and is one line, so indentation isn't a problem.
824
+ field_content = <<-ERB
825
+ <%= render 'shared/attributes/#{attribute_partial}', attribute: :#{attribute_name} %>
826
+ ERB
827
+
828
+ scaffold_add_line_to_file("./app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb", field_content.strip, ERB_NEW_FIELDS_HOOK, prepend: true)
829
+
830
+ end
831
+
832
+ #
833
+ # INDEX TABLE
834
+ #
835
+
836
+ unless cli_options["skip-table"]
837
+
838
+ # table header.
839
+ field_content = "<th#{cell_attributes.present? ? " " + cell_attributes : ""}><%= t('.fields.#{attribute_name}.heading') %></th>"
840
+
841
+ unless ["Team", "User"].include?(child)
842
+ scaffold_add_line_to_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb", field_content, "<%# 🚅 super scaffolding will insert new field headers above this line. %>", prepend: true)
843
+ end
844
+
845
+ table_cell_options = []
846
+
847
+ if first_table_cell
848
+ table_cell_options << "url: [:account, tangible_thing]"
849
+ end
850
+
851
+ # this gets stripped and is one line, so indentation isn't a problem.
852
+ field_content = <<-ERB
853
+ <td#{cell_attributes}><%= render 'shared/attributes/#{attribute_partial}', attribute: :#{attribute_name}#{", #{table_cell_options.join(", ")}" if table_cell_options.any?} %></td>
854
+ ERB
855
+
856
+ unless ["Team", "User"].include?(child)
857
+ scaffold_add_line_to_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb", field_content.strip, ERB_NEW_FIELDS_HOOK, prepend: true)
858
+ end
859
+
860
+ end
861
+
862
+ #
863
+ # LOCALIZATIONS
864
+ #
865
+
866
+ unless cli_options["skip-locales"]
867
+
868
+ yaml_template = <<~YAML
869
+
870
+ <%= name %>: <% if is_association %>&<%= attribute_name %><% end %>
871
+ _: &#{name} #{title_case}
872
+ label: *#{name}
873
+ heading: *#{name}
874
+
875
+ <% if type == "super_select" %>
876
+ <% if is_required %>
877
+ placeholder: Select <% title_case.with_indefinite_article %>
878
+ <% else %>
879
+ placeholder: None
880
+ <% end %>
881
+ <% end %>
882
+
883
+ <% if boolean_buttons %>
884
+
885
+ options:
886
+ yes: "Yes"
887
+ no: "No"
888
+
889
+ <% elsif ["buttons", "super_select", "options"].include?(type) && !is_association %>
890
+
891
+ options:
892
+ one: One
893
+ two: Two
894
+ three: Three
895
+
896
+ <% end %>
897
+
898
+ <% if is_association %>
899
+ <%= attribute_name %>: *<%= attribute_name %>
900
+ <% end %>
901
+ YAML
902
+
903
+ field_content = ERB.new(yaml_template).result(binding).lines.select(&:present?).join
904
+
905
+ scaffold_add_line_to_file("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml", field_content, RUBY_NEW_FIELDS_HOOK, prepend: true)
906
+
907
+ # active record's field label.
908
+ scaffold_add_line_to_file("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml", "#{name}: *#{name}", "# 🚅 super scaffolding will insert new activerecord attributes above this line.", prepend: true)
909
+
910
+ end
911
+
912
+ #
913
+ # STRONG PARAMETERS
914
+ #
915
+
916
+ unless cli_options["skip-form"]
917
+
918
+ # add attributes to strong params.
919
+ [
920
+ "./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb"
921
+ ].each do |file|
922
+ if is_ids
923
+ scaffold_add_line_to_file(file, "#{name}: [],", RUBY_NEW_ARRAYS_HOOK, prepend: true)
924
+ else
925
+ scaffold_add_line_to_file(file, ":#{name},", RUBY_NEW_FIELDS_HOOK, prepend: true)
926
+ end
927
+ end
928
+
929
+ special_processing = case type
930
+ when "date_field"
931
+ "assign_date(strong_params, :#{name})"
932
+ when "date_and_time_field"
933
+ "assign_date_and_time(strong_params, :#{name})"
934
+ when "buttons"
935
+ if boolean_buttons
936
+ "assign_boolean(strong_params, :#{name})"
937
+ elsif is_multiple
938
+ "assign_checkboxes(strong_params, :#{name})"
939
+ end
940
+ when "super_select"
941
+ if boolean_buttons
942
+ "assign_boolean(strong_params, :#{name})"
943
+ elsif is_multiple
944
+ "assign_select_options(strong_params, :#{name})"
945
+ end
946
+ end
947
+
948
+ scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", special_processing, RUBY_NEW_FIELDS_PROCESSING_HOOK, prepend: true) if special_processing
949
+ end
950
+
951
+ #
952
+ # API ENDPOINT
953
+ #
954
+
955
+ unless cli_options["skip-api"]
956
+
957
+ # add attributes to endpoint.
958
+ if name.match?(/_ids$/)
959
+ scaffold_add_line_to_file("./app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint.rb", "optional :#{name}, type: Array, desc: Api.heading(:#{name})", RUBY_NEW_ARRAYS_HOOK, prepend: true)
960
+ else
961
+ api_type = case type
962
+ when "date_field"
963
+ "Date"
964
+ when "date_and_time_field"
965
+ "DateTime"
966
+ when "buttons"
967
+ if boolean_buttons
968
+ "Boolean"
969
+ else
970
+ "String"
971
+ end
972
+ when "file_field"
973
+ "File"
974
+ else
975
+ "String"
976
+ end
977
+
978
+ scaffold_add_line_to_file("./app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint.rb", "optional :#{name}, type: #{api_type}, desc: Api.heading(:#{name})", RUBY_NEW_FIELDS_HOOK, prepend: true)
979
+ end
980
+
981
+ end
982
+
983
+ #
984
+ # API SERIALIZER
985
+ #
986
+
987
+ unless cli_options["skip-api"]
988
+
989
+ # TODO The serializers can't handle these `has_rich_text` attributes.
990
+ unless type == "trix_editor"
991
+ [
992
+ "./app/views/account/scaffolding/completely_concrete/tangible_things/_tangible_thing.json.jbuilder",
993
+ "./app/serializers/api/v1/scaffolding/completely_concrete/tangible_thing_serializer.rb"
994
+ ].each do |file|
995
+ scaffold_add_line_to_file(file, ":#{name},", RUBY_NEW_FIELDS_HOOK, prepend: true)
996
+ end
997
+
998
+ assertion = if type == "date_field"
999
+ "assert_equal Date.parse(tangible_thing_data['#{name}']), tangible_thing.#{name}"
1000
+ elsif type == "date_and_time_field"
1001
+ "assert_equal DateTime.parse(tangible_thing_data['#{name}']), tangible_thing.#{name}"
1002
+ else
1003
+ "assert_equal tangible_thing_data['#{name}'], tangible_thing.#{name}"
1004
+ end
1005
+ scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint_test.rb", assertion, RUBY_NEW_FIELDS_HOOK, prepend: true)
1006
+ end
1007
+
1008
+ # scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb", "assert_equal tangible_thing_attributes['#{name.gsub('_', '-')}'], tangible_thing.#{name}", RUBY_NEW_FIELDS_HOOK, prepend: true)
1009
+
1010
+ if attribute_assignment
1011
+ scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint_test.rb", "#{name}: #{attribute_assignment},", RUBY_ADDITIONAL_NEW_FIELDS_HOOK, prepend: true)
1012
+ scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint_test.rb", "assert_equal @tangible_thing.#{name}, #{attribute_assignment}", RUBY_EVEN_MORE_NEW_FIELDS_HOOK, prepend: true)
1013
+ end
1014
+ end
1015
+
1016
+ #
1017
+ # MODEL ASSOCATIONS
1018
+ #
1019
+
1020
+ unless cli_options["skip-model"]
1021
+
1022
+ if is_belongs_to
1023
+ unless attribute_options[:class_name]
1024
+ attribute_options[:class_name] = name_without_id.classify
1025
+ end
1026
+
1027
+ file_name = "app/models/#{attribute_options[:class_name].underscore}.rb"
1028
+ unless File.exist?(file_name)
1029
+ raise "You'll need to specify a `class_name` option for `#{name}` because there is no `#{attribute_options[:class_name].classify}` model defined in `#{file_name}`. Try again with `#{name}:#{type}[class_name=SomeClassName]`."
1030
+ end
1031
+
1032
+ modified_migration = false
1033
+
1034
+ # find the database migration that defines this relationship.
1035
+ expected_reference = "add_reference :#{class_names_transformer.table_name}, :#{name_without_id}"
1036
+ migration_file_name = `grep "#{expected_reference}" db/migrate/*`.split(":").first
1037
+
1038
+ # if that didn't work, see if we can find a creation of the reference when the table was created.
1039
+ unless migration_file_name
1040
+ confirmation_reference = "create_table :#{class_names_transformer.table_name}"
1041
+ confirmation_migration_file_name = `grep "#{confirmation_reference}" db/migrate/*`.split(":").first
1042
+
1043
+ fallback_reference = "t.references :#{name_without_id}"
1044
+ fallback_migration_file_name = `grep "#{fallback_reference}" db/migrate/* | grep #{confirmation_migration_file_name}`.split(":").first
1045
+
1046
+ if fallback_migration_file_name == confirmation_migration_file_name
1047
+ migration_file_name = fallback_migration_file_name
1048
+ end
1049
+ end
1050
+
1051
+ unless is_required
1052
+
1053
+ if migration_file_name
1054
+ replace_in_file(migration_file_name, ":#{name_without_id}, null: false", ":#{name_without_id}, null: true")
1055
+ modified_migration = true
1056
+ else
1057
+ add_additional_step :yellow, "We would have expected there to be a migration that defined `#{expected_reference}`, but we didn't find one. Where was the reference added to this model? It's _probably_ the original creation of the table, but we couldn't find that either. Either way, you need to rollback, change 'null: false' to 'null: true' for this column, and re-run the migration (unless, of course, that attribute _is_ required, then you need to add a validation on the model)."
1058
+ end
1059
+
1060
+ end
1061
+
1062
+ class_name_matches = name_without_id.tableize == attribute_options[:class_name].tableize.tr("/", "_")
1063
+
1064
+ # but also, if namespaces are involved, just don't...
1065
+ if attribute_options[:class_name].include?("::")
1066
+ class_name_matches = false
1067
+ end
1068
+
1069
+ # unless the table name matches the association name.
1070
+ unless class_name_matches
1071
+ if migration_file_name
1072
+ # There are two forms this association creation can take.
1073
+ replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: \"#{attribute_options[:class_name].tableize.tr("/", "_")}\"}", /t\.references :#{name_without_id}/)
1074
+ replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: \"#{attribute_options[:class_name].tableize.tr("/", "_")}\"}", /add_reference :#{child.underscore.pluralize.tr("/", "_")}, :#{name_without_id}/)
1075
+
1076
+ # TODO also solve the 60 character long index limitation.
1077
+ modified_migration = true
1078
+ else
1079
+ add_additional_step :yellow, "We would have expected there to be a migration that defined `#{expected_reference}`, but we didn't find one. Where was the reference added to this model? It's _probably_ the original creation of the table. Either way, you need to rollback, change \"foreign_key: true\" to \"foreign_key: {to_table: '#{attribute_options[:class_name].tableize.tr("/", "_")}'}\" for this column, and re-run the migration."
1080
+ end
1081
+ end
1082
+
1083
+ optional_line = ", optional: true" unless is_required
1084
+
1085
+ # if the `belongs_to` is already there from `rails g model`..
1086
+ scaffold_replace_line_in_file(
1087
+ "./app/models/scaffolding/completely_concrete/tangible_thing.rb",
1088
+ class_name_matches ?
1089
+ "belongs_to :#{name_without_id}#{optional_line}" :
1090
+ "belongs_to :#{name_without_id}, class_name: \"#{attribute_options[:class_name]}\"#{optional_line}",
1091
+ "belongs_to :#{name_without_id}"
1092
+ )
1093
+
1094
+ # if it wasn't there, the replace will not have done anything, so we insert it entirely.
1095
+ # however, this won't do anything if the association is already there.
1096
+ scaffold_add_line_to_file(
1097
+ "./app/models/scaffolding/completely_concrete/tangible_thing.rb",
1098
+ class_name_matches ?
1099
+ "belongs_to :#{name_without_id}#{optional_line}" :
1100
+ "belongs_to :#{name_without_id}, class_name: \"#{attribute_options[:class_name]}\"#{optional_line}",
1101
+ BELONGS_TO_HOOK,
1102
+ prepend: true
1103
+ )
1104
+
1105
+ if modified_migration
1106
+ add_additional_step :yellow, "If you've already run the migration in `#{migration_file_name}`, you'll need to roll back and run it again."
1107
+ end
1108
+ end
1109
+
1110
+ end
1111
+
1112
+ #
1113
+ # MODEL HOOKS
1114
+ #
1115
+
1116
+ unless cli_options["skip-model"]
1117
+
1118
+ if is_required && !is_belongs_to
1119
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "validates :#{name}, presence: true", VALIDATIONS_HOOK, prepend: true)
1120
+ end
1121
+
1122
+ case type
1123
+ when "file_field"
1124
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_one_attached :#{name}", HAS_ONE_HOOK, prepend: true)
1125
+ when "trix_editor"
1126
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_rich_text :#{name}", HAS_ONE_HOOK, prepend: true)
1127
+ end
1128
+
1129
+ end
1130
+ end
1131
+ end
1132
+
1133
+ def add_additional_step(color, message)
1134
+ additional_steps.push [color, message]
1135
+ end
1136
+
1137
+ def scaffold_crud(attributes)
1138
+ if cli_options["only-index"]
1139
+ cli_options["skip-table"] = false
1140
+ cli_options["skip-views"] = true
1141
+ cli_options["skip-controller"] = true
1142
+ cli_options["skip-form"] = true
1143
+ cli_options["skip-show"] = true
1144
+ cli_options["skip-form"] = true
1145
+ cli_options["skip-api"] = true
1146
+ cli_options["skip-model"] = true
1147
+ cli_options["skip-parent"] = true
1148
+ cli_options["skip-locales"] = true
1149
+ cli_options["skip-routes"] = true
1150
+ end
1151
+
1152
+ if cli_options["namespace"]
1153
+ cli_options["skip-api"] = true
1154
+ cli_options["skip-model"] = true
1155
+ cli_options["skip-locales"] = true
1156
+ end
1157
+
1158
+ # TODO fix this. we can do this better.
1159
+ files = if cli_options["only-index"]
1160
+ [
1161
+ "./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb",
1162
+ "./app/views/account/scaffolding/completely_concrete/tangible_things/index.html.erb"
1163
+ ]
1164
+ else
1165
+ # copy a ton of files over and do the appropriate string replace.
1166
+ [
1167
+ "./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb",
1168
+ "./app/views/account/scaffolding/completely_concrete/tangible_things",
1169
+ ("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml" unless cli_options["skip-locales"]),
1170
+ ("./app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint.rb" unless cli_options["skip-api"]),
1171
+ ("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint_test.rb" unless cli_options["skip-api"]),
1172
+ ("./app/serializers/api/v1/scaffolding/completely_concrete/tangible_thing_serializer.rb" unless cli_options["skip-api"])
1173
+ # "./app/filters/scaffolding/completely_concrete/tangible_things_filter.rb"
1174
+ ].compact
1175
+ end
1176
+
1177
+ files.each do |name|
1178
+ if File.directory?(name)
1179
+ scaffold_directory(name)
1180
+ else
1181
+ scaffold_file(name)
1182
+ end
1183
+ end
1184
+
1185
+ unless cli_options["skip-api"]
1186
+
1187
+ # add endpoint to the api.
1188
+ scaffold_add_line_to_file("./app/controllers/api/v1/root.rb", "mount Api::V1::Scaffolding::CompletelyConcrete::TangibleThingsEndpoint", ENDPOINTS_HOOK, prepend: true)
1189
+
1190
+ end
1191
+
1192
+ unless cli_options["skip-model"]
1193
+ # find the database migration that defines this relationship.
1194
+ migration_file_name = `grep "create_table :#{class_names_transformer.table_name} do |t|" db/migrate/*`.split(":").first
1195
+ unless migration_file_name.present?
1196
+ raise "No migration file seems to exist for creating the table `#{class_names_transformer.table_name}`.\n" \
1197
+ "Please run the following command first and try Super Scaffolding again:\n" \
1198
+ "rails generate model #{child} #{parent.downcase!}:references #{attributes.join(" ")}"
1199
+ end
1200
+
1201
+ # if needed, update the reference to the parent class name in the create_table migration
1202
+ current_transformer = Scaffolding::ClassNamesTransformer.new(child, parent, namespace)
1203
+ unless current_transformer.parent_variable_name_in_context.pluralize == current_transformer.parent_table_name
1204
+ replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: '#{current_transformer.parent_table_name}'}")
1205
+ end
1206
+
1207
+ # update the factory generated by `rails g`.
1208
+ content = if transform_string(":absolutely_abstract_creative_concept") == transform_string(":scaffolding_absolutely_abstract_creative_concept")
1209
+ transform_string("association :absolutely_abstract_creative_concept")
1210
+ else
1211
+ transform_string("association :absolutely_abstract_creative_concept, factory: :scaffolding_absolutely_abstract_creative_concept")
1212
+ end
1213
+ scaffold_replace_line_in_file("./test/factories/scaffolding/completely_concrete/tangible_things.rb", content, "absolutely_abstract_creative_concept { nil }")
1214
+
1215
+ add_has_many_association
1216
+
1217
+ if class_names_transformer.belongs_to_needs_class_definition?
1218
+ scaffold_replace_line_in_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", transform_string("belongs_to :absolutely_abstract_creative_concept, class_name: \"Scaffolding::AbsolutelyAbstract::CreativeConcept\"\n"), transform_string("belongs_to :absolutely_abstract_creative_concept\n"))
1219
+ end
1220
+
1221
+ # add user permissions.
1222
+ add_ability_line_to_roles_yml
1223
+ end
1224
+
1225
+ unless cli_options["skip-api"]
1226
+ scaffold_replace_line_in_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_endpoint_test.rb", build_factory_setup.join("\n"), "# 🚅 super scaffolding will insert factory setup in place of this line.")
1227
+ end
1228
+
1229
+ # add children to the show page of their parent.
1230
+ unless cli_options["skip-parent"] || parent == "None"
1231
+ scaffold_add_line_to_file("./app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb", "<%= render 'account/scaffolding/completely_concrete/tangible_things/index', tangible_things: @creative_concept.completely_concrete_tangible_things, hide_back: true %>", "<%# 🚅 super scaffolding will insert new children above this line. %>", prepend: true)
1232
+ end
1233
+
1234
+ unless cli_options["skip-model"]
1235
+ add_scaffolding_hooks_to_model
1236
+ end
1237
+
1238
+ #
1239
+ # DELEGATIONS
1240
+ #
1241
+
1242
+ unless cli_options["skip-model"]
1243
+
1244
+ if ["Team", "User"].include?(parents.last) && parent != parents.last
1245
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_one :#{parents.last.underscore}, through: :absolutely_abstract_creative_concept", HAS_ONE_HOOK, prepend: true)
1246
+ end
1247
+
1248
+ end
1249
+
1250
+ add_attributes_to_various_views(attributes, type: :crud)
1251
+
1252
+ unless cli_options["skip-locales"]
1253
+ add_locale_helper_export_fix
1254
+ end
1255
+
1256
+ # add sortability.
1257
+ if cli_options["sortable"]
1258
+ unless cli_options["skip-model"]
1259
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "def collection\n absolutely_abstract_creative_concept.completely_concrete_tangible_things\nend\n\n", METHODS_HOOK, prepend: true)
1260
+ scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "include Sortable\n", CONCERNS_HOOK, prepend: true)
1261
+ end
1262
+
1263
+ unless cli_options["skip-table"]
1264
+ scaffold_replace_line_in_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb", transform_string("<tbody data-reorder=\"<%= url_for [:reorder, :account, context, collection] %>\">"), "<tbody>")
1265
+ end
1266
+
1267
+ unless cli_options["skip-controller"]
1268
+ scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", "include SortableActions\n", "Account::ApplicationController", increase_indent: true)
1269
+ end
1270
+ end
1271
+
1272
+ # titleize the localization file.
1273
+ unless cli_options["skip-locales"]
1274
+ replace_in_file(transform_string("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml"), child, child.underscore.humanize.titleize)
1275
+ end
1276
+
1277
+ # apply routes.
1278
+ unless cli_options["skip-routes"]
1279
+ routes_namespace = cli_options["namespace"] || "account"
1280
+
1281
+ begin
1282
+ routes_path = if routes_namespace == "account"
1283
+ "config/routes.rb"
1284
+ else
1285
+ "config/routes/#{routes_namespace}.rb"
1286
+ end
1287
+ routes_manipulator = Scaffolding::RoutesFileManipulator.new(routes_path, child, parent, cli_options)
1288
+ rescue Errno::ENOENT => _
1289
+ puts "Creating '#{routes_path}'.".green
1290
+
1291
+ unless File.directory?("config/routes")
1292
+ FileUtils.mkdir_p("config/routes")
1293
+ end
1294
+
1295
+ File.write(routes_path, <<~RUBY)
1296
+ collection_actions = [:index, :new, :create]
1297
+
1298
+ # 🚅 Don't remove this block, it will break Super Scaffolding.
1299
+ begin do
1300
+ namespace :#{routes_namespace} do
1301
+ shallow do
1302
+ resources :teams do
1303
+ end
1304
+ end
1305
+ end
1306
+ end
1307
+ RUBY
1308
+
1309
+ retry
1310
+ end
1311
+
1312
+ begin
1313
+ routes_manipulator.apply([routes_namespace])
1314
+ rescue
1315
+ add_additional_step :yellow, "We weren't able to automatically add your `#{routes_namespace}` routes for you. In theory this should be very rare, so if you could reach out on Slack, you could probably provide context that will help us fix whatever the problem was. In the meantime, to add the routes manually, we've got a guide at https://blog.bullettrain.co/nested-namespaced-rails-routing-examples/ ."
1316
+ end
1317
+
1318
+ routes_manipulator.write
1319
+ end
1320
+
1321
+ unless cli_options["skip-parent"]
1322
+
1323
+ if parent == "Team" || parent == "None"
1324
+ icon_name = nil
1325
+ if cli_options["sidebar"].present?
1326
+ icon_name = cli_options["sidebar"]
1327
+ else
1328
+ puts ""
1329
+ puts "Hey, models that are scoped directly off of a Team (or nothing) are eligible to be added to the sidebar. Do you want to add this resource to the sidebar menu? (y/N)"
1330
+ response = $stdin.gets.chomp
1331
+ if response.downcase[0] == "y"
1332
+ puts ""
1333
+ puts "OK, great! Let's do this! By default these menu items appear with a puzzle piece, but after you hit enter I'll open two different pages where you can view other icon options. When you find one you like, hover your mouse over it and then come back here and and enter the name of the icon you want to use. (Or hit enter to skip this step.)"
1334
+ $stdin.gets.chomp
1335
+ if `which open`.present?
1336
+ `open https://themify.me/themify-icons`
1337
+ `open https://fontawesome.com/icons?d=gallery&s=light`
1338
+ else
1339
+ puts "Sorry! We can't open these URLs automatically on your platform, but you can visit them manually:"
1340
+ puts ""
1341
+ puts " https://themify.me/themify-icons"
1342
+ puts " https://fontawesome.com/icons?d=gallery&s=light"
1343
+ puts ""
1344
+ end
1345
+ puts ""
1346
+ puts "Did you find an icon you wanted to use? Enter the full CSS class here (e.g. 'ti ti-globe' or 'fal fa-puzzle-piece') or hit enter to just use the puzzle piece:"
1347
+ icon_name = $stdin.gets.chomp
1348
+ puts ""
1349
+ unless icon_name.length > 0 || icon_name.downcase == "y"
1350
+ icon_name = "fal fa-puzzle-piece ti ti-gift"
1351
+ end
1352
+ end
1353
+ end
1354
+ if icon_name.present?
1355
+ replace_in_file(transform_string("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml"), "fal fa-puzzle-piece", icon_name)
1356
+ scaffold_add_line_to_file("./app/views/account/shared/_menu.html.erb", "<%= render 'account/scaffolding/completely_concrete/tangible_things/menu_item' %>", "<% # added by super scaffolding. %>")
1357
+ end
1358
+ end
1359
+ end
1360
+
1361
+ add_additional_step :yellow, transform_string("If you would like the table view you've just generated to reactively update when a Tangible Thing is updated on the server, please edit `app/models/scaffolding/absolutely_abstract/creative_concept.rb`, locate the `has_many :completely_concrete_tangible_things`, and add `enable_updates: true` to it.")
1362
+
1363
+ restart_server
1364
+ end
1365
+ end