petri_flow 0.1.7 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b75f2ac3288f2b169d4364a8aaa11586497d710477ecabe92adb6af2ffbf7376
4
- data.tar.gz: 3a12e23472cc059ba4437ee1535d216b4df290f64d1de05c54a8395a648c5246
3
+ metadata.gz: 6b2bb56ad2f157e6a7fe5d3cbee2bfab0e3fcef64f9137bd709ac37ea27949b9
4
+ data.tar.gz: c2721a5074ba1fac6462ad95bef1012c7f00bfb74ff61a1eb6ae284f97c6b013
5
5
  SHA512:
6
- metadata.gz: 6aad07e5b69eb1ed0046afaa59f9679a3f77b259fa7410533eb2939883b43da7adf7518fe4b4d17e1d3f3f3b40feca8d12c954d9c837ca9c052bf1af84385942
7
- data.tar.gz: e278b0c46d95a0b7744ef457ebac2d8f8f56d5ac943ff009264086490b762378cfb37ea3e7c5b64362d7fe09d674374f48bb0eb4008f90d7aa732343cd1cd4f0
6
+ metadata.gz: 3f0f933aa496d45610048de363e7fd189586dab4af85bfb012e2b93a55703104163f0183655f61a4a8c60d7f3a20c1f9c69c64d451eb601c3da73e68e0f7b4d1
7
+ data.tar.gz: 78f35f561c65b366a7c21e20420d9235813b1368f1d06e0e87a35e888a0f8755317272854dd96224acae9ca42626635d44a1551d9b2e9853411348f45a87f2e1
data/README.md CHANGED
@@ -8,6 +8,7 @@ Workflow engine for Rails.
8
8
  * Simple web admin for workflow definition and case management.
9
9
  * Build-in simple dynamic form.
10
10
  * Replaceable dynamic form.
11
+ * Support sub workflow.
11
12
  * Graph screen for workflow definition.
12
13
  * Graph screen for case and token migration.
13
14
  * Powerful guard expression.
@@ -64,7 +64,8 @@ module Wf
64
64
  :assignment_callback,
65
65
  :unassignment_callback,
66
66
  :notification_callback,
67
- :deadline_callback
67
+ :deadline_callback,
68
+ :sub_workflow_id
68
69
  )
69
70
  end
70
71
  end
@@ -58,9 +58,13 @@ module Wf
58
58
 
59
59
  def finish_and_redirect
60
60
  if @workitem.case.finished?
61
- redirect_to workflow_case_path(@workitem.workflow, @workitem.case), notice: "workitem is done, and the case is finished."
61
+ if started_by = @workitem.case.started_by_workitem
62
+ redirect_to workflow_case_path(started_by.workflow, started_by.case), notice: "workitem is done, and goto parent case."
63
+ else
64
+ redirect_to workflow_case_path(@workitem.workflow, @workitem.case), notice: "workitem is done, and the case is finished."
65
+ end
62
66
  else
63
- redirect_to workitem_path(Wf::Workitem.last.case.workitems.enabled.first), notice: "workitem is done, and goto next fireable workitem."
67
+ redirect_to workitem_path(@workitem.case.workitems.enabled.first), notice: "workitem is done, and goto next fireable workitem."
64
68
  end
65
69
  end
66
70
 
@@ -4,19 +4,21 @@
4
4
  #
5
5
  # Table name: wf_cases
6
6
  #
7
- # id :integer not null, primary key
8
- # workflow_id :integer
9
- # targetable_type :string
10
- # targetable_id :string
11
- # state :integer default("0")
12
- # created_at :datetime not null
13
- # updated_at :datetime not null
7
+ # id :integer not null, primary key
8
+ # workflow_id :integer
9
+ # targetable_type :string
10
+ # targetable_id :string
11
+ # state :integer default("0")
12
+ # created_at :datetime not null
13
+ # updated_at :datetime not null
14
+ # started_by_workitem_id :integer
14
15
  #
15
16
 
16
17
  module Wf
17
18
  class Case < ApplicationRecord
18
19
  belongs_to :workflow
19
20
  belongs_to :targetable, optional: true, polymorphic: true
21
+ belongs_to :started_by_workitem, optional: true, class_name: "Wf::Workitem"
20
22
  has_many :workitems
21
23
  has_many :tokens
22
24
  has_many :case_assignments
@@ -26,6 +26,10 @@ module Wf::CaseCommand
26
26
  Wf::FireTimedWorkitemJob.set(wait: transition.trigger_limit.minutes).perform_later(workitem.id) if trigger_time
27
27
  SetWorkitemAssignments.call(workitem)
28
28
  workitem.transition.unassignment_callback.constantize.new(workitem.id).perform_now if workitem.workitem_assignments.count == 0
29
+ if sub_workflow = transition.sub_workflow
30
+ sub_case = Wf::CaseCommand::New.call(sub_workflow, nil, workitem).result
31
+ Wf::CaseCommand::StartCase.call(sub_case)
32
+ end
29
33
  end
30
34
  end
31
35
  end
@@ -20,7 +20,12 @@ module Wf::CaseCommand
20
20
  raise("The workflow net is misconstructed: Some parallel executions have not finished.") if free_and_locked_token_num > 1
21
21
 
22
22
  ConsumeToken.call(wf_case, end_place)
23
- wf_case.finished!
23
+ unless wf_case.finished?
24
+ wf_case.finished!
25
+ if started_by_workitem = wf_case.started_by_workitem
26
+ Wf::CaseCommand::FinishWorkitem.call(started_by_workitem)
27
+ end
28
+ end
24
29
  true
25
30
  end
26
31
  end
@@ -3,14 +3,15 @@
3
3
  module Wf::CaseCommand
4
4
  class New
5
5
  prepend SimpleCommand
6
- attr_reader :workflow, :target
7
- def initialize(workflow, target = nil)
6
+ attr_reader :workflow, :target, :started_by
7
+ def initialize(workflow, target = nil, started_by = nil)
8
8
  @workflow = workflow
9
9
  @target = target
10
+ @started_by = started_by
10
11
  end
11
12
 
12
13
  def call
13
- wf_case = workflow.cases.create!(targetable: target, state: :created)
14
+ wf_case = workflow.cases.create!(targetable: target, started_by_workitem: started_by, state: :created)
14
15
  wf_case
15
16
  end
16
17
  end
@@ -4,17 +4,16 @@
4
4
  #
5
5
  # Table name: wf_guards
6
6
  #
7
- # id :integer not null, primary key
8
- # arc_id :integer
9
- # workflow_id :integer
10
- # fieldable_type :string
11
- # fieldable_id :string
12
- # op :string
13
- # value :string
14
- # exp :string
15
- # created_at :datetime not null
16
- # updated_at :datetime not null
17
- # target_attr_name :string
7
+ # id :integer not null, primary key
8
+ # arc_id :integer
9
+ # workflow_id :integer
10
+ # fieldable_type :string
11
+ # fieldable_id :string
12
+ # op :string
13
+ # value :string
14
+ # exp :string
15
+ # created_at :datetime not null
16
+ # updated_at :datetime not null
18
17
  #
19
18
 
20
19
  module Wf
@@ -24,5 +24,9 @@ module Wf
24
24
  normal: 1,
25
25
  end: 2
26
26
  }
27
+
28
+ def graph_id
29
+ "#{name}/#{id}"
30
+ end
27
31
  end
28
32
  end
@@ -23,6 +23,7 @@
23
23
  # assignment_callback :string default("Wf::Callbacks::AssignmentDefault")
24
24
  # unassignment_callback :string default("Wf::Callbacks::UnassignmentDefault")
25
25
  # form_type :string default("Wf::Form")
26
+ # sub_workflow_id :integer
26
27
  #
27
28
 
28
29
  module Wf
@@ -33,6 +34,7 @@ module Wf
33
34
  has_many :static_parties, through: :transition_static_assignments, source: "party"
34
35
  has_many :workitems
35
36
  belongs_to :form, optional: true, polymorphic: true
37
+ belongs_to :sub_workflow, optional: true, class_name: "Wf::Workflow"
36
38
 
37
39
  enum trigger_type: {
38
40
  user: 0,
@@ -41,10 +43,24 @@ module Wf
41
43
  time: 3
42
44
  }
43
45
 
46
+ validate :validate_trigger_type_and_sub
47
+
48
+ def is_sub_workflow?
49
+ !!sub_workflow_id
50
+ end
51
+
44
52
  def explicit_or_split?
45
53
  arcs.out.sum(:guards_count) >= 1
46
54
  end
47
55
 
48
56
  validates :name, presence: true
57
+
58
+ def validate_trigger_type_and_sub
59
+ errors.add(:trigger_type, "sub workflow must have trigger type: automatic, message and time.") if user? && is_sub_workflow?
60
+ end
61
+
62
+ def graph_id
63
+ "#{name}/#{id}"
64
+ end
49
65
  end
50
66
  end
@@ -25,6 +25,8 @@ module Wf
25
25
 
26
26
  validates :name, presence: true
27
27
 
28
+ scope :valid, -> { where(is_valid: true) }
29
+
28
30
  after_save do
29
31
  do_validate!
30
32
  end
@@ -64,10 +66,11 @@ module Wf
64
66
  end
65
67
  end
66
68
 
67
- def to_graph(wf_case = nil)
69
+ def to_graph(wf_case = nil, base = nil)
68
70
  fontfamily = "system, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, Segoe UI Symbol"
69
71
  fontfamily_monospace = "SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace"
70
- graph = GraphViz.new(name, type: :digraph, rankdir: "LR", splines: true, ratio: :auto)
72
+ graph = base || GraphViz.new(name, type: :digraph, rankdir: "LR", splines: true, ratio: :auto)
73
+
71
74
  free_token_places = if wf_case
72
75
  wf_case.tokens.free.map(&:place_id)
73
76
  else
@@ -99,7 +102,7 @@ module Wf
99
102
  xlabel = nil
100
103
  end
101
104
 
102
- pg = graph.add_nodes(p.name,
105
+ pg = graph.add_nodes(p.graph_id,
103
106
  label: label,
104
107
  xlabel: xlabel,
105
108
  shape: shape,
@@ -116,10 +119,20 @@ module Wf
116
119
 
117
120
  tg_mapping = {}
118
121
  transitions.each do |t|
119
- tg = graph.add_nodes(t.name, label: t.name, shape: :box, style: :filled, fillcolor: "#d6ddfa", color: "#d6ddfa",
120
- fontcolor: "#2c50ed", fontname: fontfamily,
121
- href: Wf::Engine.routes.url_helpers.edit_workflow_transition_path(self, t))
122
+ tg = graph.add_nodes(t.graph_id, label: t.name, shape: :box, style: :filled, fillcolor: "#d6ddfa", color: "#d6ddfa",
123
+ fontcolor: "#2c50ed", fontname: fontfamily,
124
+ href: Wf::Engine.routes.url_helpers.edit_workflow_transition_path(self, t))
122
125
  tg_mapping[t] = tg
126
+ # NOTICE: if sub_workflow is transition's workflow, then graph will loop infinite, this is valid for workflow definition.
127
+ next unless t.is_sub_workflow? && t.sub_workflow != t.workflow
128
+
129
+ sub_graph = graph.add_graph("cluster#{t.sub_workflow_id}", rankdir: "LR", splines: true, ratio: :auto)
130
+ sub_graph[:label] = t.sub_workflow.name
131
+ sub_graph[:style] = :dashed
132
+ sub_graph[:color] = :lightgrey
133
+ # TODO: detect related case for sub workflow.
134
+ t.sub_workflow.to_graph(nil, sub_graph)
135
+ graph.add_edges(tg, t.sub_workflow.places.start.first.graph_id, style: :dashed, dir: :both)
123
136
  end
124
137
 
125
138
  arcs.order("direction desc").each do |arc|
@@ -31,6 +31,7 @@ module Wf
31
31
  has_many :parties, through: :workitem_assignments, source: "party"
32
32
  has_many :comments
33
33
  has_many :entries, class_name: Wf.entry_class.to_s
34
+ has_one :started_case, foreign_key: :started_by_workitem_id, class_name: "Wf::Case"
34
35
 
35
36
  enum state: {
36
37
  enabled: 0,
@@ -41,6 +41,11 @@
41
41
  <%= f.select :trigger_type, Wf::Transition.trigger_types.keys, {}, class: "form-control custom-select", placeholder: "Trigger Type" %>
42
42
  </div>
43
43
 
44
+ <div class="form-group">
45
+ <%= f.label :sub_workflow, class: "label" %>
46
+ <%= f.select :sub_workflow_id, options_for_select(Wf::Workflow.valid.all.map{|x| [x.name, x.id]} || []), {include_blank: 'Start a sub workflow?'}, class: "form-control custom-select", placeholder: "Sub Workflow" %>
47
+ </div>
48
+
44
49
  <div class="form-group">
45
50
  <%= f.label :form, class: "label" %>
46
51
  <%= f.select :form, options_for_select(Wf.form_class.constantize.all.map{|x| [x.name, x.to_global_id]} || [], selected: f.object&.form&.to_global_id), {include_blank: 'user can input from custom form?'}, class: "form-control custom-select", placeholder: "Form" %>
@@ -118,6 +118,7 @@
118
118
  <th scope="col">Trigger Type</th>
119
119
  <th scope="col">Sort Order</th>
120
120
  <th scope="col">Custom Form</th>
121
+ <th scope="col">Sub Workflow</th>
121
122
  <th scope='col'> </th>
122
123
  </tr>
123
124
  </thead>
@@ -138,6 +139,13 @@
138
139
  No Form
139
140
  <% end %>
140
141
  </td>
142
+ <td>
143
+ <% if transition.sub_workflow %>
144
+ <%= link_to transition.sub_workflow.name, workflow_path(transition.sub_workflow_id) %>
145
+ <% else %>
146
+ No
147
+ <% end %>
148
+ </td>
141
149
  <td><%= link_to 'Edit Transition', edit_workflow_transition_path(@workflow, transition), class: 'btn btn-sm btn-link mr-2' %>
142
150
  <%= link_to 'Delete Transition', [@workflow, transition], remote: true, method: :delete, data: {confirm: 'confirm?'}, class: 'btn btn-sm btn-link text-danger' %></td>
143
151
  </tr>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveUnusedColumn < ActiveRecord::Migration[6.0]
4
+ def change
5
+ remove_column :wf_guards, :target_attr_name
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddSubWorkflow < ActiveRecord::Migration[6.0]
4
+ def change
5
+ add_column :wf_transitions, :sub_workflow_id, :bigint, index: true
6
+ add_column :wf_cases, :started_by_workitem_id, :bigint, index: true, comment: "As a sub workflow instance, it is started by one workitem."
7
+ end
8
+ end
data/lib/wf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wf
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: petri_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hooopo Wang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-18 00:00:00.000000000 Z
11
+ date: 2020-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bootstrap
@@ -351,6 +351,8 @@ files:
351
351
  - db/migrate/20200213085258_add_formable.rb
352
352
  - db/migrate/20200213125753_add_form_id_for_entry.rb
353
353
  - db/migrate/20200213130900_remove_workflow_id_from_form_related.rb
354
+ - db/migrate/20200220070839_remove_unused_column.rb
355
+ - db/migrate/20200220072512_add_sub_workflow.rb
354
356
  - lib/tasks/wf_tasks.rake
355
357
  - lib/wf.rb
356
358
  - lib/wf/engine.rb