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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +77 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/command_proposal_manifest.js +1 -0
  6. data/app/assets/javascripts/command_proposal/_codemirror.js +9814 -0
  7. data/app/assets/javascripts/command_proposal/_helpers.js +9 -0
  8. data/app/assets/javascripts/command_proposal/codemirror-addon-searchcursor.js +296 -0
  9. data/app/assets/javascripts/command_proposal/codemirror-keymap-sublime.js +720 -0
  10. data/app/assets/javascripts/command_proposal/codemirror-mode-ruby.js +303 -0
  11. data/app/assets/javascripts/command_proposal/console.js +195 -0
  12. data/app/assets/javascripts/command_proposal/feed.js +51 -0
  13. data/app/assets/javascripts/command_proposal/terminal.js +40 -0
  14. data/app/assets/javascripts/command_proposal.js +1 -0
  15. data/app/assets/stylesheets/command_proposal/_variables.scss +0 -0
  16. data/app/assets/stylesheets/command_proposal/codemirror-rubyblue.scss +27 -0
  17. data/app/assets/stylesheets/command_proposal/codemirror.scss +367 -0
  18. data/app/assets/stylesheets/command_proposal/command_proposal.scss +1 -0
  19. data/app/assets/stylesheets/command_proposal/components.scss +31 -0
  20. data/app/assets/stylesheets/command_proposal/containers.scss +4 -0
  21. data/app/assets/stylesheets/command_proposal/icons.scss +12 -0
  22. data/app/assets/stylesheets/command_proposal/tables.scss +76 -0
  23. data/app/assets/stylesheets/command_proposal/terminal.scss +72 -0
  24. data/app/assets/stylesheets/command_proposal.scss +5 -0
  25. data/app/controllers/command_proposal/engine_controller.rb +6 -0
  26. data/app/controllers/command_proposal/iterations_controller.rb +83 -0
  27. data/app/controllers/command_proposal/runner_controller.rb +86 -0
  28. data/app/controllers/command_proposal/tasks_controller.rb +97 -0
  29. data/app/helpers/command_proposal/application_helper.rb +58 -0
  30. data/app/helpers/command_proposal/icons_helper.rb +15 -0
  31. data/app/helpers/command_proposal/params_helper.rb +63 -0
  32. data/app/helpers/command_proposal/permissions_helper.rb +42 -0
  33. data/app/jobs/command_proposal/application_job.rb +4 -0
  34. data/app/jobs/command_proposal/command_runner_job.rb +11 -0
  35. data/app/models/command_proposal/comment.rb +14 -0
  36. data/app/models/command_proposal/iteration.rb +78 -0
  37. data/app/models/command_proposal/service/external_belong.rb +48 -0
  38. data/app/models/command_proposal/service/json_wrapper.rb +18 -0
  39. data/app/models/command_proposal/service/proposal_presenter.rb +39 -0
  40. data/app/models/command_proposal/task.rb +106 -0
  41. data/app/views/command_proposal/tasks/_console_show.html.erb +44 -0
  42. data/app/views/command_proposal/tasks/_function_show.html.erb +54 -0
  43. data/app/views/command_proposal/tasks/_lines.html.erb +8 -0
  44. data/app/views/command_proposal/tasks/_module_show.html.erb +33 -0
  45. data/app/views/command_proposal/tasks/_past_iterations_list.html.erb +20 -0
  46. data/app/views/command_proposal/tasks/_task_detail_table.html.erb +31 -0
  47. data/app/views/command_proposal/tasks/_task_show.html.erb +55 -0
  48. data/app/views/command_proposal/tasks/error.html.erb +4 -0
  49. data/app/views/command_proposal/tasks/form.html.erb +64 -0
  50. data/app/views/command_proposal/tasks/index.html.erb +44 -0
  51. data/app/views/command_proposal/tasks/show.html.erb +10 -0
  52. data/config/routes.rb +11 -0
  53. data/lib/command_proposal/configuration.rb +41 -0
  54. data/lib/command_proposal/engine.rb +6 -0
  55. data/lib/command_proposal/services/command_interpreter.rb +108 -0
  56. data/lib/command_proposal/services/runner.rb +157 -0
  57. data/lib/command_proposal/version.rb +3 -0
  58. data/lib/command_proposal.rb +27 -0
  59. data/lib/generators/command_proposal/install/install_generator.rb +28 -0
  60. data/lib/generators/command_proposal/install/templates/initializer.rb +47 -0
  61. data/lib/generators/command_proposal/install/templates/install_command_proposal.rb +40 -0
  62. data/lib/tasks/command_proposal_tasks.rake +4 -0
  63. 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,4 @@
1
+ module CommandProposal
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ 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