dynflow 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -0,0 +1,29 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyExecutor
4
+ attr_reader :world
5
+
6
+ def initialize(world)
7
+ @world = world
8
+ @events_to_process = []
9
+ end
10
+
11
+ def event(execution_plan_id, step_id, event, future = Future.new)
12
+ @events_to_process << [execution_plan_id, step_id, event, future]
13
+ end
14
+
15
+ def progress
16
+ events = @events_to_process.dup
17
+ clear
18
+ events.each do |execution_plan_id, step_id, event, future|
19
+ future.resolve true
20
+ world.action.execute event
21
+ end
22
+ end
23
+
24
+ def clear
25
+ @events_to_process.clear
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyPlannedAction
4
+ attr_accessor :output, :plan_input
5
+ include Mimic
6
+
7
+ def initialize(klass)
8
+ mimic! klass
9
+ @output = ExecutionPlan::OutputReference.new(Testing.get_id, Testing.get_id)
10
+ end
11
+
12
+ def execute(execution_plan, event, *args)
13
+ @plan_input = args
14
+ self
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyStep
4
+ extend Mimic
5
+ mimic! ExecutionPlan::Steps::Abstract
6
+
7
+ attr_accessor :state, :error
8
+ attr_reader :id
9
+
10
+ def initialize
11
+ @state = :pending
12
+ @id = Testing.get_id
13
+ end
14
+
15
+ def save
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyWorld
4
+ extend Mimic
5
+ mimic! World
6
+
7
+ attr_reader :clock, :executor
8
+ attr_accessor :action
9
+
10
+ def initialize
11
+ @logger_adapter = Testing.logger_adapter
12
+ @clock = ManagedClock.new
13
+ @executor = DummyExecutor.new(self)
14
+ end
15
+
16
+ def action_logger
17
+ @logger_adapter.action_logger
18
+ end
19
+
20
+ def logger
21
+ @logger_adapter.dynflow_logger
22
+ end
23
+
24
+ def subscribed_actions(klass)
25
+ []
26
+ end
27
+
28
+ def event(execution_plan_id, step_id, event, future = Future.new)
29
+ executor.event execution_plan_id, step_id, event, future
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,83 @@
1
+ module Dynflow
2
+ module Testing
3
+ module Factories
4
+ include Algebrick::TypeCheck
5
+
6
+ # @return [Action::PlanPhase]
7
+ def create_action(action_class, trigger = nil)
8
+ execution_plan = DummyExecutionPlan.new
9
+ step = DummyStep.new
10
+ action_class.plan_phase.new(
11
+ { step: DummyStep.new,
12
+ execution_plan_id: execution_plan.id,
13
+ id: Testing.get_id,
14
+ plan_step_id: step.id },
15
+ execution_plan, trigger)
16
+ end
17
+
18
+ # @return [Action::PlanPhase]
19
+ def plan_action(plan_action, *args, &block)
20
+ Type! plan_action, Dynflow::Action::PlanPhase
21
+
22
+ plan_action.execute *args, &block
23
+ raise plan_action.error if plan_action.error
24
+ plan_action
25
+ end
26
+
27
+ def create_and_plan_action(action_class, *args, &block)
28
+ plan_action create_action(action_class), *args, &block
29
+ end
30
+
31
+ # @return [Action::RunPhase]
32
+ def run_action(plan_action, event = nil, &stubbing)
33
+ Type! plan_action, Dynflow::Action::PlanPhase, Dynflow::Action::RunPhase
34
+ step = DummyStep.new
35
+ run_action = if Dynflow::Action::PlanPhase === plan_action
36
+ plan_action.action_class.run_phase.new(
37
+ { step: step,
38
+ execution_plan_id: plan_action.execution_plan_id,
39
+ id: plan_action.id,
40
+ plan_step_id: plan_action.plan_step_id,
41
+ run_step_id: step.id,
42
+ input: plan_action.input },
43
+ plan_action.world)
44
+
45
+ else
46
+ plan_action
47
+ end
48
+
49
+ run_action.world.action ||= run_action
50
+ run_action.world.clock.clear
51
+ stubbing.call run_action if stubbing
52
+ run_action.execute event
53
+ raise run_action.error if run_action.error
54
+ run_action
55
+ end
56
+
57
+ # @return [Action::FinalizePhase]
58
+ def finalize_action(run_action, &stubbing)
59
+ Type! run_action, Dynflow::Action::RunPhase
60
+ step = DummyStep.new
61
+ finalize_action = run_action.action_class.finalize_phase.new(
62
+ { step: step,
63
+ execution_plan_id: run_action.execution_plan_id,
64
+ id: run_action.id,
65
+ plan_step_id: run_action.plan_step_id,
66
+ run_step_id: run_action.run_step_id,
67
+ finalize_step_id: step.id,
68
+ input: run_action.input },
69
+ run_action.world)
70
+
71
+ stubbing.call finalize_action if stubbing
72
+ finalize_action.execute
73
+ finalize_action
74
+ end
75
+
76
+ def progress_action_time action
77
+ Type! action, Dynflow::Action::RunPhase
78
+ action.world.clock.progress
79
+ action.world.executor.progress
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,23 @@
1
+ module Dynflow
2
+ module Testing
3
+ class ManagedClock
4
+ def initialize
5
+ @pings_to_process = []
6
+ end
7
+
8
+ def ping(who, time, with_what = nil, where = :<<)
9
+ @pings_to_process << [who, [where, with_what].compact]
10
+ end
11
+
12
+ def progress
13
+ copy = @pings_to_process.dup
14
+ clear
15
+ copy.each { |who, args| who.send *args }
16
+ end
17
+
18
+ def clear
19
+ @pings_to_process.clear
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module Dynflow
2
+ module Testing
3
+
4
+ # when extended into Class or an_object it makes all instances of the class or the object
5
+ # mimic the supplied types. It does so by hooking into kind_of? method.
6
+ # @example
7
+ # m = mock('product')
8
+ # m.is_a? ::Product # => false
9
+ # m.extend Mimic
10
+ # m.mimic! ::Product
11
+ # m.is_a? ::Product # => true
12
+ module Mimic
13
+ class ::Module
14
+ def ===(v)
15
+ v.kind_of? self
16
+ end
17
+ end
18
+
19
+ def mimic!(*types)
20
+ define =-> _ do
21
+ define_method :kind_of? do |type|
22
+ types.any? { |t| t <= type } || super(type)
23
+ end
24
+
25
+ alias_method :is_a?, :kind_of?
26
+ end
27
+
28
+ if self.kind_of? ::Class
29
+ self.class_eval &define
30
+ else
31
+ self.singleton_class.class_eval &define
32
+ end
33
+
34
+ self
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+
4
+ require 'dynflow/transaction_adapters/abstract'
5
+ require 'dynflow/transaction_adapters/none'
6
+ require 'dynflow/transaction_adapters/active_record'
7
+
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+ class Abstract
4
+ # start transaction around +block+
5
+ def transaction(&block)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ # rollback the transaction
10
+ def rollback
11
+ raise NotImplementedError
12
+ end
13
+
14
+ # Called on each thread after work is done.
15
+ # E.g. it's used to checkin ActiveRecord connections back to pool.
16
+ def cleanup
17
+ # override if needed
18
+ end
19
+
20
+ # Called after World instantiation, it can be used to check Dynflow configuration etc.
21
+ def check(world)
22
+ # override if needed
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+ class ActiveRecord < Abstract
4
+ def transaction(&block)
5
+ ::ActiveRecord::Base.transaction(&block)
6
+ end
7
+
8
+ def rollback
9
+ raise ::ActiveRecord::Rollback
10
+ end
11
+
12
+ def cleanup
13
+ ::ActiveRecord::Base.clear_active_connections!
14
+ end
15
+
16
+ def check(world)
17
+ # missing reader in ConnectionPool
18
+ ar_pool_size = ::ActiveRecord::Base.connection_pool.instance_variable_get(:@size)
19
+ if (world.options[:pool_size] / 2.0) > ar_pool_size
20
+ world.logger.warn 'Consider increasing ActiveRecord::Base.connection_pool size, ' +
21
+ "it's #{ar_pool_size} but there is #{world.options[:pool_size]} " +
22
+ 'threads in Dynflow pool.'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+ class None < Abstract
4
+ def transaction(&block)
5
+ block.call
6
+ end
7
+
8
+ def rollback
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = "0.1.0"
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,277 @@
1
+ require 'dynflow'
2
+ require 'pp'
3
+ require 'sinatra'
4
+
5
+ module Dynflow
6
+ class WebConsole < Sinatra::Base
7
+
8
+ def self.setup(&block)
9
+ Sinatra.new(self) do
10
+ instance_exec(&block)
11
+ end
12
+ end
13
+
14
+ web_dir = File.join(File.expand_path('../../../web', __FILE__))
15
+
16
+ set :public_folder, File.join(web_dir, 'assets')
17
+ set :views, File.join(web_dir, 'views')
18
+ set :per_page, 10
19
+
20
+ helpers ERB::Util
21
+
22
+ helpers do
23
+ def world
24
+ settings.world
25
+ end
26
+
27
+ def prettyprint(value)
28
+ value = prettyprint_references(value)
29
+ if value
30
+ pretty_value = value.pretty_inspect
31
+ <<-HTML
32
+ <pre class="prettyprint">#{h(pretty_value)}</pre>
33
+ HTML
34
+ else
35
+ ""
36
+ end
37
+ end
38
+
39
+ def prettyprint_references(value)
40
+ case value
41
+ when Hash
42
+ value.reduce({}) do |h, (key, val)|
43
+ h.update(key => prettyprint_references(val))
44
+ end
45
+ when Array
46
+ value.map { |val| prettyprint_references(val) }
47
+ when ExecutionPlan::OutputReference
48
+ value.inspect
49
+ else
50
+ value
51
+ end
52
+ end
53
+
54
+ def load_action(step)
55
+ world.persistence.load_action(step)
56
+ end
57
+
58
+ def step_error(step)
59
+ if step.error
60
+ ['<pre>',
61
+ "#{h(step.error.message)} (#{h(step.error.exception_class)})\n",
62
+ h(step.error.backtrace.join("\n")),
63
+ '</pre>'].join
64
+ end
65
+ end
66
+
67
+ def show_action_data(label, value)
68
+ value_html = prettyprint(value)
69
+ if !value_html.empty?
70
+ <<-HTML
71
+ <p>
72
+ #{h(label)}
73
+ #{value_html}
74
+ </p>
75
+ HTML
76
+ else
77
+ ""
78
+ end
79
+ end
80
+
81
+ def atom_css_classes(atom)
82
+ classes = ["atom"]
83
+ step = @plan.steps[atom.step_id]
84
+ case step.state
85
+ when :success
86
+ classes << "success"
87
+ when :error
88
+ classes << "error"
89
+ when :skipped
90
+ classes << "skipped"
91
+ end
92
+ return classes.join(" ")
93
+ end
94
+
95
+ def flow_css_classes(flow, sub_flow = nil)
96
+ classes = []
97
+ case flow
98
+ when Flows::Sequence
99
+ classes << "sequence"
100
+ when Flows::Concurrence
101
+ classes << "concurrence"
102
+ when Flows::Atom
103
+ classes << atom_css_classes(flow)
104
+ else
105
+ raise "Unknown run plan #{run_plan.inspect}"
106
+ end
107
+ classes << atom_css_classes(sub_flow) if sub_flow.is_a? Flows::Atom
108
+ return classes.join(" ")
109
+ end
110
+
111
+ def step_css_class(step)
112
+ case step.state
113
+ when :success
114
+ "success"
115
+ when :error
116
+ "important"
117
+ end
118
+ end
119
+
120
+ def progress_width(action)
121
+ if action.state == :error
122
+ 100 # we want to show the red bar in full width
123
+ else
124
+ action.progress_done * 100
125
+ end
126
+ end
127
+
128
+ def step(step_id)
129
+ @plan.steps[step_id]
130
+ end
131
+
132
+ def paginate?
133
+ world.persistence.adapter.pagination?
134
+ end
135
+
136
+ def updated_url(new_params)
137
+ url("?" + Rack::Utils.build_query(params.merge(new_params.stringify_keys)))
138
+ end
139
+
140
+ def paginated_url(delta)
141
+ h(updated_url(page: [0, page + delta].max))
142
+ end
143
+
144
+ def pagination_options
145
+ if paginate?
146
+ { page: page, per_page: per_page }
147
+ else
148
+ if params[:page] || params[:per_page]
149
+ halt 400, "The persistence doesn't support pagination"
150
+ end
151
+ return {}
152
+ end
153
+ end
154
+
155
+ def page
156
+ (params[:page] || 0).to_i
157
+ end
158
+
159
+ def per_page
160
+ (params[:per_page] || 10).to_i
161
+ end
162
+
163
+ def supported_ordering?(ord_attr)
164
+ world.persistence.adapter.ordering_by.any? do |attr|
165
+ attr.to_s == ord_attr.to_s
166
+ end
167
+ end
168
+
169
+ def ordering_options
170
+ return @ordering_options if @ordering_options
171
+
172
+ if params[:order_by]
173
+ unless supported_ordering?(params[:order_by])
174
+ halt 400, "Unsupported ordering"
175
+ end
176
+ @ordering_options = { order_by: params[:order_by],
177
+ desc: (params[:desc] == 'true') }
178
+ elsif supported_ordering?('started_at')
179
+ @ordering_options = { order_by: 'started_at', desc: true }
180
+ else
181
+ @ordering_options = {}
182
+ end
183
+ return @ordering_options
184
+ end
185
+
186
+ def order_link(attr, label)
187
+ return h(label) unless supported_ordering?(attr)
188
+ new_ordering_options = { order_by: attr.to_s,
189
+ desc: false }
190
+ arrow = ""
191
+ if ordering_options[:order_by].to_s == attr.to_s
192
+ arrow = ordering_options[:desc] ? "&#9660;" : "&#9650;"
193
+ new_ordering_options[:desc] = !ordering_options[:desc]
194
+ end
195
+ url = updated_url(new_ordering_options)
196
+ return %{<a href="#{url}"> #{arrow} #{h(label)}</a>}
197
+ end
198
+
199
+ def supported_filter?(filter_attr)
200
+ world.persistence.adapter.filtering_by.any? do |attr|
201
+ attr.to_s == filter_attr.to_s
202
+ end
203
+ end
204
+
205
+ def filtering_options
206
+ return @filtering_options if @filtering_options
207
+
208
+ if params[:filters]
209
+ params[:filters].map do |key, value|
210
+ unless supported_filter?(key)
211
+ halt 400, "Unsupported ordering"
212
+ end
213
+ end
214
+
215
+ filters = params[:filters]
216
+ elsif supported_filter?('state')
217
+ filters = { 'state' => ExecutionPlan.states.map(&:to_s) - ['stopped'] }
218
+ else
219
+ filters = {}
220
+ end
221
+ @filtering_options = { filters: filters }.with_indifferent_access
222
+ return @filtering_options
223
+ end
224
+
225
+ def filter_checkbox(field, values)
226
+ out = "<p>#{field}: %s</p>"
227
+ checkboxes = values.map do |value|
228
+ field_filter = filtering_options[:filters][field]
229
+ checked = field_filter && field_filter.include?(value)
230
+ %{<input type="checkbox" name="filters[#{field}][]" value="#{value}" #{ "checked" if checked }/>#{value}}
231
+ end.join(' ')
232
+ out %= checkboxes
233
+ return out
234
+ end
235
+
236
+ end
237
+
238
+ get('/') do
239
+ options = HashWithIndifferentAccess.new
240
+ options.merge!(filtering_options)
241
+ options.merge!(pagination_options)
242
+ options.merge!(ordering_options)
243
+
244
+ @plans = world.persistence.find_execution_plans(options)
245
+ erb :index
246
+ end
247
+
248
+ get('/:id') do |id|
249
+ @plan = world.persistence.load_execution_plan(id)
250
+ erb :show
251
+ end
252
+
253
+ post('/:id/resume') do |id|
254
+ plan = world.persistence.load_execution_plan(id)
255
+ if plan.state != :paused
256
+ redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to resume')}")
257
+ else
258
+ world.execute(plan.id)
259
+ redirect(url "/#{plan.id}?notice=#{url_encode('The execution was resumed')}")
260
+ end
261
+ end
262
+
263
+ post('/:id/skip/:step_id') do |id, step_id|
264
+ plan = world.persistence.load_execution_plan(id)
265
+ step = plan.steps[step_id.to_i]
266
+ if plan.state != :paused
267
+ redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to skip')}")
268
+ elsif step.state != :error
269
+ redirect(url "/#{plan.id}?notice=#{url_encode('The step has to be failed to be able to skip')}")
270
+ else
271
+ plan.skip(step)
272
+ redirect(url "/#{plan.id}")
273
+ end
274
+ end
275
+
276
+ end
277
+ end