datashift_journey 0.1.2

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 (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