require "indefinite_article"
require "yaml"
require "scaffolding/file_manipulator"
require "scaffolding/class_names_transformer"
require "scaffolding/attribute"
class Scaffolding::Transformer
attr_accessor :child, :parent, :parents, :class_names_transformer, :cli_options, :additional_steps, :namespace, :suppress_could_not_find
def update_models_abstract_class
end
def created_by_reference(created_by_index_name)
end
def approved_by_reference(approved_by_index_name)
end
def permit_parents
["Team"]
end
def last_joinable_parent
"Team"
end
def top_level_model?
parent == "Team" || no_parent?
end
# We write an explicit method here so we know we
# aren't handling `parent` in this situation as `nil`.
def no_parent?
parent == "None"
end
def update_action_models_abstract_class(targets_n)
end
def initialize(child, parents, cli_options = {})
self.child = child
self.parent = parents.first
self.parents = parents
self.namespace = cli_options["namespace"] || "account"
self.class_names_transformer = Scaffolding::ClassNamesTransformer.new(child, parent, namespace)
self.cli_options = cli_options
self.additional_steps = []
end
RUBY_NEW_FIELDS_PROCESSING_HOOK = "# 🚅 super scaffolding will insert processing for new fields above this line."
RUBY_NEW_ARRAYS_HOOK = "# 🚅 super scaffolding will insert new arrays above this line."
RUBY_NEW_FIELDS_HOOK = "# 🚅 super scaffolding will insert new fields above this line."
RUBY_ADDITIONAL_NEW_FIELDS_HOOK = "# 🚅 super scaffolding will also insert new fields above this line."
RUBY_EVEN_MORE_NEW_FIELDS_HOOK = "# 🚅 super scaffolding will additionally insert new fields above this line."
RUBY_NEW_API_VERSION_HOOK = "# 🚅 super scaffolding will insert new api versions above this line."
RUBY_FILES_HOOK = "# 🚅 super scaffolding will insert file-related logic above this line."
RUBY_FACTORY_SETUP_HOOK = "# 🚅 super scaffolding will insert factory setup in place of this line."
ERB_NEW_FIELDS_HOOK = "<%#{RUBY_NEW_FIELDS_HOOK} %>"
CONCERNS_HOOK = "# 🚅 add concerns above."
ATTR_ACCESSORS_HOOK = "# 🚅 add attribute accessors above."
BELONGS_TO_HOOK = "# 🚅 add belongs_to associations above."
HAS_MANY_HOOK = "# 🚅 add has_many associations above."
OAUTH_PROVIDERS_HOOK = "# 🚅 add oauth providers above."
HAS_ONE_HOOK = "# 🚅 add has_one associations above."
SCOPES_HOOK = "# 🚅 add scopes above."
VALIDATIONS_HOOK = "# 🚅 add validations above."
CALLBACKS_HOOK = "# 🚅 add callbacks above."
DELEGATIONS_HOOK = "# 🚅 add delegations above."
METHODS_HOOK = "# 🚅 add methods above."
def encode_double_replacement_fix(string)
string.chars.join("~!@BT@!~")
end
def decode_double_replacement_fix(string)
string.gsub("~!@BT@!~", "")
end
def transform_string(string)
full_class_name = [
"Scaffolding::AbsolutelyAbstract::CreativeConcepts",
"Scaffolding::CompletelyConcrete::TangibleThings",
"ScaffoldingAbsolutelyAbstractCreativeConcepts",
"ScaffoldingCompletelyConcreteTangibleThings",
"Scaffolding Absolutely Abstract Creative Concepts",
"Scaffolding Completely Concrete Tangible Things",
"Scaffolding/Absolutely Abstract/Creative Concepts",
"Scaffolding/Completely Concrete/Tangible Things",
"scaffolding/absolutely_abstract/creative_concepts",
"scaffolding/completely_concrete/tangible_things",
"scaffolding/completely_concrete/_tangible_things",
"scaffolding_absolutely_abstract_creative_concepts",
"scaffolding_completely_concrete_tangible_things",
"scaffolding-absolutely-abstract-creative-concepts",
"scaffolding-completely-concrete-tangible-things",
"scaffolding.completely_concrete.tangible_things"
]
class_name_with_context = [
"absolutely_abstract_creative_concepts",
"completely_concrete_tangible_things",
"absolutely_abstract/creative_concepts",
"completely_concrete/tangible_things",
"absolutely-abstract-creative-concepts",
"completely-concrete-tangible-things"
]
class_name = [
"creative_concepts",
"tangible_things",
"creative-concepts",
"tangible-things",
"Creative Concepts",
"Tangible Things",
"Creative concepts",
"Tangible things",
"creative concepts",
"tangible things"
]
(
full_class_name + full_class_name.map(&:singularize) +
class_name_with_context + class_name_with_context.map(&:singularize) +
class_name + class_name.map(&:singularize) +
[":account", "/account/"] # Account namespace vs. others.
).each do |needle|
string = string.gsub(needle, encode_double_replacement_fix(class_names_transformer.replacement_for(needle)))
end
{
"/v1/" => "/#{BulletTrain::Api.current_version}/",
"::V1::" => "::#{BulletTrain::Api.current_version}::",
"_v1_" => "_#{BulletTrain::Api.current_version}_",
":v1," => ":#{BulletTrain::Api.current_version},"
}.each do |from, to|
string = string.gsub(from.upcase, encode_double_replacement_fix(to.upcase))
string = string.gsub(from.downcase, encode_double_replacement_fix(to.downcase))
end
decode_double_replacement_fix(string)
end
def resolve_template_path(file)
# Figure out the actual location of the file.
BulletTrain::SuperScaffolding.template_paths.map do |base_path|
base_path = Pathname.new(base_path)
resolved_path = base_path.join(file).to_s
File.exist?(resolved_path) ? resolved_path : nil
end.compact.first || raise("Couldn't find the Super Scaffolding template for `#{file}` in any of the following locations:\n\n#{BulletTrain::SuperScaffolding.template_paths.join("\n")}")
end
def resolve_target_path(file)
# Only do something here if they are trying to specify a target directory.
return file unless ENV["TARGET"]
# If the file exists in the application repository, we want to target it there.
return file if File.exist?(file)
ENV["OTHER_TARGETS"]&.split(",")&.each do |possible_target|
candidate_path = "#{possible_target}/#{file}".gsub("//", "/")
return candidate_path if File.exist?(candidate_path)
end
"#{ENV["TARGET"]}/#{file}".gsub("//", "/")
end
def get_transformed_file_content(file)
transformed_file_content = []
skipping = false
gathering_lines_to_repeat = false
parents_to_repeat_for = []
gathered_lines_for_repeating = nil
File.open(resolve_template_path(file)).each_line do |line|
if line.include?("# 🚅 skip when scaffolding.")
next
end
if line.include?("# 🚅 skip this section if resource is nested directly under team.")
skipping = true if parent == "Team"
next
end
if line.include?("# 🚅 skip this section when scaffolding.")
skipping = true
next
end
if line.include?("# 🚅 stop any skipping we're doing now.")
skipping = false
next
end
if line.include?("# 🚅 for each child resource from team down to the resource we're scaffolding, repeat the following:")
gathering_lines_to_repeat = true
parents_to_repeat_for = ([child] + parents.dup).reverse
gathered_lines_for_repeating = []
next
end
if line.include?("# 🚅 stop repeating.")
gathering_lines_to_repeat = false
while parents_to_repeat_for.count > 1
current_parent = parents_to_repeat_for[0]
current_child = parents_to_repeat_for[1]
current_transformer = self.class.new(current_child, current_parent)
transformed_file_content << current_transformer.transform_string(gathered_lines_for_repeating.join)
parents_to_repeat_for.shift
end
next
end
if gathering_lines_to_repeat
gathered_lines_for_repeating << line
next
end
if skipping
next
end
# remove lines with 'remove in scaffolded files.'
unless line.include?("remove in scaffolded files.")
# only transform it if it doesn't have the lock emoji.
if line.include?("🔒")
# remove any comments that start with a lock.
line.gsub!(/\s+?#\s+🔒.*/, "")
else
line = transform_string(line)
end
transformed_file_content << line
end
end
transformed_file_content.join
end
def scaffold_file(file, overrides: false)
transformed_file_content = get_transformed_file_content(file)
transformed_file_name = resolve_target_path(transform_string(file))
# Remove `_overrides` from the file name if we're sourcing from a local override folder.
transformed_file_name.gsub!("_overrides", "") if overrides
transformed_directory_name = File.dirname(transformed_file_name)
unless File.directory?(transformed_directory_name)
FileUtils.mkdir_p(transformed_directory_name)
end
puts "Writing '#{transformed_file_name}'." unless silence_logs?
File.write(transformed_file_name, transformed_file_content.strip + "\n")
if transformed_file_name.split(".").last == "rb"
puts "Fixing Standard Ruby on '#{transformed_file_name}'." unless silence_logs?
# `standardrb --fix #{transformed_file_name} 2> /dev/null`
end
end
def scaffold_directory(directory)
transformed_directory_name = transform_string(directory)
begin
Dir.mkdir(transformed_directory_name)
rescue Errno::EEXIST => _
puts "The directory #{transformed_directory_name} already exists, skipping generation.".yellow
rescue Errno::ENOENT => _
puts "Proceeding to generate '#{transformed_directory_name}'."
end
Dir.foreach(resolve_template_path(directory)) do |file|
file = "#{directory}/#{file}"
next if file.match?("/_menu_item.html.erb") && !top_level_model?
unless File.directory?(resolve_template_path(file))
scaffold_file(file)
end
end
# Allow local developers to override just certain files of a directory.
override_path = begin
resolve_template_path(directory + "_overrides")
rescue RuntimeError
nil
end
if override_path
Dir.foreach(override_path) do |file|
file = "#{directory}_overrides/#{file}"
next if file.match?("/_menu_item.html.erb") && !top_level_model?
unless File.directory?(resolve_template_path(file))
scaffold_file(file, overrides: true)
end
end
end
end
def add_line_to_file(file, content, hook, options = {})
increase_indent = options[:increase_indent]
add_before = options[:add_before]
add_after = options[:add_after]
transformed_file_name = file
transformed_content = content
transform_hook = hook
begin
target_file_content = File.read(transformed_file_name)
rescue Errno::ENOENT => _
puts "Couldn't find '#{transformed_file_name}'".red unless suppress_could_not_find || options[:suppress_could_not_find]
return false
end
# When Super Scaffolding strong parameters, if an attribute named :project exists for a model `Project`,
# the `account_load_and_authorize_resource :project,` code prevents the attribute from being scaffolded
# since the transformed content is `:project,`. We bypass that here with this check.
content_matches_model_name = transformed_content.gsub(/[:|,]/, "").capitalize == child
if target_file_content.include?(transformed_content) && !content_matches_model_name
puts "No need to update '#{transformed_file_name}'. It already has '#{transformed_content}'." unless silence_logs?
else
new_target_file_content = []
target_file_content.split("\n").each do |line|
if options[:exact_match] ? line == transform_hook : line.match(/#{Regexp.escape(transform_hook)}\s*$/)
if add_before
new_target_file_content << "#{line} #{add_before}"
else
unless options[:prepend]
new_target_file_content << line
end
end
line =~ /^(\s*).*#{Regexp.escape(transform_hook)}.*/
leading_whitespace = $1
incoming_leading_whitespace = nil
transformed_content.lines.each do |content_line|
content_line.rstrip
content_line =~ /^(\s*).*/
# this ignores empty lines.
# it accepts any amount of whitespace if we haven't seen any whitespace yet.
if content_line.present? && $1 && (incoming_leading_whitespace.nil? || $1.length < incoming_leading_whitespace.length)
incoming_leading_whitespace = $1
end
end
incoming_leading_whitespace ||= ""
transformed_content.lines.each do |content_line|
new_target_file_content << "#{leading_whitespace}#{" " if increase_indent}#{content_line.gsub(/^#{incoming_leading_whitespace}/, "").rstrip}".presence
end
new_target_file_content << "#{leading_whitespace}#{add_after}" if add_after
if options[:prepend]
new_target_file_content << line
end
else
new_target_file_content << line
end
end
puts "Updating '#{transformed_file_name}'." unless silence_logs?
File.write(transformed_file_name, new_target_file_content.join("\n").strip + "\n")
end
end
def scaffold_add_line_to_file(file, content, hook, options = {})
file = resolve_target_path(transform_string(file))
content = transform_string(content)
hook = transform_string(hook)
add_line_to_file(file, content, hook, options)
end
def scaffold_replace_line_in_file(file, content, content_to_replace)
file = resolve_target_path(transform_string(file))
# we specifically don't transform the content, we assume a builder function created this content.
transformed_content_to_replace = transform_string(content_to_replace)
content_replacement_transformed = content_to_replace != transformed_content_to_replace
options = {suppress_could_not_find: suppress_could_not_find, content_replacement_transformed: content_replacement_transformed}
Scaffolding::FileManipulator.replace_line_in_file(file, content, transformed_content_to_replace, options)
end
# if class_name isn't specified, we use `child`.
# if class_name is specified, then `child` is assumed to be a parent of `class_name`.
# returns an array with the ability line and a boolean indicating whether the ability line should be inserted among
# the abilities for admins only. (this happens when building an ability line for a resources that doesn't ultimately
# belong to a Team or a User.)
def build_ability_line(class_names = nil)
# e.g. ['Conversations::Message', 'Conversation']
if class_names
# e.g. 'Conversations::Message'
class_name = class_names.shift
# e.g. ['Conversation', 'Deliverable', 'Phase', 'Project', 'Team']
working_parents = class_names + [child] + parents
else
# e.g. 'Deliverable'
class_name = child
# e.g. ['Phase', 'Project', 'Team']
working_parents = parents.dup
end
case working_parents.last
when "User"
working_parents.pop
ability_line = "user_id: user.id"
when "Team"
working_parents.pop
ability_line = "team_id: user.team_ids"
else
# if a resources is specified that isn't ultimately owned by a team or a user, then only admins can manage it.
return ["can :manage, #{class_name}", true]
end
# e.g. ['Phase', 'Project']
while working_parents.any?
current_parent = working_parents.pop
current_transformer = Scaffolding::ClassNamesTransformer.new(working_parents.last || class_name, current_parent, namespace)
ability_line = "#{current_transformer.parent_variable_name_in_context}: {#{ability_line}}"
end
# e.g. "can :manage, Deliverable, phase: {project: {team_id: user.team_ids}}"
["can :manage, #{class_name}, #{ability_line}", false]
end
def build_conversation_ability_line
build_ability_line(["Conversations::Message", "Conversation"])
end
def add_scaffolding_hooks_to_model
before_scaffolding_hooks = <<~RUBY
#{CONCERNS_HOOK}
#{ATTR_ACCESSORS_HOOK}
RUBY
after_scaffolding_hooks = <<-RUBY
#{BELONGS_TO_HOOK}
#{HAS_MANY_HOOK}
#{HAS_ONE_HOOK}
#{SCOPES_HOOK}
#{VALIDATIONS_HOOK}
#{CALLBACKS_HOOK}
#{DELEGATIONS_HOOK}
#{METHODS_HOOK}
RUBY
# add scaffolding hooks to the model.
unless File.readlines(transform_string("./app/models/scaffolding/completely_concrete/tangible_thing.rb")).join.include?(CONCERNS_HOOK)
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", before_scaffolding_hooks, "ApplicationRecord", increase_indent: true)
end
unless File.readlines(transform_string("./app/models/scaffolding/completely_concrete/tangible_thing.rb")).join.include?(BELONGS_TO_HOOK)
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)
end
end
def add_ability_line_to_roles_yml(class_names = nil)
model_names = class_names || [child]
role_file = "./config/models/roles.yml"
roles_hash = YAML.load_file(role_file)
default_role_placements = [
[:default, :models],
[:admin, :models]
]
model_names.each do |model_name|
default_role_placements.each do |role_placement|
stringified_role_placement = role_placement.map { |placement| placement.to_s }
if roles_hash.dig(*stringified_role_placement)[model_name].nil?
role_type = (role_placement.first == :admin) ? "manage" : "read"
Scaffolding::FileManipulator.add_line_to_yml_file(role_file, "#{model_name}: #{role_type}", role_placement)
end
end
end
end
def build_factory_setup
class_name = child
working_parents = parents.dup
current_parent = working_parents.pop
current_transformer = Scaffolding::Transformer.new(working_parents.last || class_name, [current_parent])
setup_lines = []
unless current_parent == "Team" || current_parent == "User"
setup_lines << current_transformer.transform_string("@absolutely_abstract_creative_concept = create(:scaffolding_absolutely_abstract_creative_concept)")
end
previous_assignment = current_transformer.transform_string("absolutely_abstract_creative_concept: @absolutely_abstract_creative_concept")
current_parent = working_parents.pop
while current_parent
current_transformer = Scaffolding::Transformer.new(working_parents.last || class_name, [current_parent])
setup_lines << current_transformer.transform_string("@absolutely_abstract_creative_concept = create(:scaffolding_absolutely_abstract_creative_concept, #{previous_assignment})")
previous_assignment = current_transformer.transform_string("absolutely_abstract_creative_concept: @absolutely_abstract_creative_concept")
current_parent = working_parents.pop
end
setup_lines << current_transformer.transform_string("@tangible_thing = build(:scaffolding_completely_concrete_tangible_thing, #{previous_assignment})")
setup_lines
end
def replace_in_file(file, before, after, target_regexp = nil)
puts "Replacing in '#{file}'." unless silence_logs?
if target_regexp.present?
target_file_content = ""
File.open(file).each_line do |l|
l.gsub!(before, after) if !!l.match(target_regexp)
target_file_content += l
end
else
target_file_content = File.read(file)
target_file_content.gsub!(before, after)
end
File.write(file, target_file_content)
end
def restart_server
# restart the server.
puts "Restarting the server so it picks up the new localization .yml file."
`./bin/rails restart`
end
def add_locale_helper_export_fix
namespaced_locale_export_hook = "# 🚅 super scaffolding will insert the export for the locale view helper here."
spacer = " "
indentation = spacer * 3
namespace_elements = child.underscore.pluralize.split("/")
last_element = namespace_elements.shift
lines_to_add = [last_element + ":"]
namespace_elements.map do |namespace_element|
lines_to_add << indentation + namespace_element + ":"
last_element = namespace_element
indentation += spacer
end
lines_to_add << lines_to_add.pop + " *#{last_element}"
scaffold_replace_line_in_file("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml", lines_to_add.join("\n"), namespaced_locale_export_hook)
end
def scaffold_new_breadcrumbs(child, parents)
scaffold_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_breadcrumbs.html.erb")
puts
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
puts
puts transform_string("en:\n scaffolding/completely_concrete/tangible_things: &tangible_things\n label: &label Things\n breadcrumbs:\n label: *label").yellow
puts
end
def add_has_many_association
has_many_line = ["has_many :completely_concrete_tangible_things"]
# Specify the class name if the model is namespaced.
if child.match?("::")
has_many_line << "class_name: \"Scaffolding::CompletelyConcrete::TangibleThing\""
end
has_many_line << "dependent: :destroy"
# Specify the foreign key if the parent is namespaced.
if parent.match?("::")
has_many_line << "foreign_key: :absolutely_abstract_creative_concept_id"
# And if we need `foreign_key`, we should also specify `inverse_of`.
has_many_line << "inverse_of: :absolutely_abstract_creative_concept"
end
has_many_string = transform_string(has_many_line.join(", "))
add_line_to_file(transform_string("./app/models/scaffolding/absolutely_abstract/creative_concept.rb"), has_many_string, HAS_MANY_HOOK, prepend: true)
# Return the name of the has_many association.
has_many_string.split(",").first.split(":").last
end
def add_has_many_through_associations(has_many_through_transformer)
has_many_association = add_has_many_association
has_many_through_string = has_many_through_transformer.transform_string("has_many :completely_concrete_tangible_things, through: :$HAS_MANY_ASSOCIATION")
has_many_through_string.gsub!("$HAS_MANY_ASSOCIATION", has_many_association)
add_line_to_file(transform_string("./app/models/scaffolding/absolutely_abstract/creative_concept.rb"), has_many_through_string, HAS_MANY_HOOK, prepend: true)
end
def add_attributes_to_various_views(attributes, scaffolding_options = {})
sql_type_to_field_type_mapping = {
# 'binary' => '',
"boolean" => "options",
"date" => "date_field",
"datetime" => "date_and_time_field",
"decimal" => "text_field",
"float" => "text_field",
"integer" => "text_field",
"bigint" => "text_field",
# 'primary_key' => '',
# 'references' => '',
"string" => "text_field",
"text" => "text_area"
# 'time' => '',
# 'timestamp' => '',
}
# add attributes to various views.
attributes.each_with_index do |attribute_definition, index|
attribute = Scaffolding::Attribute.new(attribute_definition, scaffolding_options[:type], index)
if attribute.is_first_attribute? && ["trix_editor", "ckeditor", "text_area"].include?(attribute.type)
puts ""
puts "The first attribute of your model cannot be any of the following types:".red
puts "1. trix_editor"
puts "2. ckeditor"
puts "3. text_area"
puts ""
puts "Please ensure you have another attribute type as the first attribute for your model and try again."
exit
end
if sql_type_to_field_type_mapping[attribute.type]
attribute.type = sql_type_to_field_type_mapping[attribute.type]
end
cell_attributes = if attribute.is_boolean?
' class="text-center"'
end
# don't do table columns for certain types of fields and attribute partials
if ["trix_editor", "ckeditor", "text_area"].include?(attribute.type) || ["html", "has_many"].include?(attribute.partial_name)
cli_options["skip-table"] = true
end
if attribute.type == "none"
cli_options["skip-form"] = true
end
if attribute.partial_name == "none"
cli_options["skip-show"] = true
cli_options["skip-table"] = true
end
#
# MODEL VALIDATIONS
#
unless cli_options["skip-form"] || attribute.is_unscoped?
file_name = "./app/models/scaffolding/completely_concrete/tangible_thing.rb"
if attribute.is_association?
field_content = if attribute.options[:source]
<<~RUBY
def valid_#{attribute.collection_name}
#{attribute.options[:source]}
end
RUBY
else
add_additional_step :yellow, transform_string("You'll need to implement the `valid_#{attribute.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 `#{attribute.type}` field and also validate that users aren't trying to exploit multitenancy.")
<<~RUBY
def valid_#{attribute.collection_name}
raise "please review and implement `valid_#{attribute.collection_name}` in `app/models/scaffolding/completely_concrete/tangible_thing.rb`."
# please specify what objects should be considered valid for assigning to `#{attribute.name_without_id}`.
# the resulting code should probably look something like `team.#{attribute.collection_name}`.
end
RUBY
end
scaffold_add_line_to_file(file_name, field_content, METHODS_HOOK, prepend: true)
if attribute.is_belongs_to?
scaffold_add_line_to_file(file_name, "validates :#{attribute.name_without_id}, scope: true", VALIDATIONS_HOOK, prepend: true)
end
# TODO we need to add a multitenancy check for has many associations.
end
end
#
# FORM FIELD
#
unless cli_options["skip-form"] || attribute.options[:readonly]
# add `has_rich_text` for trix editor fields.
if attribute.type == "trix_editor"
file_name = "./app/models/scaffolding/completely_concrete/tangible_thing.rb"
scaffold_add_line_to_file(file_name, "has_rich_text :#{attribute.name}", HAS_ONE_HOOK, prepend: true)
end
# field on the form.
field_attributes = {method: ":#{attribute.name}"}
field_options = {}
options = {}
if attribute.is_first_attribute?
field_options[:autofocus] = "true"
end
if attribute.is_id? && attribute.type == "super_select"
options[:include_blank] = "t('.fields.#{attribute.name}.placeholder')"
# 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.")
end
field_options[:multiple] = "true" if attribute.is_multiple?
valid_values = if attribute.is_id?
"valid_#{attribute.name_without_id.pluralize}"
elsif attribute.is_ids?
"valid_#{attribute.collection_name}"
end
# https://stackoverflow.com/questions/21582464/is-there-a-ruby-hashto-s-equivalent-for-the-new-hash-syntax
if field_options.any? || options.any?
field_options_key = if attribute.type == "super_select"
if options.any?
field_attributes[:options] = "{" + field_options.map { |key, value| "#{key}: #{value}" }.join(", ") + "}"
end
:html_options
else
field_options.merge!(options)
:options
end
field_attributes[field_options_key] = "{" + field_options.map { |key, value| "#{key}: #{value}" }.join(", ") + "}"
end
if attribute.is_association?
short = attribute.association_class_name
case attribute.type
when "buttons", "options"
field_attributes["\n options"] = "@tangible_thing.#{valid_values}.map { |#{short}| [#{short}.id, #{short}.#{attribute.options[:label]}] }"
when "super_select"
field_attributes["\n choices"] = "@tangible_thing.#{valid_values}.map { |#{short}| [#{short}.#{attribute.options[:label]}, #{short}.id] }"
end
end
if attribute.type == "color_picker"
field_attributes[:color_picker_field_options] = "t('#{child.pluralize.underscore}.fields.#{attribute.name}.options')"
end
field_content = "<%= render 'shared/fields/#{attribute.type}'#{", " if field_attributes.any?}#{field_attributes.map { |key, value| "#{key}: #{value}" }.join(", ")} %>"
# TODO Add more of these from other packages?
is_core_model = ["Team", "User", "Membership"].include?(child)
scaffold_add_line_to_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_form.html.erb", field_content, ERB_NEW_FIELDS_HOOK, prepend: true, suppress_could_not_find: is_core_model)
scaffold_add_line_to_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_fields.html.erb", field_content, ERB_NEW_FIELDS_HOOK, prepend: true, suppress_could_not_find: !is_core_model)
end
#
# SHOW VIEW
#
unless cli_options["skip-show"]
if attribute.is_id?
<<~ERB
<% if @tangible_thing.#{attribute.name_without_id} %>
<% end %>
ERB
elsif attribute.is_ids?
<<~ERB
<% if @tangible_thing.#{attribute.collection_name}.any? %>
<% end %>
ERB
end
# this gets stripped and is one line, so indentation isn't a problem.
field_content = <<-ERB
<%= render 'shared/attributes/#{attribute.partial_name}', attribute: :#{attribute.is_vanilla? ? attribute.name : attribute.name_without_id_suffix} %>
ERB
if attribute.type == "password_field"
field_content.gsub!(/\s%>/, ", options: { password: true } %>")
end
show_page_doesnt_exist = child == "User"
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, suppress_could_not_find: show_page_doesnt_exist)
end
#
# INDEX TABLE
#
unless cli_options["skip-table"]
# table header.
field_content = "<%= t('.fields.#{attribute.is_vanilla? ? attribute.name : attribute.name_without_id_suffix}.heading') %> | "
unless ["Team", "User"].include?(child)
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)
end
# If these strings are the same, we get duplicate variable names in the _index.html.erb partial,
# so we account for that here. Run the Super Scaffolding test setup script and check the index partial
# of models with namespaced parents for reference (i.e. - Objective, Projects::Step).
transformed_abstract_str = transform_string("absolutely_abstract_creative_concept")
transformed_concept_str = transform_string("creative_concept")
transformed_file_name = transform_string("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb")
if (transformed_abstract_str == transformed_concept_str) && File.exist?(transformed_file_name)
replace_in_file(
transformed_file_name,
"#{transformed_abstract_str} = @#{transformed_abstract_str} || @#{transformed_concept_str}",
"#{transformed_abstract_str} = @#{transformed_concept_str}"
)
end
table_cell_options = []
if attribute.is_first_attribute?
table_cell_options << "url: [:account, tangible_thing]"
end
# this gets stripped and is one line, so indentation isn't a problem.
field_content = <<-ERB
<%= render 'shared/attributes/#{attribute.partial_name}', attribute: :#{attribute.is_vanilla? ? attribute.name : attribute.name_without_id_suffix}#{", #{table_cell_options.join(", ")}" if table_cell_options.any?} %> |
ERB
case attribute.type
when "password_field"
field_content.gsub!(/\s%>/, ", options: { password: true } %>")
when "address_field"
field_content.gsub!(/\s%>/, ", one_line: true %>")
end
unless ["Team", "User"].include?(child)
scaffold_add_line_to_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_tangible_thing.html.erb", field_content.strip, ERB_NEW_FIELDS_HOOK, prepend: true)
end
end
#
# LOCALIZATIONS
#
unless cli_options["skip-locales"]
yaml_template = <<~YAML
<%= attribute.name %>: <% if attribute.is_association? %>&<%= attribute.name_without_id_suffix %><% end %>
_: {attribute.name} #{attribute.title_case}
label: *#{attribute.name}
heading: *#{attribute.name}
api_title: *#{attribute.name}
api_description: *#{attribute.name}
<% if attribute.type == "super_select" %>
<% if attribute.is_required? %>
placeholder: Select <% attribute.title_case.with_indefinite_article %>
<% else %>
placeholder: None
<% end %>
<% end %>
<% if attribute.is_boolean? %>
options:
yes: "Yes"
no: "No"
<% elsif ["buttons", "super_select", "options"].include?(attribute.type) && !attribute.is_association? %>
options:
one: One
two: Two
three: Three
<% end %>
<% if attribute.type == "color_picker" %>
options:
- '#9C73D2'
- '#48CDFE'
- '#53F3ED'
- '#47E37F'
- '#F2593D'
- '#F68421'
- '#F9DE00'
- '#929292'
<% end %>
<% if attribute.is_association? %>
<%= attribute.name_without_id_suffix %>: *<%= attribute.name_without_id_suffix %>
<% end %>
YAML
field_content = ERB.new(yaml_template).result(binding).lines.select(&:present?).join
scaffold_add_line_to_file("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml", field_content, RUBY_NEW_FIELDS_HOOK, prepend: true)
# active record's field label.
scaffold_add_line_to_file("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml", "#{attribute.name}: *#{attribute.name}", "# 🚅 super scaffolding will insert new activerecord attributes above this line.", prepend: true)
end
#
# STRONG PARAMETERS
#
unless cli_options["skip-form"] || attribute.options[:readonly]
# add attributes to strong params.
[
"./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb",
"./app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb"
].each do |file|
if attribute.is_ids? || attribute.is_multiple?
scaffold_add_line_to_file(file, "#{attribute.name}: [],", RUBY_NEW_ARRAYS_HOOK, prepend: true)
if attribute.type == "file_field"
scaffold_add_line_to_file(file, "#{attribute.name}_removal: [],", RUBY_NEW_ARRAYS_HOOK, prepend: true)
end
elsif attribute.type == "address_field"
address_strong_params = <<~RUBY
#{attribute.name}_attributes: [
:id,
:_destroy,
:address_one,
:address_two,
:city,
:country_id,
:region_id,
:postal_code
],
RUBY
scaffold_add_line_to_file(file, address_strong_params, RUBY_NEW_ARRAYS_HOOK, prepend: true)
else
scaffold_add_line_to_file(file, ":#{attribute.name},", RUBY_NEW_FIELDS_HOOK, prepend: true)
if attribute.type == "file_field"
scaffold_add_line_to_file(file, ":#{attribute.name}_removal,", RUBY_NEW_FIELDS_HOOK, prepend: true)
end
end
end
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", attribute.special_processing, RUBY_NEW_FIELDS_PROCESSING_HOOK, prepend: true) if attribute.special_processing
end
#
# ASSOCIATED MODELS
#
unless cli_options["skip-form"] || attribute.options[:readonly]
# set default values for associated models.
case attribute.type
when "address_field"
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", "before_action :set_default_#{attribute.name}, except: :index", "ApplicationController", increase_indent: true)
method_content = <<~RUBY
def set_default_#{attribute.name}
@tangible_thing.#{attribute.name} ||= Address.new
end
RUBY
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", method_content, "end", prepend: true, increase_indent: true, exact_match: true)
end
end
#
# API SERIALIZER
#
unless cli_options["skip-api"]
# TODO The serializers can't handle these `has_rich_text` attributes.
unless attribute.type == "trix_editor"
unless attribute.type == "file_field"
scaffold_add_line_to_file("./app/views/api/v1/scaffolding/completely_concrete/tangible_things/_tangible_thing.json.jbuilder", ":#{attribute.name},", RUBY_NEW_FIELDS_HOOK, prepend: true, suppress_could_not_find: true)
end
assertion = case attribute.type
when "date_field"
"assert_equal_or_nil Date.parse(tangible_thing_data['#{attribute.name}']), tangible_thing.#{attribute.name}"
when "date_and_time_field"
"assert_equal_or_nil DateTime.parse(tangible_thing_data['#{attribute.name}']), tangible_thing.#{attribute.name}"
when "file_field"
if attribute.is_multiple?
"assert_equal tangible_thing_data['#{attribute.name}'], @tangible_thing.#{attribute.name}.map{|file| rails_blob_path(file)} unless controller.action_name == 'create'"
else
"assert_equal tangible_thing_data['#{attribute.name}'], rails_blob_path(@tangible_thing.#{attribute.name}) unless controller.action_name == 'create'"
end
else
"assert_equal_or_nil tangible_thing_data['#{attribute.name}'], tangible_thing.#{attribute.name}"
end
scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb", assertion, RUBY_NEW_FIELDS_HOOK, prepend: true)
end
# File fields are handled in a specific way when using the jsonapi-serializer.
if attribute.type == "file_field"
jbuilder_content = if attribute.is_multiple?
<<~RUBY
json.#{attribute.name} do
json.array! tangible_thing.#{attribute.name}.map { |file| url_for(file) }
end if tangible_thing.#{attribute.name}.attached?
RUBY
else
"json.#{attribute.name} url_for(tangible_thing.#{attribute.name}) if tangible_thing.#{attribute.name}.attached?"
end
scaffold_add_line_to_file("./app/views/api/v1/scaffolding/completely_concrete/tangible_things/_tangible_thing.json.jbuilder", jbuilder_content, RUBY_FILES_HOOK, prepend: true, suppress_could_not_find: true)
# We also want to make sure we attach the dummy file in the API test on setup
file_name = "./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb"
content = if attribute.is_multiple?
<<~RUBY
@#{child.underscore}.#{attribute.name} = [Rack::Test::UploadedFile.new("test/support/foo.txt")]
@another_#{child.underscore}.#{attribute.name} = [Rack::Test::UploadedFile.new("test/support/foo.txt")]
RUBY
else
<<~RUBY
@#{child.underscore}.#{attribute.name} = Rack::Test::UploadedFile.new("test/support/foo.txt")
@another_#{child.underscore}.#{attribute.name} = Rack::Test::UploadedFile.new("test/support/foo.txt")
RUBY
end
scaffold_add_line_to_file(file_name, content, RUBY_FILES_HOOK, prepend: true)
end
if attribute.default_value
unless attribute.options[:readonly]
scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb", "#{attribute.name}: #{attribute.default_value},", RUBY_ADDITIONAL_NEW_FIELDS_HOOK, prepend: true)
scaffold_add_line_to_file("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb", "assert_equal @tangible_thing.#{attribute.name}, #{attribute.default_value}", RUBY_EVEN_MORE_NEW_FIELDS_HOOK, prepend: true)
end
end
end
#
# OPENAPI DOCUMENTS
#
unless cli_options["skip-api"]
# We always want to suppress this error for this file, since it doesn't exist by default. We reset this below.
suppress_could_not_find_state = suppress_could_not_find
self.suppress_could_not_find = true
# It's OK that this won't be found most of the time.
scaffold_add_line_to_file(
"./app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb",
"<%= attribute :#{attribute.name} %>",
"<%# 🚅 super scaffolding will insert new attributes above this line. %>",
prepend: true
)
# It's OK that this won't be found most of the time.
scaffold_add_line_to_file(
"./app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb",
"<%= parameter :#{attribute.name} %>",
"<%# 🚅 super scaffolding will insert new parameter above this line. %>",
prepend: true
)
self.suppress_could_not_find = suppress_could_not_find_state
end
#
# MODEL ASSOCATIONS
#
unless cli_options["skip-model"]
if attribute.is_belongs_to?
unless attribute.options[:class_name]
attribute.options[:class_name] = attribute.name_without_id.classify
end
file_name = "app/models/#{attribute.options[:class_name].underscore}.rb"
unless File.exist?(file_name)
raise "You'll need to specify a `class_name` option for `#{attribute.name}` because there is no `#{attribute.options[:class_name].classify}` model defined in `#{file_name}`. Try again with `#{attribute.name}:#{attribute.type}[class_name=SomeClassName]`."
end
modified_migration = false
# find the database migration that defines this relationship.
expected_reference = "add_reference :#{class_names_transformer.table_name}, :#{attribute.name_without_id}"
migration_file_name = `grep "#{expected_reference}" db/migrate/*`.split(":").first
# if that didn't work, see if we can find a creation of the reference when the table was created.
unless migration_file_name
confirmation_reference = "create_table :#{class_names_transformer.table_name}"
confirmation_migration_file_name = `grep "#{confirmation_reference}" db/migrate/*`.split(":").first
fallback_reference = "t.references :#{attribute.name_without_id}"
fallback_migration_file_name = `grep "#{fallback_reference}" db/migrate/* | grep #{confirmation_migration_file_name}`.split(":").first
if fallback_migration_file_name == confirmation_migration_file_name
migration_file_name = fallback_migration_file_name
end
end
unless attribute.is_required?
if migration_file_name
replace_in_file(migration_file_name, ":#{attribute.name_without_id}, null: false", ":#{attribute.name_without_id}, null: true")
modified_migration = true
else
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)."
end
end
class_name_matches = attribute.name_without_id.tableize == attribute.options[:class_name].tableize.tr("/", "_")
# but also, if namespaces are involved, just don't...
if attribute.options[:class_name].include?("::")
class_name_matches = false
end
# unless the table name matches the association name.
unless class_name_matches
if migration_file_name
# There are two forms this association creation can take.
replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: \"#{attribute.options[:class_name].tableize.tr("/", "_")}\"}", /t\.references :#{attribute.name_without_id}/)
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("/", "_")}, :#{attribute.name_without_id}/)
modified_migration = true
else
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."
end
end
optional_line = ", optional: true" unless attribute.is_required?
# if the `belongs_to` is already there from `rails g model`..
scaffold_replace_line_in_file(
"./app/models/scaffolding/completely_concrete/tangible_thing.rb",
class_name_matches ?
"belongs_to :#{attribute.name_without_id}#{optional_line}" :
"belongs_to :#{attribute.name_without_id}, class_name: \"#{attribute.options[:class_name]}\"#{optional_line}",
"belongs_to :#{attribute.name_without_id}"
)
# if it wasn't there, the replace will not have done anything, so we insert it entirely.
# however, this won't do anything if the association is already there.
scaffold_add_line_to_file(
"./app/models/scaffolding/completely_concrete/tangible_thing.rb",
class_name_matches ?
"belongs_to :#{attribute.name_without_id}#{optional_line}" :
"belongs_to :#{attribute.name_without_id}, class_name: \"#{attribute.options[:class_name]}\"#{optional_line}",
BELONGS_TO_HOOK,
prepend: true
)
if modified_migration
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."
end
end
# Add `default: false` to boolean migrations.
if attribute.is_boolean?
# Give priority to crud-field migrations if they exist.
add_column_reference = "add_column :#{class_names_transformer.table_name}, :#{attribute.name}"
create_table_reference = "create_table :#{class_names_transformer.table_name}"
confirmation_migration_file_name = `grep "#{add_column_reference}" db/migrate/*`.split(":").first
confirmation_migration_file_name ||= `grep "#{create_table_reference}" db/migrate/*`.split(":").first
old_line, new_line = nil
File.open(confirmation_migration_file_name) do |migration_file|
old_lines = migration_file.readlines
old_lines.each do |line|
target_attribute = line.match?(/:#{class_names_transformer.table_name}, :#{attribute.name}, :boolean/) || line.match?(/\s*t\.boolean :#{attribute.name}/)
if target_attribute
old_line = line
new_line = "#{old_line.chomp}, default: false\n"
end
end
end
replace_in_file(confirmation_migration_file_name, old_line, new_line)
end
end
#
# MODEL HOOKS
#
unless cli_options["skip-model"]
if attribute.is_required? && !attribute.is_belongs_to?
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "validates :#{attribute.name}, presence: true", VALIDATIONS_HOOK, prepend: true)
end
case attribute.type
when "file_field"
remove_file_methods = if attribute.is_multiple?
<<~RUBY
def #{attribute.name}_removal?
#{attribute.name}_removal&.any?
end
def remove_#{attribute.name}
#{attribute.name}_attachments.where(id: #{attribute.name}_removal).map(&:purge)
end
def #{attribute.name}=(attachables)
attachables = Array(attachables).compact_blank
if attachables.any?
attachment_changes["#{attribute.name}"] =
ActiveStorage::Attached::Changes::CreateMany.new("#{attribute.name}", self, #{attribute.name}.blobs + attachables)
end
end
RUBY
else
<<~RUBY
def #{attribute.name}_removal?
#{attribute.name}_removal.present?
end
def remove_#{attribute.name}
#{attribute.name}.purge
end
RUBY
end
# Generating a model with an `attachment(s)` data type (i.e. - `rails g ModelName file:attachment`)
# adds `has_one_attached` or `has_many_attached` to our model, just not directly above the
# HAS_ONE_HOOK or the HAS_MANY_HOOK. We move the string here so it's scaffolded above the proper hook.
model_file_path = transform_string("./app/models/scaffolding/completely_concrete/tangible_thing.rb")
model_contents = File.readlines(model_file_path)
reflection_declaration = attribute.is_multiple? ? "has_many_attached :#{attribute.name}" : "has_one_attached :#{attribute.name}"
# Save the file without the hook so we can write it via the `scaffold_add_line_to_file` method below.
model_without_attached_hook = model_contents.reject.each { |line| line.include?(reflection_declaration) }
File.open(model_file_path, "w") do |f|
model_without_attached_hook.each { |line| f.write(line) }
end
hook_type = attribute.is_multiple? ? HAS_MANY_HOOK : HAS_ONE_HOOK
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", reflection_declaration, hook_type, prepend: true)
# TODO: We may need to edit these depending on how we save multiple files.
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "attr_accessor :#{attribute.name}_removal", ATTR_ACCESSORS_HOOK, prepend: true)
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", remove_file_methods, METHODS_HOOK, prepend: true)
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "after_validation :remove_#{attribute.name}, if: :#{attribute.name}_removal?", CALLBACKS_HOOK, prepend: true)
when "trix_editor"
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_rich_text :#{attribute.name}", HAS_ONE_HOOK, prepend: true)
when "address_field"
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_one :#{attribute.name}, class_name: \"Address\", as: :addressable", HAS_ONE_HOOK, prepend: true)
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "accepts_nested_attributes_for :#{attribute.name}", HAS_ONE_HOOK, prepend: true)
when "buttons"
if attribute.is_boolean?
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "validates :#{attribute.name}, inclusion: [true, false]", VALIDATIONS_HOOK, prepend: true)
end
end
end
end
end
def add_additional_step(color, message)
additional_steps.push [color, message]
end
def scaffold_crud(attributes)
if cli_options["only-index"]
cli_options["skip-table"] = false
cli_options["skip-views"] = true
cli_options["skip-controller"] = true
cli_options["skip-form"] = true
cli_options["skip-show"] = true
cli_options["skip-form"] = true
cli_options["skip-api"] = true
cli_options["skip-model"] = true
cli_options["skip-parent"] = true
cli_options["skip-locales"] = true
cli_options["skip-routes"] = true
end
if cli_options["namespace"]
cli_options["skip-api"] = true
cli_options["skip-model"] = true
cli_options["skip-locales"] = true
end
# TODO fix this. we can do this better.
files = if cli_options["only-index"]
[
"./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb",
"./app/views/account/scaffolding/completely_concrete/tangible_things/index.html.erb",
"./app/views/account/scaffolding/completely_concrete/tangible_things/_tangible_thing.html.erb"
]
else
# copy a ton of files over and do the appropriate string replace.
[
"./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb",
"./app/views/account/scaffolding/completely_concrete/tangible_things",
"./app/views/api/v1/scaffolding/completely_concrete/tangible_things",
("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml" unless cli_options["skip-locales"]),
("./app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb" unless cli_options["skip-api"]),
("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb" unless cli_options["skip-api"])
# "./app/filters/scaffolding/completely_concrete/tangible_things_filter.rb"
].compact
end
files.each do |name|
if File.directory?(resolve_template_path(name))
scaffold_directory(name)
else
scaffold_file(name)
end
end
unless cli_options["skip-model"]
# find the database migration that defines this relationship.
migration_file_name = `grep "create_table :#{class_names_transformer.table_name}.*do |t|$" db/migrate/*`.split(":").first
unless migration_file_name.present?
raise "No migration file seems to exist for creating the table `#{class_names_transformer.table_name}`.\n" \
"Please run the following command first and try Super Scaffolding again:\n" \
"rails generate model #{child} #{parent.underscore}:references #{attributes.join(" ")}"
end
# if needed, update the reference to the parent class name in the create_table migration
current_transformer = Scaffolding::ClassNamesTransformer.new(child, parent, namespace)
unless current_transformer.parent_variable_name_in_context.pluralize == current_transformer.parent_table_name
replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: '#{current_transformer.parent_table_name}'}")
end
# update the factory generated by `rails g`.
content = if transform_string(":absolutely_abstract_creative_concept") == transform_string(":scaffolding_absolutely_abstract_creative_concept")
transform_string(" association :absolutely_abstract_creative_concept")
else
transform_string(" association :absolutely_abstract_creative_concept, factory: :scaffolding_absolutely_abstract_creative_concept")
end
scaffold_replace_line_in_file("./test/factories/scaffolding/completely_concrete/tangible_things.rb", content, " absolutely_abstract_creative_concept { nil }")
add_has_many_association
if class_names_transformer.belongs_to_needs_class_definition?
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"))
end
update_models_abstract_class
# add user permissions.
add_ability_line_to_roles_yml
end
# Add factory setup in API controller test.
unless cli_options["skip-api"]
test_name = transform_string("./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb")
test_lines = File.open(test_name).readlines
# Shift contents of controller test after skipping `unless scaffolding_things_disabled?` block.
class_block_index = Scaffolding::FileManipulator.find(test_lines, "class #{transform_string("Api::V1::Scaffolding::CompletelyConcrete::TangibleThingsControllerTest")}")
new_lines = Scaffolding::BlockManipulator.shift_block(lines: test_lines, block_start: test_lines[class_block_index], shift_contents_only: true)
Scaffolding::FileManipulator.write(test_name, new_lines)
# Ensure variables built with factories are indented properly.
factory_hook_index = Scaffolding::FileManipulator.find(new_lines, RUBY_FACTORY_SETUP_HOOK)
factory_hook_indentation = Scaffolding::BlockManipulator.indentation_of(factory_hook_index, new_lines)
indented_factory_lines = build_factory_setup.map { |line| "#{factory_hook_indentation}#{line}\n" }
scaffold_replace_line_in_file(test_name, indented_factory_lines.join, new_lines[factory_hook_index])
end
# add children to the show page of their parent.
unless cli_options["skip-parent"] || parent == "None"
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
)
end
unless cli_options["skip-api"]
# add children to the show page of their parent.
scaffold_add_line_to_file(
"./app/views/api/#{BulletTrain::Api.current_version}/open_api/index.yaml.erb",
"<%= automatic_components_for Scaffolding::CompletelyConcrete::TangibleThing %>",
"<%# 🚅 super scaffolding will insert new components above this line. %>",
prepend: true
)
# add children to the show page of their parent.
scaffold_add_line_to_file(
"./app/views/api/#{BulletTrain::Api.current_version}/open_api/index.yaml.erb",
"<%= automatic_paths_for Scaffolding::CompletelyConcrete::TangibleThing, Scaffolding::AbsolutelyAbstract::CreativeConcept %>",
"<%# 🚅 super scaffolding will insert new paths above this line. %>",
prepend: true
)
end
unless cli_options["skip-model"]
add_scaffolding_hooks_to_model
end
#
# DELEGATIONS
#
unless cli_options["skip-model"]
if ["Team", "User"].include?(parents.last) && parent != parents.last
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)
end
end
add_attributes_to_various_views(attributes, type: :crud)
unless cli_options["skip-locales"]
add_locale_helper_export_fix
end
# add sortability.
if cli_options["sortable"]
scaffold_replace_line_in_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb", transform_string("\">"), "")
unless cli_options["skip-model"]
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)
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "include Sortable\n", CONCERNS_HOOK, prepend: true)
migration = Dir.glob("db/migrate/*").last
migration_lines = File.open(migration).readlines
parent_line_idx = Scaffolding::FileManipulator.find(migration_lines, "t.references :#{parent.underscore}")
new_lines = Scaffolding::BlockManipulator.insert_line("t.integer :sort_order", parent_line_idx, migration_lines, false)
Scaffolding::FileManipulator.write(migration, new_lines)
end
unless cli_options["skip-controller"]
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", "include SortableActions\n", "Account::ApplicationController", increase_indent: true)
end
end
# titleize the localization file.
unless cli_options["skip-locales"]
replace_in_file(transform_string("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml"), child, child.underscore.humanize.titleize)
end
# apply routes.
unless cli_options["skip-routes"]
routes_namespace = cli_options["namespace"] || "account"
begin
routes_path = if routes_namespace == "account"
"config/routes.rb"
else
"config/routes/#{routes_namespace}.rb"
end
routes_manipulator = Scaffolding::RoutesFileManipulator.new(routes_path, child, parent, cli_options)
rescue Errno::ENOENT => _
puts "Creating '#{routes_path}'.".green
unless File.directory?("config/routes")
FileUtils.mkdir_p("config/routes")
end
File.write(routes_path, <<~RUBY)
collection_actions = [:index, :new, :create]
# 🚅 Don't remove this block, it will break Super Scaffolding.
begin
namespace :#{routes_namespace} do
shallow do
resources :teams do
end
end
end
end
RUBY
retry
end
begin
routes_manipulator.apply([routes_namespace])
Scaffolding::FileManipulator.write(routes_path, routes_manipulator.lines)
rescue => _
add_additional_step :red, "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/ ."
end
# If we're using a custom namespace, we have to make sure the newly
# scaffolded routes are drawn in the `config/routes.rb` and API routes files.
if cli_options["namespace"]
draw_line = "draw \"#{routes_namespace}\""
[
"config/routes.rb",
"config/routes/api/#{BulletTrain::Api.current_version}.rb"
].each do |routes_file|
original_lines = File.readlines(routes_file)
# Define which line we want to place the draw line under in the original routes files.
insert_line = if routes_file.match?("api")
draw_line = " #{draw_line}" # Add necessary indentation.
"namespace :v1 do"
else
"draw \"sidekiq\""
end
new_lines = Scaffolding::BlockManipulator.insert(draw_line, lines: original_lines, within: insert_line)
Scaffolding::FileManipulator.write(routes_file, new_lines)
end
end
unless cli_options["skip-api"]
begin
api_routes_manipulator = Scaffolding::RoutesFileManipulator.new("config/routes/api/#{BulletTrain::Api.current_version}.rb", child, parent, cli_options)
api_routes_manipulator.apply([BulletTrain::Api.current_version.to_sym])
Scaffolding::FileManipulator.write("config/routes/api/#{BulletTrain::Api.current_version}.rb", api_routes_manipulator.lines)
rescue => _
add_additional_step :red, "We weren't able to automatically add your `api/#{BulletTrain::Api.current_version}` 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/ ."
end
end
end
unless cli_options["skip-parent"]
if top_level_model?
icon_name = nil
if cli_options["navbar"].present?
icon_name = if cli_options["navbar"].match?(/^ti/)
"ti #{cli_options["navbar"]}"
elsif cli_options["navbar"].match?(/^fa/)
"fal #{cli_options["navbar"]}"
else
puts ""
puts "'#{cli_options["navbar"]}' is not a valid icon.".red
puts "Please refer to the Themify or Font Awesome documentation and pass the value like so:"
puts "--navbar=\"ti-world\""
exit
end
else
puts ""
# TODO: Update this help text letting developers know they can Super Scaffold
# models without a parent after the `--skip-parent` logic is implemented.
puts "Hey, models that are scoped directly off of a Team are eligible to be added to the navbar."
puts "Do you want to add this resource to the navbar menu? (y/N)"
response = $stdin.gets.chomp
if response.downcase[0] == "y"
puts ""
puts "OK, great! Let's do this! By default these menu items appear as a #{font_awesome? ? "puzzle piece" : "gift icon"},"
puts "but after you hit enter I'll open #{font_awesome? ? "two different pages" : "a page"} where you can view other icon options."
puts "When you find one you like, hover your mouse over it and then come back here and"
puts "enter the name of the icon you want to use."
puts "(Or hit enter when choosing to skip this step.)"
$stdin.gets.chomp
if TerminalCommands.can_open?
TerminalCommands.open_file_or_link("https://themify.me/themify-icons")
if font_awesome?
TerminalCommands.open_file_or_link("https://fontawesome.com/icons?d=gallery&s=light")
end
else
puts "Sorry! We can't open these URLs automatically on your platform, but you can visit them manually:"
puts ""
puts " https://themify.me/themify-icons"
if font_awesome?
puts " https://fontawesome.com/icons?d=gallery&s=light"
end
puts ""
end
puts ""
loop do
puts "Did you find an icon you wanted to use?"
puts "Enter the full CSS class here (e.g. 'ti ti-world'#{" or 'fal fa-puzzle-piece'" if font_awesome?}) or hit enter to just use the #{font_awesome? ? "puzzle piece" : "gift icon"}:"
icon_name = $stdin.gets.chomp
unless icon_name.match?(/ti\s.*/) || icon_name.match?(/fal\s.*/) || icon_name.strip.empty?
puts ""
puts "Please enter the full CSS class or hit enter."
next
end
break
end
puts ""
unless icon_name.length > 0 || icon_name.downcase == "y"
icon_name = "fal fa-puzzle-piece ti ti-gift"
end
end
end
if icon_name.present?
replace_in_file(transform_string("./config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml"), "fal fa-puzzle-piece", icon_name)
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. %>")
end
end
end
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_cable_ready_updates: true` to it.")
restart_server unless ENV["CI"].present?
end
end