command_proposal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +77 -0
- data/Rakefile +18 -0
- data/app/assets/config/command_proposal_manifest.js +1 -0
- data/app/assets/javascripts/command_proposal/_codemirror.js +9814 -0
- data/app/assets/javascripts/command_proposal/_helpers.js +9 -0
- data/app/assets/javascripts/command_proposal/codemirror-addon-searchcursor.js +296 -0
- data/app/assets/javascripts/command_proposal/codemirror-keymap-sublime.js +720 -0
- data/app/assets/javascripts/command_proposal/codemirror-mode-ruby.js +303 -0
- data/app/assets/javascripts/command_proposal/console.js +195 -0
- data/app/assets/javascripts/command_proposal/feed.js +51 -0
- data/app/assets/javascripts/command_proposal/terminal.js +40 -0
- data/app/assets/javascripts/command_proposal.js +1 -0
- data/app/assets/stylesheets/command_proposal/_variables.scss +0 -0
- data/app/assets/stylesheets/command_proposal/codemirror-rubyblue.scss +27 -0
- data/app/assets/stylesheets/command_proposal/codemirror.scss +367 -0
- data/app/assets/stylesheets/command_proposal/command_proposal.scss +1 -0
- data/app/assets/stylesheets/command_proposal/components.scss +31 -0
- data/app/assets/stylesheets/command_proposal/containers.scss +4 -0
- data/app/assets/stylesheets/command_proposal/icons.scss +12 -0
- data/app/assets/stylesheets/command_proposal/tables.scss +76 -0
- data/app/assets/stylesheets/command_proposal/terminal.scss +72 -0
- data/app/assets/stylesheets/command_proposal.scss +5 -0
- data/app/controllers/command_proposal/engine_controller.rb +6 -0
- data/app/controllers/command_proposal/iterations_controller.rb +83 -0
- data/app/controllers/command_proposal/runner_controller.rb +86 -0
- data/app/controllers/command_proposal/tasks_controller.rb +97 -0
- data/app/helpers/command_proposal/application_helper.rb +58 -0
- data/app/helpers/command_proposal/icons_helper.rb +15 -0
- data/app/helpers/command_proposal/params_helper.rb +63 -0
- data/app/helpers/command_proposal/permissions_helper.rb +42 -0
- data/app/jobs/command_proposal/application_job.rb +4 -0
- data/app/jobs/command_proposal/command_runner_job.rb +11 -0
- data/app/models/command_proposal/comment.rb +14 -0
- data/app/models/command_proposal/iteration.rb +78 -0
- data/app/models/command_proposal/service/external_belong.rb +48 -0
- data/app/models/command_proposal/service/json_wrapper.rb +18 -0
- data/app/models/command_proposal/service/proposal_presenter.rb +39 -0
- data/app/models/command_proposal/task.rb +106 -0
- data/app/views/command_proposal/tasks/_console_show.html.erb +44 -0
- data/app/views/command_proposal/tasks/_function_show.html.erb +54 -0
- data/app/views/command_proposal/tasks/_lines.html.erb +8 -0
- data/app/views/command_proposal/tasks/_module_show.html.erb +33 -0
- data/app/views/command_proposal/tasks/_past_iterations_list.html.erb +20 -0
- data/app/views/command_proposal/tasks/_task_detail_table.html.erb +31 -0
- data/app/views/command_proposal/tasks/_task_show.html.erb +55 -0
- data/app/views/command_proposal/tasks/error.html.erb +4 -0
- data/app/views/command_proposal/tasks/form.html.erb +64 -0
- data/app/views/command_proposal/tasks/index.html.erb +44 -0
- data/app/views/command_proposal/tasks/show.html.erb +10 -0
- data/config/routes.rb +11 -0
- data/lib/command_proposal/configuration.rb +41 -0
- data/lib/command_proposal/engine.rb +6 -0
- data/lib/command_proposal/services/command_interpreter.rb +108 -0
- data/lib/command_proposal/services/runner.rb +157 -0
- data/lib/command_proposal/version.rb +3 -0
- data/lib/command_proposal.rb +27 -0
- data/lib/generators/command_proposal/install/install_generator.rb +28 -0
- data/lib/generators/command_proposal/install/templates/initializer.rb +47 -0
- data/lib/generators/command_proposal/install/templates/install_command_proposal.rb +40 -0
- data/lib/tasks/command_proposal_tasks.rake +4 -0
- metadata +167 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
module ApplicationHelper
|
3
|
+
# In order to keep the regular app's routes working in the base template, we have to manually
|
4
|
+
# render the engine routes. Built a helper for this because it's long and nasty otherwise.
|
5
|
+
def cmd_path(*args)
|
6
|
+
return string_path(*args) if args.first.is_a?(String)
|
7
|
+
model_names = [:tasks, :iterations, :comments, :task, :iteration, :comment]
|
8
|
+
host = nil
|
9
|
+
args.map! { |arg|
|
10
|
+
next host ||= arg.delete(:host) if arg.is_a?(Hash) && arg.key?(:host)
|
11
|
+
if arg.in?(model_names)
|
12
|
+
"command_proposal_#{arg}".to_sym
|
13
|
+
elsif arg == :task_iterations
|
14
|
+
:iterations
|
15
|
+
else
|
16
|
+
arg
|
17
|
+
end
|
18
|
+
}
|
19
|
+
args << { host: host, port: nil } if host.present?
|
20
|
+
|
21
|
+
begin
|
22
|
+
router.url_for(args.compact)
|
23
|
+
rescue NoMethodError => e
|
24
|
+
raise "Error generating route! Please make sure `default_url_options` are set."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def string_path(*args)
|
29
|
+
[router.command_proposal_tasks_url + args.shift, args.to_param.presence].compact.join("?")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Runner controller doesn't map to a model, so needs special handling
|
33
|
+
def runner_path(task, iteration=nil)
|
34
|
+
if iteration.present?
|
35
|
+
router.command_proposal_task_runner_url(task, iteration)
|
36
|
+
else
|
37
|
+
router.command_proposal_task_runner_index_url(task)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def router
|
42
|
+
@@router ||= begin
|
43
|
+
routes = ::CommandProposal::Engine.routes
|
44
|
+
routes.default_url_options = rails_default_url_options
|
45
|
+
routes.url_helpers
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def rails_default_url_options
|
50
|
+
Rails.application.config.action_mailer.default_url_options.tap do |default_opts|
|
51
|
+
default_opts ||= {}
|
52
|
+
default_opts[:host] ||= "localhost"
|
53
|
+
default_opts[:port] ||= "3000"
|
54
|
+
default_opts[:protocol] ||= "http"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
module IconsHelper
|
3
|
+
def icon(status)
|
4
|
+
{
|
5
|
+
created: "cmd-icon-grey fa fa-check",
|
6
|
+
approved: "cmd-icon-green fa fa-check",
|
7
|
+
started: "cmd-icon-grey fa fa-clock-o",
|
8
|
+
cancelling: "cmd-icon-yellow fa fa-hourglass-half",
|
9
|
+
cancelled: "cmd-icon-yellow fa fa-stop-circle",
|
10
|
+
failed: "cmd-icon-red fa fa-times-circle",
|
11
|
+
success: "cmd-icon-green fa fa-check-circle",
|
12
|
+
}[status&.to_sym] || "cmd-icon-yellow fa fa-question"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ::CommandProposal::ParamsHelper
|
2
|
+
def sort_order
|
3
|
+
params[:order] == "desc" ? "desc" : "asc"
|
4
|
+
end
|
5
|
+
|
6
|
+
def toggled_sort_order
|
7
|
+
params[:order] == "desc" ? "asc" : "desc"
|
8
|
+
end
|
9
|
+
|
10
|
+
def current_params(merged={})
|
11
|
+
params.except(:action, :controller, :host, :port, :authenticity_token, :utf8, :commit).to_unsafe_h.merge(merged)
|
12
|
+
end
|
13
|
+
|
14
|
+
def toggled_param(toggle_h)
|
15
|
+
toggle_key = toggle_h.keys.first
|
16
|
+
toggle_val = toggle_h.values.first
|
17
|
+
|
18
|
+
if params[toggle_key].to_s == toggle_val.to_s
|
19
|
+
cmd_path(:tasks, current_params.except(toggle_key))
|
20
|
+
else
|
21
|
+
cmd_path(:tasks, current_params(toggle_h))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def truthy?(val)
|
26
|
+
val.to_s.downcase.in?(["true", "t", "1"])
|
27
|
+
end
|
28
|
+
|
29
|
+
def true_param?(*param_keys)
|
30
|
+
truthy?(params&.dig(*param_keys))
|
31
|
+
end
|
32
|
+
|
33
|
+
def humanized_duration(seconds)
|
34
|
+
return "N/A" if seconds.blank?
|
35
|
+
|
36
|
+
remaining = seconds.round
|
37
|
+
str_parts = []
|
38
|
+
|
39
|
+
durations = {
|
40
|
+
w: 7 * 24 * 60 * 60,
|
41
|
+
d: 24 * 60 * 60,
|
42
|
+
h: 60 * 60,
|
43
|
+
m: 60,
|
44
|
+
s: 1,
|
45
|
+
}
|
46
|
+
|
47
|
+
durations.each do |label, length|
|
48
|
+
count_at_length = 0
|
49
|
+
|
50
|
+
while remaining > length do
|
51
|
+
remaining -= length
|
52
|
+
count_at_length += 1
|
53
|
+
end
|
54
|
+
|
55
|
+
next if count_at_length == 0
|
56
|
+
|
57
|
+
str_parts.push("#{count_at_length}#{label}")
|
58
|
+
end
|
59
|
+
|
60
|
+
return "< 1s" if str_parts.none?
|
61
|
+
str_parts.join(" ")
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
module PermissionsHelper
|
3
|
+
def can_command?(user=command_user)
|
4
|
+
return true unless cmd_config.approval_required?
|
5
|
+
|
6
|
+
command_user.try("#{cmd_config.role_scope}?")
|
7
|
+
end
|
8
|
+
|
9
|
+
def can_approve?(iteration)
|
10
|
+
return true unless cmd_config.approval_required?
|
11
|
+
return if iteration.nil?
|
12
|
+
|
13
|
+
command_user.try("#{cmd_config.role_scope}?") && !current_is_author?(iteration)
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_approval?(task)
|
17
|
+
return true unless cmd_config.approval_required?
|
18
|
+
|
19
|
+
task&.approved?
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_is_author?(iteration)
|
23
|
+
command_user&.id == iteration&.requester&.id
|
24
|
+
end
|
25
|
+
|
26
|
+
def command_user(user=nil)
|
27
|
+
@command_user ||= begin
|
28
|
+
if user.present?
|
29
|
+
user
|
30
|
+
elsif cmd_config.controller_var.blank?
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
try(cmd_config.controller_var)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def cmd_config
|
39
|
+
::CommandProposal.configuration
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
class CommandRunnerJob < ApplicationJob
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def perform(iteration_id)
|
6
|
+
iteration = ::CommandProposal::Iteration.find(iteration_id)
|
7
|
+
|
8
|
+
::CommandProposal::Services::Runner.new.execute(iteration)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# belongs_to :iteration
|
2
|
+
# integer :line_number
|
3
|
+
# belongs_to :author
|
4
|
+
# text :body
|
5
|
+
|
6
|
+
require "command_proposal/service/external_belong"
|
7
|
+
|
8
|
+
class ::CommandProposal::Comment < ApplicationRecord
|
9
|
+
self.table_name = :command_proposal_comments
|
10
|
+
include ::CommandProposal::Service::ExternalBelong
|
11
|
+
|
12
|
+
belongs_to :iteration, optional: true
|
13
|
+
external_belongs_to :author
|
14
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# has_many :comments
|
2
|
+
# belongs_to :task
|
3
|
+
# text :args
|
4
|
+
# text :code
|
5
|
+
# text :result
|
6
|
+
# integer :status
|
7
|
+
# belongs_to :requester
|
8
|
+
# belongs_to :approver
|
9
|
+
# datetime :approved_at
|
10
|
+
# datetime :started_at
|
11
|
+
# datetime :completed_at
|
12
|
+
# datetime :stopped_at
|
13
|
+
|
14
|
+
# ADD: iteration_count?
|
15
|
+
|
16
|
+
require "command_proposal/service/external_belong"
|
17
|
+
require "command_proposal/service/json_wrapper"
|
18
|
+
|
19
|
+
class ::CommandProposal::Iteration < ApplicationRecord
|
20
|
+
self.table_name = :command_proposal_iterations
|
21
|
+
serialize :args, ::CommandProposal::Service::JSONWrapper
|
22
|
+
include ::CommandProposal::Service::ExternalBelong
|
23
|
+
|
24
|
+
has_many :comments
|
25
|
+
belongs_to :task
|
26
|
+
external_belongs_to :requester
|
27
|
+
external_belongs_to :approver
|
28
|
+
|
29
|
+
enum status: {
|
30
|
+
created: 0,
|
31
|
+
approved: 1,
|
32
|
+
started: 2,
|
33
|
+
failed: 3,
|
34
|
+
cancelling: 4, # Running, but told to stop
|
35
|
+
cancelled: 5,
|
36
|
+
success: 6,
|
37
|
+
}
|
38
|
+
|
39
|
+
delegate :name, to: :task
|
40
|
+
delegate :description, to: :task
|
41
|
+
delegate :session_type, to: :task
|
42
|
+
|
43
|
+
def params
|
44
|
+
code.scan(/params\[[:\"\'](.*?)[\'\"]?\]/).flatten
|
45
|
+
end
|
46
|
+
|
47
|
+
def brings
|
48
|
+
bring_str = code.scan(/bring.*?\n/).flatten.first
|
49
|
+
return [] unless bring_str.present?
|
50
|
+
|
51
|
+
::CommandProposal::Task.module.where(friendly_id: bring_str.scan(/\s+\:(\w+),?/).flatten)
|
52
|
+
end
|
53
|
+
|
54
|
+
def complete?
|
55
|
+
success? || failed? || cancelled?
|
56
|
+
end
|
57
|
+
|
58
|
+
def pending?
|
59
|
+
created?
|
60
|
+
end
|
61
|
+
|
62
|
+
def duration
|
63
|
+
return unless started_at?
|
64
|
+
|
65
|
+
(completed_at || stopped_at || Time.current) - started_at
|
66
|
+
end
|
67
|
+
|
68
|
+
def force_reset
|
69
|
+
# Debugging method. Should never actually be called.
|
70
|
+
update(status: :approved, result: nil, completed_at: nil, stopped_at: nil, started_at: nil)
|
71
|
+
end
|
72
|
+
|
73
|
+
def line_count
|
74
|
+
return 0 if code.blank?
|
75
|
+
|
76
|
+
code.count("\n")
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
module Service
|
3
|
+
module ExternalBelong
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
def self.external_belongs_to key
|
8
|
+
define_getters key
|
9
|
+
define_setters key
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.define_getters key
|
13
|
+
define_method key do
|
14
|
+
return if user_class.blank?
|
15
|
+
|
16
|
+
if role_scope.present?
|
17
|
+
user_class.send(role_scope).find_by(id: public_send("#{key}_id"))
|
18
|
+
else
|
19
|
+
user_class.find_by(id: public_send("#{key}_id"))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method "#{key}_name" do
|
24
|
+
public_send(key)&.public_send(user_name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.define_setters key
|
29
|
+
define_method "#{key}=" do |obj|
|
30
|
+
self.send("#{key}_id=", obj&.id)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def user_class
|
35
|
+
::CommandProposal.configuration.user_class
|
36
|
+
end
|
37
|
+
|
38
|
+
def role_scope
|
39
|
+
::CommandProposal.configuration.role_scope
|
40
|
+
end
|
41
|
+
|
42
|
+
def user_name
|
43
|
+
::CommandProposal.configuration.user_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
module Service
|
3
|
+
class JSONWrapper
|
4
|
+
# Allows directly setting pre-stringified JSON.
|
5
|
+
def self.dump(obj)
|
6
|
+
return obj if obj.is_a?(String)
|
7
|
+
|
8
|
+
JSON.dump(obj)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(str)
|
12
|
+
return {} unless str.present?
|
13
|
+
|
14
|
+
JSON.parse(str).with_indifferent_access
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CommandProposal
|
2
|
+
module Service
|
3
|
+
class ProposalPresenter
|
4
|
+
include ::CommandProposal::ApplicationHelper
|
5
|
+
attr_accessor :iteration
|
6
|
+
|
7
|
+
def initialize(iteration)
|
8
|
+
@iteration = iteration
|
9
|
+
end
|
10
|
+
|
11
|
+
delegate :name, to: :iteration
|
12
|
+
delegate :description, to: :iteration
|
13
|
+
delegate :args, to: :iteration
|
14
|
+
delegate :code, to: :iteration
|
15
|
+
delegate :status, to: :iteration
|
16
|
+
delegate :approved_at, to: :iteration
|
17
|
+
delegate :started_at, to: :iteration
|
18
|
+
delegate :completed_at, to: :iteration
|
19
|
+
delegate :stopped_at, to: :iteration
|
20
|
+
delegate :duration, to: :iteration
|
21
|
+
|
22
|
+
def url(host: nil)
|
23
|
+
cmd_path(@iteration.task, host: host)
|
24
|
+
end
|
25
|
+
|
26
|
+
def requester
|
27
|
+
@iteration.requester_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def approver
|
31
|
+
@iteration.approver_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def type
|
35
|
+
@iteration.session_type
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# has_many :iterations
|
2
|
+
# text :name
|
3
|
+
# text :friendly_id
|
4
|
+
# text :description
|
5
|
+
# integer :session_type
|
6
|
+
# datetime :last_executed_at
|
7
|
+
|
8
|
+
class ::CommandProposal::Task < ApplicationRecord
|
9
|
+
self.table_name = :command_proposal_tasks
|
10
|
+
attr_accessor :user
|
11
|
+
|
12
|
+
has_many :iterations
|
13
|
+
has_many :ordered_iterations, -> { order(created_at: :desc) }, class_name: "CommandProposal::Iteration"
|
14
|
+
|
15
|
+
scope :search, ->(text) {
|
16
|
+
where("name ILIKE :q OR description ILIKE :q", q: "%#{text}%")
|
17
|
+
}
|
18
|
+
|
19
|
+
enum session_type: {
|
20
|
+
# Task will have multiple iterations that are all essentially the same just with code changes
|
21
|
+
task: 0,
|
22
|
+
# Console iterations are actually line by line, so order matters
|
23
|
+
console: 1,
|
24
|
+
# Function iterations are much like tasks
|
25
|
+
function: 2,
|
26
|
+
# Modules are included in tasks and not run independently
|
27
|
+
module: 3,
|
28
|
+
}
|
29
|
+
|
30
|
+
validates :name, presence: true
|
31
|
+
|
32
|
+
after_initialize -> { self.session_type ||= :task }
|
33
|
+
before_save -> { self.friendly_id = to_param }
|
34
|
+
|
35
|
+
delegate :line_count, to: :current_iteration, allow_nil: true
|
36
|
+
delegate :code, to: :current_iteration, allow_nil: true
|
37
|
+
delegate :result, to: :current_iteration, allow_nil: true
|
38
|
+
delegate :status, to: :primary_iteration, allow_nil: true
|
39
|
+
delegate :duration, to: :primary_iteration, allow_nil: true
|
40
|
+
|
41
|
+
def lines
|
42
|
+
iterations.order(created_at: :asc).where.not(id: first_iteration.id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_param
|
46
|
+
friendly_id || generate_friendly_id
|
47
|
+
end
|
48
|
+
|
49
|
+
def approved?
|
50
|
+
primary_iteration&.approved_at?
|
51
|
+
end
|
52
|
+
|
53
|
+
def first_iteration
|
54
|
+
ordered_iterations.last
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_iteration
|
58
|
+
ordered_iterations.first
|
59
|
+
end
|
60
|
+
|
61
|
+
def primary_iteration
|
62
|
+
console? ? first_iteration : current_iteration
|
63
|
+
end
|
64
|
+
|
65
|
+
def current_iteration_at
|
66
|
+
current_iteration&.completed_at
|
67
|
+
end
|
68
|
+
|
69
|
+
def current_iteration_by
|
70
|
+
current_iteration&.requester_name
|
71
|
+
end
|
72
|
+
|
73
|
+
def started_at
|
74
|
+
iterations.minimum(:started_at)
|
75
|
+
end
|
76
|
+
|
77
|
+
def completed_at
|
78
|
+
iterations.maximum(:completed_at)
|
79
|
+
end
|
80
|
+
|
81
|
+
def code=(new_code)
|
82
|
+
iterations.create(code: new_code, requester: user)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def reserved_names
|
88
|
+
[
|
89
|
+
"new",
|
90
|
+
"edit",
|
91
|
+
]
|
92
|
+
end
|
93
|
+
|
94
|
+
def generate_friendly_id
|
95
|
+
return if name.blank?
|
96
|
+
temp_id = name.downcase.gsub(/\s+/, "_").gsub(/[^a-z_]/, "")
|
97
|
+
|
98
|
+
loop do
|
99
|
+
duplicate_names = self.class.where(friendly_id: temp_id).where.not(id: id)
|
100
|
+
|
101
|
+
return temp_id if duplicate_names.none? && reserved_names.exclude?(temp_id)
|
102
|
+
|
103
|
+
temp_id = "#{temp_id}_#{duplicate_names.count}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|