datashift_journey 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +391 -0
  4. data/Rakefile +26 -0
  5. data/app/assets/stylesheets/datashift_journey/partials/_state_jumper_toolbar.scss.erb +27 -0
  6. data/app/controllers/concerns/datashift_journey/error_renderer.rb +9 -0
  7. data/app/controllers/concerns/datashift_journey/review_renderer.rb +21 -0
  8. data/app/controllers/concerns/datashift_journey/token_based_access.rb +23 -0
  9. data/app/controllers/concerns/datashift_journey/validate_state.rb +59 -0
  10. data/app/controllers/datashift_journey/abandon_enrollments_controller.rb +14 -0
  11. data/app/controllers/datashift_journey/abandonments_controller.rb +17 -0
  12. data/app/controllers/datashift_journey/api/v1/states_controller.rb +49 -0
  13. data/app/controllers/datashift_journey/application_controller.rb +53 -0
  14. data/app/controllers/datashift_journey/errors_controller.rb +24 -0
  15. data/app/controllers/datashift_journey/journey_ends_controller.rb +19 -0
  16. data/app/controllers/datashift_journey/journey_plans_controller.rb +166 -0
  17. data/app/controllers/datashift_journey/page_states_controller.rb +49 -0
  18. data/app/controllers/datashift_journey/reviews_controller.rb +32 -0
  19. data/app/controllers/datashift_journey/state_jumper_controller.rb +51 -0
  20. data/app/factories/datashift_journey/form_object_factory.rb +68 -0
  21. data/app/forms/datashift_journey/collector/base_collector_form.rb +60 -0
  22. data/app/forms/datashift_journey/concerns/form_mixin.rb +64 -0
  23. data/app/forms/datashift_journey/null_form.rb +20 -0
  24. data/app/helpers/datashift_journey/application_helper.rb +50 -0
  25. data/app/helpers/datashift_journey/back_link_helper.rb +9 -0
  26. data/app/models/datashift_journey/collector/data_node.rb +18 -0
  27. data/app/models/datashift_journey/collector/form_definition.rb +35 -0
  28. data/app/models/datashift_journey/collector/form_field.rb +61 -0
  29. data/app/models/datashift_journey/journey_review.rb +65 -0
  30. data/app/models/datashift_journey/review_data_section.rb +32 -0
  31. data/app/serializers/datashift_journey/collector/page_state_serializer.rb +9 -0
  32. data/app/serializers/state_machines/state/state_serializer.rb +5 -0
  33. data/app/views/datashift_journey/collector/_generic_form.html.erb +14 -0
  34. data/app/views/datashift_journey/errors/401.html.erb +13 -0
  35. data/app/views/datashift_journey/errors/403.html.erb +13 -0
  36. data/app/views/datashift_journey/errors/404.html.erb +13 -0
  37. data/app/views/datashift_journey/errors/422.html.erb +11 -0
  38. data/app/views/datashift_journey/errors/500.html.erb +13 -0
  39. data/app/views/datashift_journey/errors/503.html.erb +11 -0
  40. data/app/views/datashift_journey/errors/invalid_authenticity_token.html.erb +14 -0
  41. data/app/views/datashift_journey/journey_ends/new.html.erb +5 -0
  42. data/app/views/datashift_journey/journey_ends/show.html.erb +5 -0
  43. data/app/views/datashift_journey/journey_plans/_form.html.erb +14 -0
  44. data/app/views/datashift_journey/journey_plans/_render_fields.html.erb +24 -0
  45. data/app/views/datashift_journey/journey_plans/edit.html.erb +6 -0
  46. data/app/views/datashift_journey/journey_plans/new.html.erb +6 -0
  47. data/app/views/datashift_journey/shared/_default_actions.html.erb +6 -0
  48. data/app/views/datashift_journey/shared/_errors.html.erb +19 -0
  49. data/app/views/datashift_journey/shared/_submit_action.html.erb +5 -0
  50. data/app/views/datashift_journey/state_jumper/_toolbar.html.erb +16 -0
  51. data/config/brakeman.ignore +42 -0
  52. data/config/i18n-tasks.yml +103 -0
  53. data/config/initializers/exceptions_app.rb +3 -0
  54. data/config/initializers/mime_types.rb +1 -0
  55. data/config/initializers/rswag-api.rb +14 -0
  56. data/config/initializers/rswag-ui.rb +9 -0
  57. data/config/locales/en.yml +39 -0
  58. data/config/routes.rb +44 -0
  59. data/lib/datashift_journey/collector/field_snippet.rb +12 -0
  60. data/lib/datashift_journey/collector/page_state_snippet.rb +12 -0
  61. data/lib/datashift_journey/collector/snippet.rb +14 -0
  62. data/lib/datashift_journey/configuration.rb +103 -0
  63. data/lib/datashift_journey/engine.rb +38 -0
  64. data/lib/datashift_journey/exceptions.rb +26 -0
  65. data/lib/datashift_journey/helpers/back_link.rb +58 -0
  66. data/lib/datashift_journey/journey/machine_builder.rb +59 -0
  67. data/lib/datashift_journey/prepare_data_for_review.rb +219 -0
  68. data/lib/datashift_journey/reference_generator.rb +51 -0
  69. data/lib/datashift_journey/state_machines/branch_sequence_map.rb +35 -0
  70. data/lib/datashift_journey/state_machines/extensions.rb +40 -0
  71. data/lib/datashift_journey/state_machines/planner.rb +206 -0
  72. data/lib/datashift_journey/state_machines/sequence.rb +86 -0
  73. data/lib/datashift_journey/state_machines/state_machine_core_ext.rb +72 -0
  74. data/lib/datashift_journey/version.rb +3 -0
  75. data/lib/datashift_journey.rb +57 -0
  76. data/lib/generators/datashift_journey/collector/collector_generator.rb +43 -0
  77. data/lib/generators/datashift_journey/collector/install_collector_generator.rb +61 -0
  78. data/lib/generators/datashift_journey/collector/install_mongo_collector_generator.rb +44 -0
  79. data/lib/generators/datashift_journey/collector/templates/collector_concern.rb.tt +34 -0
  80. data/lib/generators/datashift_journey/collector/templates/collector_migration.rb.tt +46 -0
  81. data/lib/generators/datashift_journey/forms_generator.rb +45 -0
  82. data/lib/generators/datashift_journey/generate_common.rb +33 -0
  83. data/lib/generators/datashift_journey/setup/USAGE +12 -0
  84. data/lib/generators/datashift_journey/setup/setup_generator.rb +44 -0
  85. data/lib/generators/datashift_journey/setup/templates/initializer.rb.tt +17 -0
  86. data/lib/generators/datashift_journey/setup/templates/model_concern.rb.tt +37 -0
  87. data/lib/generators/datashift_journey/templates/base_form.rb.tt +7 -0
  88. data/lib/generators/datashift_journey/templates/collector_form.rb.tt +18 -0
  89. data/lib/generators/datashift_journey/templates/collector_view.rb.tt +15 -0
  90. data/lib/generators/datashift_journey/templates/journey_plan_form.rb.tt +23 -0
  91. data/lib/generators/datashift_journey/templates/journey_plan_view.rb.tt +15 -0
  92. data/lib/generators/datashift_journey/views_generator.rb +35 -0
  93. data/lib/tasks/state_machine.thor +48 -0
  94. data/spec/datashift_journey/complex_journey_spec.rb +132 -0
  95. data/spec/datashift_journey/machine_builder_spec.rb +268 -0
  96. data/spec/datashift_journey/planner_spec.rb +129 -0
  97. data/spec/datashift_journey/sequence_spec.rb +20 -0
  98. data/spec/dummy/Rakefile +6 -0
  99. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  100. data/spec/dummy/app/assets/stylesheets/application.css +16 -0
  101. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  102. data/spec/dummy/app/forms/base_form.rb +8 -0
  103. data/spec/dummy/app/forms/business_details_form.rb +17 -0
  104. data/spec/dummy/app/forms/business_type_form.rb +17 -0
  105. data/spec/dummy/app/forms/contact_details_form.rb +17 -0
  106. data/spec/dummy/app/forms/enter_reg_number_form.rb +17 -0
  107. data/spec/dummy/app/forms/new_or_renew_form.rb +17 -0
  108. data/spec/dummy/app/forms/postal_address_form.rb +17 -0
  109. data/spec/dummy/app/forms/question1_form.rb +9 -0
  110. data/spec/dummy/app/forms/question2_form.rb +12 -0
  111. data/spec/dummy/app/forms/sole_trader_name_form.rb +17 -0
  112. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  113. data/spec/dummy/app/models/payment.rb +5 -0
  114. data/spec/dummy/app/services/datashift_journey/models/collector_journey.rb +51 -0
  115. data/spec/dummy/app/views/_question1.html.erb +13 -0
  116. data/spec/dummy/app/views/_question2.html.erb +9 -0
  117. data/spec/dummy/app/views/layouts/alternative.html.erb +14 -0
  118. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  119. data/spec/dummy/app/views/pages/home.html.erb +6 -0
  120. data/spec/dummy/app/views/pages/start.html.erb +5 -0
  121. data/spec/dummy/bin/bundle +3 -0
  122. data/spec/dummy/bin/rails +4 -0
  123. data/spec/dummy/bin/rake +4 -0
  124. data/spec/dummy/bin/setup +29 -0
  125. data/spec/dummy/config/application.rb +39 -0
  126. data/spec/dummy/config/boot.rb +5 -0
  127. data/spec/dummy/config/database.yml +22 -0
  128. data/spec/dummy/config/environment.rb +5 -0
  129. data/spec/dummy/config/environments/development.rb +47 -0
  130. data/spec/dummy/config/environments/production.rb +79 -0
  131. data/spec/dummy/config/environments/test.rb +47 -0
  132. data/spec/dummy/config/initializers/assets.rb +11 -0
  133. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  134. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  135. data/spec/dummy/config/initializers/datashift_journey.rb +6 -0
  136. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  137. data/spec/dummy/config/initializers/inflections.rb +16 -0
  138. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  139. data/spec/dummy/config/initializers/session_store.rb +3 -0
  140. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  141. data/spec/dummy/config/locales/en.yml +27 -0
  142. data/spec/dummy/config/routes.rb +28 -0
  143. data/spec/dummy/config/secrets.yml +22 -0
  144. data/spec/dummy/config.ru +4 -0
  145. data/spec/dummy/db/migrate/20160101091218_create_dummy_checkout.rb +43 -0
  146. data/spec/dummy/db/migrate/20161221100703_datashift_journey_create_collector.rb +56 -0
  147. data/spec/dummy/db/schema.rb +142 -0
  148. data/spec/dummy/lib/version.rb +1 -0
  149. data/spec/factories/collector_factory.rb +16 -0
  150. data/spec/factories/collector_snippet_factory.rb +9 -0
  151. data/spec/factories/collector_state_page_factory.rb +12 -0
  152. data/spec/factories/data_node_factory.rb +9 -0
  153. data/spec/factories/form_factory.rb +6 -0
  154. data/spec/features/basic_navigation_spec.rb +125 -0
  155. data/spec/helpers/application_helper_spec.rb +40 -0
  156. data/spec/integration/collector/page_state_spec.rb +45 -0
  157. data/spec/models/collector/collector_spec.rb +100 -0
  158. data/spec/models/collector/page_state_spec.rb +30 -0
  159. data/spec/rails_helper.rb +73 -0
  160. data/spec/requests/collector/api/v1/page_state_spec.rb +85 -0
  161. data/spec/requests/collector/api/v1/states_spec.rb +28 -0
  162. data/spec/spec_helper.rb +63 -0
  163. data/spec/support/asserts.rb +27 -0
  164. data/spec/support/mailer_macros.rb +25 -0
  165. data/spec/support/page_objects/base_page_object.rb +77 -0
  166. data/spec/swagger_helper.rb +25 -0
  167. metadata +425 -0
@@ -0,0 +1,35 @@
1
+ require 'forwardable'
2
+
3
+ module DatashiftJourney
4
+
5
+ module StateMachines
6
+
7
+ # Map a Sequence to its ID, created in form
8
+ #
9
+ # branch_sequence :other_sequence, [:other_business]
10
+ #
11
+ # BranchSequenceMap[:branch_sequence] => Sequence([:other_business])
12
+ #
13
+ class BranchSequenceMap < ActiveSupport::HashWithIndifferentAccess
14
+
15
+ # Create a new Sequence if ID not yet in Map, otherwise
16
+ # add the state list to the existing Sequence
17
+
18
+ def add_or_concat(id, list)
19
+ key?(id) ? self[id].add_states(list) : add_branch(id, Sequence.new(list.flatten, id: id))
20
+ end
21
+
22
+ def add_branch(id, sequence)
23
+ # puts "DEBUG: ADDING TO SEQ [#{id}] BRANCH #{sequence.inspect}"
24
+ self[id] = sequence
25
+ end
26
+
27
+ # Find the matching branch sequences for a parent Split (first state)
28
+ def branches_for(sequence)
29
+ values.find_all { |branch| (branch.entry_state && branch.entry_state == sequence.split_entry_state) }
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ # Decorate a state machine enabled class with a set of extensions to :
2
+ #
3
+ # https://github.com/state-machines/state_machines
4
+ # https://github.com/state-machines/state_machines-activerecord
5
+ #
6
+ module DatashiftJourney
7
+
8
+ module StateMachines
9
+
10
+ module Extensions
11
+
12
+ def transitions_for
13
+ self.class.state_machine.events.transitions_for(self)
14
+ end
15
+
16
+ def pp_state_paths
17
+ state_paths.each_with_index { |s, i| puts "Event [#{s.events[i]}] from=#{s[i].from} to=#{s[i].to}" }
18
+ end
19
+
20
+ # Expects a symbol
21
+ # Returns nil when no such state
22
+ def state_index(state)
23
+ state.nil? ? nil : state_paths.to_states.index(state.to_sym).to_i
24
+ end
25
+
26
+ def current_state_index
27
+ state_paths.to_states.index(state_name).to_i
28
+ end
29
+
30
+ def next_state_name
31
+ transitions_for.find { |t| t.event == :skip_fwd }.try(:to_name)
32
+ end
33
+
34
+ def previous_state_name
35
+ transitions_for.find { |t| t.event == :back }.try(:to_name)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,206 @@
1
+ require 'forwardable'
2
+ require_relative 'sequence'
3
+
4
+ module DatashiftJourney
5
+
6
+ module StateMachines
7
+
8
+ # Mixed into the State Machine class, so your JourneyPlan class has access to these methods and
9
+ # attributes
10
+ #
11
+ module Planner
12
+
13
+ def init_plan
14
+ sequence_list.clear # In development context where models get reloaded, this could duplicate
15
+ branch_sequence_map.clear
16
+ end
17
+
18
+ # The complete, Ordered collection of sequences, used to generate the steps (states) of the plan
19
+ def sequence_list
20
+ @sequence_list ||= StateList.new
21
+ end
22
+
23
+ # Key - sequence ID
24
+ def branch_sequence_map
25
+ @branch_sequence_map ||= BranchSequenceMap.new
26
+ end
27
+
28
+ def sequence(*list)
29
+ raise PlannerApiError, 'Empty list passed to sequence - check your MachineBuilder syntax' if list.empty?
30
+ sequence_list << Sequence.new(list.flatten)
31
+ end
32
+
33
+ # Path splits down different branches, based on values stored on the main Journey model
34
+ # usually collected from from input e.g. radio, text or checkbox
35
+ #
36
+ # Requires the starting or parent state, and the routing criteria to each target sequence
37
+ #
38
+ # split_on_equality( :new_or_renew,
39
+ # "what_branch?", # Helper method on the journey Class
40
+ # branch_1: 'branch_1',
41
+ # branch_2: 'branch_2'
42
+ # )
43
+ #
44
+ # target_on_value_map is a hash mapping between the value collected from website and the associated named branch.
45
+ #
46
+ # if value collected on parent state == stored target state, journey is routed down that branch
47
+ #
48
+ def split_on_equality(state, attr_reader, seq_to_target_value_map, _options = {})
49
+ unless seq_to_target_value_map.is_a? Hash
50
+ raise 'BadDefinition - target_on_value_map must be hash map value => associated branch state'
51
+ end
52
+
53
+ sequence_list << Sequence.new(state, split: true)
54
+
55
+ seq_to_target_value_map.each do |seq_id, trigger_value|
56
+ if branch_sequence_map[seq_id]
57
+ branch_sequence_map[seq_id].entry_state = state
58
+ branch_sequence_map[seq_id].trigger_method = attr_reader
59
+ branch_sequence_map[seq_id].trigger_value = trigger_value
60
+ else
61
+ seq = Sequence.new(nil, id: seq_id, entry_state: state, trigger_method: attr_reader, trigger_value: trigger_value)
62
+ branch_sequence_map.add_branch(seq_id, seq)
63
+ end
64
+ end
65
+ end
66
+
67
+ def branch_sequence(sequence_id, *list)
68
+ branch_sequence_map.add_or_concat(sequence_id, list)
69
+ end
70
+
71
+ def self.hash_klass
72
+ ActiveSupport::HashWithIndifferentAccess
73
+ end
74
+
75
+ protected
76
+
77
+ # Based upon the current sequences, events defined build the Complete Plan,
78
+ # including back and next navigation
79
+ #
80
+ def build_journey_plan
81
+ # The Order of sequences should have been preserved as insertion order
82
+
83
+ #puts "DEBUG: START PLAN - Processing SEQUENCES\n#{sequence_list.inspect}"
84
+
85
+ sequence_list.each_with_index do |sequence, i|
86
+ prev_seq = i.zero? ? EmptySequence.new : sequence_list[i - 1]
87
+
88
+ next_seq = sequence_list[i + 1] || EmptySequence.new
89
+
90
+ if sequence.split?
91
+ #puts "\nDEBUG: *** BUILDING SPLITTER #{sequence.inspect} (#{i})"
92
+ build_split_sequence_events(sequence, prev_seq, next_seq)
93
+ else
94
+
95
+ # If previous seq is a branch we need to build conditional back transitions, to the end state
96
+ # of each branch (based on the same criteria that originally split the branch)
97
+ if prev_seq.split?
98
+ begin
99
+ #puts "\nDEBUG: *** BUILDING SEQ TO SPLIT #{sequence.inspect} (#{i})"
100
+ build_triggered_back(sequence, prev_seq)
101
+ rescue => x
102
+ puts x.inspect
103
+ puts "Failed in Seq [#{sequence.inspect}] (#{i}) - to create back events to Previous Seq #{prev_seq}"
104
+ raise x
105
+ end
106
+
107
+ elsif prev_seq.last
108
+ #puts "\nDEBUG: *** BUILDING SEQ #{sequence.inspect} (#{i})"
109
+ create_back(sequence.first, prev_seq.last)
110
+ end
111
+
112
+ # The simple navigation through states within the sequence
113
+ create_pairs sequence
114
+
115
+ #puts "\nDEBUG: *** CREATED PAIRS FOR SEQ #{sequence.inspect} (#{i})"
116
+ create_next(sequence.last, next_seq.first) if next_seq.first.present?
117
+ end
118
+ end
119
+ end
120
+
121
+ def build_split_sequence_events(sequence, prev_seq, next_seq)
122
+ # puts "\n\nDEBUG: PROCESS SPLIT SEQ #{sequence.inspect}"
123
+ # puts "DEBUG: SPLIT prev_seq #{prev_seq.inspect}"
124
+ # puts "DEBUG: SPLIT next_seq #{next_seq.inspect}"
125
+
126
+ # Create BACK from this entry state to the exit point of any PREVIOUS sequence
127
+ create_back(sequence.split_entry_state, prev_seq.last) if prev_seq.last
128
+
129
+ branch_sequence_map.branches_for(sequence).each do |branch|
130
+ begin
131
+ # puts "\n\nDEBUG: Process Branch - #{branch.inspect}"
132
+
133
+ # Back and next for any states within the split sequence itself
134
+ create_pairs branch
135
+
136
+ # N.B A split sequence can actually be empty
137
+ #
138
+ # i.e Some branches may jump straight from the split point straight to next common sequence
139
+
140
+ # Now work out the start and end points for this split.
141
+ next_state = branch.empty? ? next_seq.first : branch.first
142
+
143
+ # back from first seq state (or if empty next sequence) to this decision state
144
+ split_entry_state = sequence.split_entry_state
145
+
146
+ # If branch has no states, a VALUE triggered BACK will be created later
147
+ create_back(branch.first, split_entry_state) unless branch.empty?
148
+
149
+ build_triggered_next(branch, split_entry_state, next_state)
150
+
151
+ # N.B When multiple splits occur one after the other, branch.last can equal next_seq.first
152
+
153
+ # Not sure if that's reflective that logic not too clever elsewhere but for now
154
+ # make sure we don't create such a next event to itself
155
+
156
+ # LAST item in branch connects to FIRST item of NEXT sequence (unless empty and already built with trigger)
157
+
158
+ if !branch.empty? && next_seq.first && (branch.last != next_seq.first)
159
+ create_next(branch.last, next_seq.first)
160
+ end
161
+
162
+ rescue => x
163
+ puts x.inspect
164
+ puts "Failed in Split Sequnce to process Branch #{branch.inspect}"
165
+ raise x
166
+ end
167
+ end
168
+ end
169
+
170
+ def build_triggered_back(sequence, prev_seq)
171
+ #puts "DEBUG: * BUILD triggered Back for #{sequence.inspect}"
172
+
173
+ # Create back from FIRST item of THIS sequence to LAST entry of EACH previous BRANCH
174
+ branch_sequence_map.branches_for(prev_seq).each do |branch|
175
+ # Branches can be empty - i.e chain direct to next common sequence
176
+ # in which case back goes to the split sequence state itself (parent of branch)
177
+ to_state = branch.last.nil? ? prev_seq.first : branch.last
178
+
179
+ create_back(sequence.first, to_state) do
180
+ lambda do |o|
181
+ unless o && o.respond_to?(branch.trigger_method)
182
+ raise PlannerBlockError, "Cannot Go back - No such method #{branch.trigger_method} on Class #{o.class}"
183
+ end
184
+ o.send(branch.trigger_method) == branch.trigger_value
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def build_triggered_next(branch, from, to)
191
+ # N.B sequences can self terminate i.e no further sequences and end of the journey
192
+ return unless from && from != to
193
+
194
+ create_next(from, to) do
195
+ lambda do |o|
196
+ unless o && o.respond_to?(branch.trigger_method)
197
+ raise PlannerBlockError, "Cannot split - No such method #{branch.trigger_method} on Class #{o.class}"
198
+ end
199
+ o.send(branch.trigger_method) == branch.trigger_value
200
+ end
201
+ end
202
+ end
203
+
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,86 @@
1
+ require 'forwardable'
2
+
3
+ module DatashiftJourney
4
+
5
+ module StateMachines
6
+
7
+ class StateList
8
+ extend Forwardable
9
+
10
+ def_delegators :@states, :each, :clear, :each_with_index, :[], :<<, :<=>, :<<, :==, :[], :[]=
11
+
12
+ def initialize
13
+ @states = []
14
+ end
15
+
16
+ end
17
+
18
+ class Sequence
19
+ extend Forwardable
20
+
21
+ attr_reader :id
22
+
23
+ attr_reader :entry_state, :exit_state, :states
24
+
25
+ attr_accessor :split, :trigger_method, :trigger_value
26
+
27
+ def_delegators :@states,
28
+ :clear, :drop, :each, :each_with_index,
29
+ :empty?, :size,
30
+ :first, :last,
31
+ :[], :<<, :<=>, :<<, :==, :[], :[]=
32
+
33
+ # rubocop:disable Metrics/ParameterLists
34
+ def initialize(states, id: '', entry_state: nil, exit_state: nil, trigger_value: nil, trigger_method: nil, split: false)
35
+ @states = [*states]
36
+
37
+ @id = id
38
+ @entry_state = entry_state
39
+ @exit_state = exit_state
40
+ @trigger_method = trigger_method
41
+ @trigger_value = trigger_value
42
+ @split = split
43
+ end
44
+
45
+ def add_states(list)
46
+ @states.concat(list.flatten)
47
+ end
48
+
49
+ def inspect
50
+ "#{self.class.name}(#{id}) - #{@states.inspect} [splitter = #{split?}]"
51
+ end
52
+
53
+ def split?
54
+ split == true
55
+ end
56
+
57
+ def branch?
58
+ !trigger_value.nil?
59
+ end
60
+
61
+ def split_entry_state
62
+ return nil unless split?
63
+ states.first
64
+ end
65
+
66
+ def entry_state=(state)
67
+ @entry_state = state unless entry_state
68
+ end
69
+
70
+ def exit_state=(state)
71
+ @exit_state = state unless exit_state
72
+ end
73
+
74
+ private
75
+
76
+ attr_writer :states
77
+ end
78
+
79
+ class EmptySequence < Sequence
80
+ def initialize
81
+ super(nil)
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,72 @@
1
+ require_relative 'planner'
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+
5
+ StateMachines::Machine.class_eval do
6
+ include DatashiftJourney::StateMachines::Planner
7
+ extend DatashiftJourney::StateMachines::Planner
8
+
9
+ # Create both a next link from lhs to rhs, and a back link from rhs to lhs
10
+
11
+ def create_pair(lhs, rhs)
12
+ create_back(lhs, rhs)
13
+ create_next(rhs, lhs)
14
+ end
15
+
16
+ def create_pairs(sequence)
17
+ create_back_transitions sequence
18
+ create_next_transitions sequence
19
+ end
20
+
21
+ def create_back(from, to)
22
+ raise "Bad transitions supplied for Back - FROM #{from} - TO #{to}" if from.nil? || to.nil?
23
+ if block_given?
24
+ #puts "DEBUG: Creating BACK transition from #{from} to #{to} with Block"
25
+ transition(from => to, on: :back, if: yield)
26
+ else
27
+ #puts "DEBUG: Creating BACK transition from #{from} to #{to}"
28
+ transition(from => to, on: :back)
29
+ end
30
+ end
31
+
32
+ # We use skip_fwd as the event type to avoid keyword next
33
+ #
34
+ # This will add usual helpers like
35
+ #
36
+ # vehicle.skip_fwd? # => true
37
+ # vehicle.can_skip_fwd? # => true
38
+ #
39
+ def create_next(from, to)
40
+ raise "Bad transitions supplied for Next - FROM #{from} - TO #{to}" if from.nil? || to.nil?
41
+ if block_given?
42
+ #puts "DEBUG: Creating NEXT transition from #{from} to #{to} with Block "
43
+ transition(from => to, on: :skip_fwd, if: yield)
44
+ else
45
+ #puts "DEBUG: Creating NEXT transition from #{from} to #{to}"
46
+ transition(from => to, on: :skip_fwd)
47
+ end
48
+ end
49
+
50
+ # BACK - Create a 'back' event for each step in list
51
+ # Automatically removes first state, as nothing to go back to from that state
52
+ # You can exclude any other steps with the except list
53
+ #
54
+ def create_back_transitions(journey, except = [])
55
+ journey.drop(1).each_with_index do |t, i|
56
+ next if except.include?(t)
57
+ create_back(t, journey[i]) # n.b previous index is actually i not (i-1) due to the drop
58
+ end
59
+ end
60
+
61
+ # NEXT - Create a 'next' event for each step (apart from last) in journey
62
+ # You can exclude any other steps with the except list
63
+ #
64
+ def create_next_transitions(journey, except = [])
65
+
66
+ #puts "DEBUG: Creating NEXT transitions for #{journey.inspect}"
67
+ journey[0...-1].each_with_index do |t, i|
68
+ next if except.include?(t)
69
+ create_next(t, journey[i + 1])
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module DatashiftJourney
2
+ VERSION = '0.1.2'
3
+ end
@@ -0,0 +1,57 @@
1
+ require 'rails'
2
+ require 'state_machines-activerecord'
3
+ require 'reform'
4
+ require 'reform/form'
5
+
6
+ require_relative 'datashift_journey/state_machines/planner'
7
+ require_relative 'datashift_journey/state_machines/extensions'
8
+ require_relative 'datashift_journey/state_machines/state_machine_core_ext'
9
+ require_relative 'datashift_journey/engine'
10
+
11
+ module DatashiftJourney
12
+
13
+ def self.library_path
14
+ File.expand_path("#{File.dirname(__FILE__)}/../lib")
15
+ end
16
+
17
+ # Load all the datashift Thor commands and make them available throughout app
18
+ #
19
+ def self.load_commands
20
+ base = File.join(library_path, 'tasks', '**')
21
+
22
+ Dir["#{base}/*.thor"].each do |f|
23
+ next unless File.file?(f)
24
+ load(f)
25
+ end
26
+ end
27
+
28
+ # Set the main model class that contains the plan and associated state engine
29
+ #
30
+ def self.journey_plan_class=(x)
31
+ raise 'DSJ - journey_plan_class MUST be String or Symbol, not a Class.' if x.is_a?(Class)
32
+
33
+ @journey_plan_class = x
34
+
35
+ class << self
36
+ define_method :"concern_file" do
37
+ "#{@journey_plan_class.underscore}_journey.rb"
38
+ end
39
+ end
40
+
41
+ # This is called from an initializer, we dont want to trigger the machine building till
42
+ # the model class itself is loaded so do NOT do this here
43
+ # @journey_plan_class = x.to_s.constantize if x.is_a?(String) || x.is_a?(Symbol)
44
+
45
+ @journey_plan_class
46
+ end
47
+
48
+ def self.journey_plan_class
49
+ @journey_plan_class = @journey_plan_class.to_s.constantize if @journey_plan_class.is_a?(String) || @journey_plan_class.is_a?(Symbol)
50
+ @journey_plan_class
51
+ end
52
+
53
+ def self.state_names(machine: :state)
54
+ DatashiftJourney.journey_plan_class.state_machine(machine).states.map(&:name)
55
+ end
56
+
57
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../generate_common'
2
+
3
+ module DatashiftJourney
4
+ class CollectorGenerator < Rails::Generators::Base
5
+
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ include Rails::Generators::Migration
9
+
10
+ include DatashiftJourney::GenerateCommon
11
+ extend DatashiftJourney::GenerateCommon
12
+
13
+ desc 'Copies over migrations enabling use of our generic data collection facilities'
14
+
15
+ def create_collector
16
+ @migration_version = '6.1' # TODO: how can we get this dynamically from Rails version ?
17
+
18
+ migration_template 'collector_migration.rb', 'db/migrate/datashift_journey_create_collector.rb'#, migration_version: migration_version
19
+
20
+ code = <<-EOS
21
+ has_many :data_nodes, class_name: 'DatashiftJourney::Collector::DataNode', as: :plan, foreign_key: :plan_id, dependent: :destroy
22
+ accepts_nested_attributes_for :data_nodes
23
+
24
+ EOS
25
+
26
+ inject_into_file model_path, :after => /class.* < ApplicationRecord/ do
27
+ "\n#{code}"
28
+ end
29
+
30
+ route(%(
31
+ # This mounts Datashift Journey's Collector routes
32
+ #
33
+ scope :api, constraints: { format: 'json' } do
34
+ scope :v1 do
35
+ resources :page_states, only: [:create], controller: 'datashift_journey/page_states'
36
+ end
37
+ end
38
+ )
39
+ )
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,61 @@
1
+ require 'rails/generators/active_record'
2
+ require_relative '../initializer_common'
3
+
4
+ module DatashiftJourney
5
+
6
+ class InstallCollectorGenerator < Rails::Generators::Base
7
+
8
+ include Rails::Generators::Migration
9
+
10
+ source_root File.expand_path('../templates', __FILE__)
11
+
12
+ desc 'This generator copies over DSJ migrations to use the generic Collector data collector'
13
+
14
+ def copy_collector_migration
15
+ migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb"
16
+ migration_template 'collector_migration.rb', 'db/migrate/datashift_journey_create_collector.rb', migration_version: migration_version
17
+ end
18
+
19
+ extend DatashiftJourney::InitializerCommon
20
+ include DatashiftJourney::InitializerCommon
21
+
22
+ # Hmm bit odd but to get thor to work appears we need to wrap calls to our common methods
23
+ def install_common
24
+ create_initializer_file(klass)
25
+
26
+ notify_about_routes
27
+
28
+ insert_into_file File.join('config', 'routes.rb'), before: "end\n" do
29
+ %(
30
+ # This line mounts Datashift Journey's Collector routes
31
+ #
32
+ scope :api, constraints: { format: 'json' } do
33
+ scope :v1 do
34
+ resources :page_states, only: [:create], controller: 'datashift_journey/page_states'
35
+ end
36
+ end
37
+ )
38
+ end
39
+
40
+ journey_plan_host_file(klass)
41
+
42
+ model_journey_code(klass)
43
+ end
44
+
45
+ private
46
+
47
+ def rails5?
48
+ Rails.version.start_with? '5'
49
+ end
50
+
51
+ def klass
52
+ 'DatashiftJourney::Collector::Collector'
53
+ end
54
+
55
+ def migration_version
56
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if rails5?
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../initializer_common'
2
+
3
+ module DatashiftJourney
4
+
5
+ class InstallMongoCollectorGenerator < Rails::Generators::Base
6
+
7
+ desc 'This generator copies over DSJ migrations to use the generic Collector data collector'
8
+
9
+ def install_migrations
10
+ say_status :copying, 'migrations'
11
+ `rake railties:install:migrations`
12
+ end
13
+
14
+ extend DatashiftJourney::InitializerCommon
15
+ include DatashiftJourney::InitializerCommon
16
+
17
+ # Hmm bit odd but to get thor to work appears we need to wrap calls to our common methods
18
+ def install_common
19
+ create_initializer_file(klass)
20
+
21
+ notify_about_routes
22
+
23
+ journey_decorator(klass)
24
+
25
+ model_journey_code(klass)
26
+ end
27
+
28
+ def migration_data
29
+ <<RUBY
30
+ field :form, type: String
31
+ field :field , type: String
32
+ field :value, type: String
33
+ RUBY
34
+ end
35
+
36
+ private
37
+
38
+ def klass
39
+ 'DatashiftJourney::MongoCollector::MongoCollector'
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,34 @@
1
+ module DatashiftJourney
2
+ module Collector
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include DatashiftJourney::ReferenceGenerator.new(prefix: 'C')
8
+
9
+ # See app/models/datashift_journey/collector/data_node.rb
10
+ has_many :data_nodes, class_name: 'DatashiftJourney::Collector::DataNode', foreign_key: :plan_id, dependent: :destroy, as: :plan
11
+
12
+ has_many :form_fields, through: :data_nodes, source: :form_field
13
+
14
+ has_many :page_states, through: :form_fields
15
+ end
16
+
17
+ def node_for_form_and_field(form_name, field_name)
18
+ form_field = DatashiftJourney::Collector::FormBackingModel.for_form_and_field(form_name, field_name)
19
+ return nil unless form_field
20
+ data_nodes.where(form_field: form_field).first
21
+ end
22
+
23
+ def node_for_form_field(form_field)
24
+ data_nodes.find(form_field).first
25
+ end
26
+
27
+ def nodes_for_form(form_name)
28
+ form = page_states.where(form_name: form_name).first
29
+ return [] unless form
30
+ form.data_nodes.all.to_a
31
+ end
32
+
33
+ end
34
+ end