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