command_proposal 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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