datashift_journey 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +391 -0
- data/Rakefile +26 -0
- data/app/assets/stylesheets/datashift_journey/partials/_state_jumper_toolbar.scss.erb +27 -0
- data/app/controllers/concerns/datashift_journey/error_renderer.rb +9 -0
- data/app/controllers/concerns/datashift_journey/review_renderer.rb +21 -0
- data/app/controllers/concerns/datashift_journey/token_based_access.rb +23 -0
- data/app/controllers/concerns/datashift_journey/validate_state.rb +59 -0
- data/app/controllers/datashift_journey/abandon_enrollments_controller.rb +14 -0
- data/app/controllers/datashift_journey/abandonments_controller.rb +17 -0
- data/app/controllers/datashift_journey/api/v1/states_controller.rb +49 -0
- data/app/controllers/datashift_journey/application_controller.rb +53 -0
- data/app/controllers/datashift_journey/errors_controller.rb +24 -0
- data/app/controllers/datashift_journey/journey_ends_controller.rb +19 -0
- data/app/controllers/datashift_journey/journey_plans_controller.rb +166 -0
- data/app/controllers/datashift_journey/page_states_controller.rb +49 -0
- data/app/controllers/datashift_journey/reviews_controller.rb +32 -0
- data/app/controllers/datashift_journey/state_jumper_controller.rb +51 -0
- data/app/factories/datashift_journey/form_object_factory.rb +68 -0
- data/app/forms/datashift_journey/collector/base_collector_form.rb +60 -0
- data/app/forms/datashift_journey/concerns/form_mixin.rb +64 -0
- data/app/forms/datashift_journey/null_form.rb +20 -0
- data/app/helpers/datashift_journey/application_helper.rb +50 -0
- data/app/helpers/datashift_journey/back_link_helper.rb +9 -0
- data/app/models/datashift_journey/collector/data_node.rb +18 -0
- data/app/models/datashift_journey/collector/form_definition.rb +35 -0
- data/app/models/datashift_journey/collector/form_field.rb +61 -0
- data/app/models/datashift_journey/journey_review.rb +65 -0
- data/app/models/datashift_journey/review_data_section.rb +32 -0
- data/app/serializers/datashift_journey/collector/page_state_serializer.rb +9 -0
- data/app/serializers/state_machines/state/state_serializer.rb +5 -0
- data/app/views/datashift_journey/collector/_generic_form.html.erb +14 -0
- data/app/views/datashift_journey/errors/401.html.erb +13 -0
- data/app/views/datashift_journey/errors/403.html.erb +13 -0
- data/app/views/datashift_journey/errors/404.html.erb +13 -0
- data/app/views/datashift_journey/errors/422.html.erb +11 -0
- data/app/views/datashift_journey/errors/500.html.erb +13 -0
- data/app/views/datashift_journey/errors/503.html.erb +11 -0
- data/app/views/datashift_journey/errors/invalid_authenticity_token.html.erb +14 -0
- data/app/views/datashift_journey/journey_ends/new.html.erb +5 -0
- data/app/views/datashift_journey/journey_ends/show.html.erb +5 -0
- data/app/views/datashift_journey/journey_plans/_form.html.erb +14 -0
- data/app/views/datashift_journey/journey_plans/_render_fields.html.erb +24 -0
- data/app/views/datashift_journey/journey_plans/edit.html.erb +6 -0
- data/app/views/datashift_journey/journey_plans/new.html.erb +6 -0
- data/app/views/datashift_journey/shared/_default_actions.html.erb +6 -0
- data/app/views/datashift_journey/shared/_errors.html.erb +19 -0
- data/app/views/datashift_journey/shared/_submit_action.html.erb +5 -0
- data/app/views/datashift_journey/state_jumper/_toolbar.html.erb +16 -0
- data/config/brakeman.ignore +42 -0
- data/config/i18n-tasks.yml +103 -0
- data/config/initializers/exceptions_app.rb +3 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/initializers/rswag-api.rb +14 -0
- data/config/initializers/rswag-ui.rb +9 -0
- data/config/locales/en.yml +39 -0
- data/config/routes.rb +44 -0
- data/lib/datashift_journey/collector/field_snippet.rb +12 -0
- data/lib/datashift_journey/collector/page_state_snippet.rb +12 -0
- data/lib/datashift_journey/collector/snippet.rb +14 -0
- data/lib/datashift_journey/configuration.rb +103 -0
- data/lib/datashift_journey/engine.rb +38 -0
- data/lib/datashift_journey/exceptions.rb +26 -0
- data/lib/datashift_journey/helpers/back_link.rb +58 -0
- data/lib/datashift_journey/journey/machine_builder.rb +59 -0
- data/lib/datashift_journey/prepare_data_for_review.rb +219 -0
- data/lib/datashift_journey/reference_generator.rb +51 -0
- data/lib/datashift_journey/state_machines/branch_sequence_map.rb +35 -0
- data/lib/datashift_journey/state_machines/extensions.rb +40 -0
- data/lib/datashift_journey/state_machines/planner.rb +206 -0
- data/lib/datashift_journey/state_machines/sequence.rb +86 -0
- data/lib/datashift_journey/state_machines/state_machine_core_ext.rb +72 -0
- data/lib/datashift_journey/version.rb +3 -0
- data/lib/datashift_journey.rb +57 -0
- data/lib/generators/datashift_journey/collector/collector_generator.rb +43 -0
- data/lib/generators/datashift_journey/collector/install_collector_generator.rb +61 -0
- data/lib/generators/datashift_journey/collector/install_mongo_collector_generator.rb +44 -0
- data/lib/generators/datashift_journey/collector/templates/collector_concern.rb.tt +34 -0
- data/lib/generators/datashift_journey/collector/templates/collector_migration.rb.tt +46 -0
- data/lib/generators/datashift_journey/forms_generator.rb +45 -0
- data/lib/generators/datashift_journey/generate_common.rb +33 -0
- data/lib/generators/datashift_journey/setup/USAGE +12 -0
- data/lib/generators/datashift_journey/setup/setup_generator.rb +44 -0
- data/lib/generators/datashift_journey/setup/templates/initializer.rb.tt +17 -0
- data/lib/generators/datashift_journey/setup/templates/model_concern.rb.tt +37 -0
- data/lib/generators/datashift_journey/templates/base_form.rb.tt +7 -0
- data/lib/generators/datashift_journey/templates/collector_form.rb.tt +18 -0
- data/lib/generators/datashift_journey/templates/collector_view.rb.tt +15 -0
- data/lib/generators/datashift_journey/templates/journey_plan_form.rb.tt +23 -0
- data/lib/generators/datashift_journey/templates/journey_plan_view.rb.tt +15 -0
- data/lib/generators/datashift_journey/views_generator.rb +35 -0
- data/lib/tasks/state_machine.thor +48 -0
- data/spec/datashift_journey/complex_journey_spec.rb +132 -0
- data/spec/datashift_journey/machine_builder_spec.rb +268 -0
- data/spec/datashift_journey/planner_spec.rb +129 -0
- data/spec/datashift_journey/sequence_spec.rb +20 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +16 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/forms/base_form.rb +8 -0
- data/spec/dummy/app/forms/business_details_form.rb +17 -0
- data/spec/dummy/app/forms/business_type_form.rb +17 -0
- data/spec/dummy/app/forms/contact_details_form.rb +17 -0
- data/spec/dummy/app/forms/enter_reg_number_form.rb +17 -0
- data/spec/dummy/app/forms/new_or_renew_form.rb +17 -0
- data/spec/dummy/app/forms/postal_address_form.rb +17 -0
- data/spec/dummy/app/forms/question1_form.rb +9 -0
- data/spec/dummy/app/forms/question2_form.rb +12 -0
- data/spec/dummy/app/forms/sole_trader_name_form.rb +17 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/payment.rb +5 -0
- data/spec/dummy/app/services/datashift_journey/models/collector_journey.rb +51 -0
- data/spec/dummy/app/views/_question1.html.erb +13 -0
- data/spec/dummy/app/views/_question2.html.erb +9 -0
- data/spec/dummy/app/views/layouts/alternative.html.erb +14 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/pages/home.html.erb +6 -0
- data/spec/dummy/app/views/pages/start.html.erb +5 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config/application.rb +39 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +22 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +47 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +47 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/datashift_journey.rb +6 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +27 -0
- data/spec/dummy/config/routes.rb +28 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20160101091218_create_dummy_checkout.rb +43 -0
- data/spec/dummy/db/migrate/20161221100703_datashift_journey_create_collector.rb +56 -0
- data/spec/dummy/db/schema.rb +142 -0
- data/spec/dummy/lib/version.rb +1 -0
- data/spec/factories/collector_factory.rb +16 -0
- data/spec/factories/collector_snippet_factory.rb +9 -0
- data/spec/factories/collector_state_page_factory.rb +12 -0
- data/spec/factories/data_node_factory.rb +9 -0
- data/spec/factories/form_factory.rb +6 -0
- data/spec/features/basic_navigation_spec.rb +125 -0
- data/spec/helpers/application_helper_spec.rb +40 -0
- data/spec/integration/collector/page_state_spec.rb +45 -0
- data/spec/models/collector/collector_spec.rb +100 -0
- data/spec/models/collector/page_state_spec.rb +30 -0
- data/spec/rails_helper.rb +73 -0
- data/spec/requests/collector/api/v1/page_state_spec.rb +85 -0
- data/spec/requests/collector/api/v1/states_spec.rb +28 -0
- data/spec/spec_helper.rb +63 -0
- data/spec/support/asserts.rb +27 -0
- data/spec/support/mailer_macros.rb +25 -0
- data/spec/support/page_objects/base_page_object.rb +77 -0
- data/spec/swagger_helper.rb +25 -0
- 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,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
|