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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2c69a2e39ef25608483ad7d2597251e7a3579c8b40cfb62c77bce84e51bceca9
|
|
4
|
+
data.tar.gz: 272adc891084777add171f204ce3f39f7b768ed34effde573288ca7e3939cc0f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cf5f9ab13ff5643093f7daac5fcbcfc9cfb08f7e843b9e07cfa701978905e64a125f74d309e752cc600010e6feb222a274c274b4b0a1d2db5df3e13f7c5acafd
|
|
7
|
+
data.tar.gz: 00c8c8b31b4094289182568be0a93e2f22942978105118dd41318aef9cdc07f9e9e0d5039180861c71245739f666b1233693e7350fc8dfb3342e07b9f6c27416
|
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
|
20
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
## DataShift Journey
|
|
2
|
+
|
|
3
|
+
[](https://travis-ci.org/autotelik/datashift_journey)
|
|
4
|
+
[](https://codeclimate.com/github/autotelik/datashift_journey)
|
|
5
|
+
[](https://codeclimate.com/github/autotelik/datashift_journey/coverage)
|
|
6
|
+
|
|
7
|
+
A Rails software [Wizard](https://en.wikipedia.org/wiki/Wizard_%28software%29)
|
|
8
|
+
|
|
9
|
+
Quickly create a sequence of forms (dialogs) that lead a visitor through a series
|
|
10
|
+
of defined steps - ideal for questionnaires, application forms, checkouts, configurations, surveys, registration processes etc.
|
|
11
|
+
|
|
12
|
+
Provides a simple DSL to quickly define a multi page journey through your site,
|
|
13
|
+
including complex branching, and rejoining, dependent on collected values.
|
|
14
|
+
|
|
15
|
+
State is maintained in one of the backends, with different storage models being provided
|
|
16
|
+
out of the box, or use your own model structure.
|
|
17
|
+
|
|
18
|
+
Full server-side processing can be delayed until the submission of the final form.
|
|
19
|
+
|
|
20
|
+
The DSL provides a simplified layer on top of a State Machine, with the main underlying gems being :
|
|
21
|
+
|
|
22
|
+
* https://github.com/state-machines/state_machines
|
|
23
|
+
* https://github.com/state-machines/state_machines-activerecord
|
|
24
|
+
* https://github.com/apotonick/reform
|
|
25
|
+
|
|
26
|
+
## Getting started
|
|
27
|
+
|
|
28
|
+
This is a Rails engine so simply add this line to your application's Gemfile:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem 'datashift_journey', git: 'https://github.com/autotelik/datashift_journey'
|
|
32
|
+
```
|
|
33
|
+
And then execute:
|
|
34
|
+
|
|
35
|
+
$ bundle install
|
|
36
|
+
|
|
37
|
+
DatashiftJourney needs a model on which to store the Wizard or Journey Plan through a state machine definition.
|
|
38
|
+
|
|
39
|
+
See below if you already have model you wish to decorate.
|
|
40
|
+
|
|
41
|
+
If you're starting from scratch, a **generator** - `rails generate datashift_journey:setup` - is provided to setup everything for you.
|
|
42
|
+
|
|
43
|
+
For example, to create a Checkout model, that will collect the data entered during a checkout journey,
|
|
44
|
+
such as confirm order, billing address, shipping address and payment data.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
rails generate datashift_journey:setup Checkout
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This will generate a number of files, including a model file, migration to create the model table with a single column called `state`
|
|
51
|
+
>If using an existing model, it's **vital** that this journey class has a string column called `state`
|
|
52
|
+
i.e If you need to add an associated migration yourself it should contain `t.string :state`
|
|
53
|
+
|
|
54
|
+
In this example this model would be located at : `app/models/checkout.rb`
|
|
55
|
+
|
|
56
|
+
A stub for entering the journey plan is added to the model.
|
|
57
|
+
|
|
58
|
+
You should edit this model and configure your required steps in the plan.
|
|
59
|
+
|
|
60
|
+
Details of the API are supplied in comments in the file and TODO: <link>
|
|
61
|
+
|
|
62
|
+
An initializer will also be created at : `config/initializers/datashift_journey.rb`
|
|
63
|
+
|
|
64
|
+
Inside the initializer you can change which model to use as the plan and set various configuration options.
|
|
65
|
+
|
|
66
|
+
### Defining the Journey Plan
|
|
67
|
+
|
|
68
|
+
A skeleton journey definition is added as a comment seciton to the plan model.
|
|
69
|
+
|
|
70
|
+
In here you defines the steps of the apps journey and set the initial step.
|
|
71
|
+
|
|
72
|
+
Here's a simple example for a basic checkout, on an ActiveRecord model, `Checkout`
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
|
|
76
|
+
class Checkout < ApplicationRecord
|
|
77
|
+
|
|
78
|
+
DatashiftJourney::Journey::MachineBuilder.create_journey_plan(initial: :ship_address) do
|
|
79
|
+
|
|
80
|
+
# Two simple sequential steps
|
|
81
|
+
sequence [:ship_address, :bill_address]
|
|
82
|
+
|
|
83
|
+
# At the next step, we will have a branch so first define the branch nodes - they also can
|
|
84
|
+
# be sequences of multiple steps, a single step, or nothing (skip straight to branch recombination step)
|
|
85
|
+
branch_sequence :visa_sequence, [:visa_page1]
|
|
86
|
+
|
|
87
|
+
branch_sequence :mastercard_sequence, [:page_mastercard1, :page_mastercard2]
|
|
88
|
+
|
|
89
|
+
branch_sequence :paypal_sequence, []
|
|
90
|
+
|
|
91
|
+
# Define the next state (after :bill_address, and parent state of the branch)
|
|
92
|
+
# and the routing criteria to each sequence
|
|
93
|
+
# So after bill address we reach payment - then we split to a single step, depending on the card type entered
|
|
94
|
+
|
|
95
|
+
split_on_equality( :payment,
|
|
96
|
+
"payment_card", # Helper method on Checkout that returns card type from Payment
|
|
97
|
+
visa_sequence: 'visa',
|
|
98
|
+
mastercard_sequence: 'mastercard',
|
|
99
|
+
paypal_sequence: 'paypal'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# All different card type branches, recombine here at review
|
|
103
|
+
sequence [:review, :complete ]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
....
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A state machine will be generated with all steps starting at :ship_address and finishing at :complete,
|
|
110
|
+
and forward and backwards navigation between them.
|
|
111
|
+
|
|
112
|
+
> *A backing Reform style Form and associated view partial, will be expected for each state.*
|
|
113
|
+
|
|
114
|
+
### View Forms
|
|
115
|
+
|
|
116
|
+
The default controllers expect the Forms pattern to back each view.
|
|
117
|
+
|
|
118
|
+
DSJ includes the Reform gem and utilises their Form - see - https://github.com/trailblazer/reform
|
|
119
|
+
|
|
120
|
+
These tend to do the work traditionally performed in the Controller, so our controller can stay generic
|
|
121
|
+
and focused on navigation.
|
|
122
|
+
|
|
123
|
+
Forms give you the flexability to implement your own strategies for dealing with the presentation data required for a view,
|
|
124
|
+
managing params, validating entreed values, and saving the data entered into forms.
|
|
125
|
+
|
|
126
|
+
A **generator** is provided that can create skeleton Forms for you, one per state(page).
|
|
127
|
+
|
|
128
|
+
Options
|
|
129
|
+
|
|
130
|
+
> [--base-class=ClassName] # Class to use as the Base class for generated Forms
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
rails generate datashift_journey:forms
|
|
134
|
+
```
|
|
135
|
+
Generated Forms derive from `datashift_journey/app/forms/datashift_journey/base_form.rb`
|
|
136
|
+
|
|
137
|
+
And ultimately from Reform::Form
|
|
138
|
+
|
|
139
|
+
### Views
|
|
140
|
+
|
|
141
|
+
Each step will need a view, usually a form to collect information from, but can be a static page or any content you like really.
|
|
142
|
+
|
|
143
|
+
A **generator** is provided that can create a starter set of partial views for you, one per state(page).
|
|
144
|
+
|
|
145
|
+
So, once the journey plan has been fully defined run :
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
rails generate datashift_journey:views
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The Controller will expect a view partial, for each related Form.
|
|
152
|
+
|
|
153
|
+
The partials are rendered passing in the Form as a local variable.
|
|
154
|
+
|
|
155
|
+
The location of the partial to use for a certain state is given by helper
|
|
156
|
+
|
|
157
|
+
def journey_plan_partial_location( state )
|
|
158
|
+
|
|
159
|
+
The default is `app/views` but path can be changed using Configuration option `partial_location`
|
|
160
|
+
|
|
161
|
+
This will be required in the path format, if you are using multiple namespaces/folders
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
DatashiftJourney::Configuration.configure do |config|
|
|
165
|
+
config.partial_location = "checkout_engine"
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
### Data Collection
|
|
169
|
+
Ultimately the views and forms are there to collect data from a User, validate and store it.
|
|
170
|
+
|
|
171
|
+
Generate and use associated backing Reform forms to validate and store data, collected from your visitors.
|
|
172
|
+
|
|
173
|
+
The Reform form expects to be backed by a model, and can write data back to the model via sync and save methods,
|
|
174
|
+
enabling you to populate the data however you choose within your Forms - see section 'Custom Data Collector'
|
|
175
|
+
|
|
176
|
+
Alternatively a generic SQL based data collector is provided for use with the generic generated Forms to save the data.
|
|
177
|
+
|
|
178
|
+
### Data Collector
|
|
179
|
+
|
|
180
|
+
This setup will add a migration, creating a number of tables to manage the collection of data, on a form by form basis,
|
|
181
|
+
stored as a series of nodes, essentially keyed on the form class and name (state).
|
|
182
|
+
|
|
183
|
+
The journey plan class is decorated with an association the the data nodes, so each instance of the journey holds all the data for that joureny,
|
|
184
|
+
as a collection of fields (name/value) i.e one database row per question/answer.
|
|
185
|
+
|
|
186
|
+
To use this concern as your main data collection agent, simply run the installer
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
rails generate datashift_journey:collector
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This will create relevant migrations and decorate your journey plan class with data collection attributes.
|
|
193
|
+
|
|
194
|
+
To access manually, derive your form from `DatashiftJourney::Collector::BaseCollectorForm`.
|
|
195
|
+
|
|
196
|
+
### Mongo Data Collector
|
|
197
|
+
|
|
198
|
+
#### TODO
|
|
199
|
+
|
|
200
|
+
An optional MongoDB based Collector is under development, which will collect data in a single document per journey.
|
|
201
|
+
|
|
202
|
+
To use this model as your main data collection class, simply run the installer to setup the model.
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
rails generate datashift_journey:install_mongo_collector
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### Journeys End
|
|
209
|
+
|
|
210
|
+
The controller should identify when the last state has been submitted and there are no further states to be rendered.
|
|
211
|
+
|
|
212
|
+
After processing the last state, the controller will redirect to the JourneyEndsController and its default new view will be rendered.
|
|
213
|
+
|
|
214
|
+
To provide your own end page, in your app simply override this view - `app/views/datashift_journey/journey_ends/new.html.erb`
|
|
215
|
+
|
|
216
|
+
You can also implement an `on_journey_end` hook, in your JourneyPlan class, which will be called by the controller
|
|
217
|
+
(if implemented) **before** the view is rendered.
|
|
218
|
+
|
|
219
|
+
For example, you can spin off jobs that parse and process the data
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class Checkout < ApplicationRecord
|
|
223
|
+
def on_journey_end
|
|
224
|
+
Apoc::CreateOrderWorker.perform_async(self.id)
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Internals
|
|
229
|
+
|
|
230
|
+
> The Classified version of the state name, plus "Form", so a `:billing_address` state should have an associated form called `BillingAddressForm`
|
|
231
|
+
|
|
232
|
+
The DSJ Controller will search for a matching Form for each state using the Factory class/method
|
|
233
|
+
|
|
234
|
+
## OUT OF DATE
|
|
235
|
+
```ruby
|
|
236
|
+
DatashiftState::FormObjectFactory.form_object_for(journey_plan)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
In code, the expected form Class name is defined as :
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
"#{modules}::#{journey_plan.state.classify}Form"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
When using namespaces the *module* structure , can be set via the DSJ Configuration object,
|
|
246
|
+
which can be set, using a standard block format, in an initializer, as so:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
DatashiftJourney::Configuration.configure do |config|
|
|
250
|
+
config.forms_module_name = 'MyCheckoutEngine'
|
|
251
|
+
end
|
|
252
|
+
````
|
|
253
|
+
|
|
254
|
+
So given a module name configuration setting of
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
MyCheckoutEngine::States
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
And a current state of :address - then the Controller will attempt to use Form class
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
MyCheckoutEngine::States::AddressForm
|
|
264
|
+
```
|
|
265
|
+
## END OUT OF DATE
|
|
266
|
+
|
|
267
|
+
#### Null Forms
|
|
268
|
+
|
|
269
|
+
When **no form** is required for a specific HTML page, you an specify that NullForm is to be used,
|
|
270
|
+
either globally for ALL missing forms, or for specific named forms.
|
|
271
|
+
|
|
272
|
+
The null form means no params are validated, and no save performed, for example for a mid sequence, text only
|
|
273
|
+
helper page, or for a text only branch terminating page.
|
|
274
|
+
|
|
275
|
+
To set globally
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
DatashiftJourney::Configuration.configure do |config|
|
|
279
|
+
config.use_null_form_when_no_form = true
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
To set for individual states, with no data collection requirements, add to list of `null_form` states
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
DatashiftJourney::Configuration.configure do |config|
|
|
287
|
+
config.null_form_list = [:confirm_page_with_no_data, :brexit]
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### DatashiftJourney::Collector
|
|
292
|
+
|
|
293
|
+
This option uses the journey plan class itself to store the data collected from each state/page in `data_nodes`
|
|
294
|
+
|
|
295
|
+
This collection is a series of `DatashiftJourney::Collector::DataNode` objects.
|
|
296
|
+
|
|
297
|
+
The generator will geneate a series of forms that inherit from `DatashiftJourney::Collector::BaseCollectorForm`
|
|
298
|
+
and your state form becomes very simple, for example
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
class ResourcesForm < ::BaseForm
|
|
302
|
+
journey_plan_form_field name: :namespace, category: :string
|
|
303
|
+
journey_plan_form_field name: :number_of_cpu, category: :number
|
|
304
|
+
journey_plan_form_field name: :memory, category: :number
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
This will create field definitions, which can then be rendered automatically using the default views,
|
|
309
|
+
and on submit, the data entered will be saved to the DB as a DataNode, one per field name
|
|
310
|
+
|
|
311
|
+
```JSON
|
|
312
|
+
DataNode: {
|
|
313
|
+
"id":8,
|
|
314
|
+
"form_field_id": 1,
|
|
315
|
+
"field_value": "My New Namespace"
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Routes
|
|
320
|
+
|
|
321
|
+
The generator will add the engines routes to your app's `config/routes.rb` file.
|
|
322
|
+
You can manually change the mount point to whatever suits your application.
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
Rails.application.routes.draw do
|
|
326
|
+
mount DatashiftJourney::Engine => "/"
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
If youd like to set your apps root to be the initial state, you can manually add the following :
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
Rails.application.routes.draw do
|
|
334
|
+
root to: "datashift_journey/journey_plans#new"
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### State Jumper Toolbar
|
|
339
|
+
|
|
340
|
+
There is breadcrumb style toolbar available for creating and jumping straight to any State
|
|
341
|
+
|
|
342
|
+
This must be activated by setting
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
config.add_state_jumper_toolbar = true
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
In development, so that any data required for previous states can be created, it supports passing in a Factory
|
|
349
|
+
that creates that data for you.
|
|
350
|
+
|
|
351
|
+
The factory should return an instance of your DatashiftJourney.journey_plan_class
|
|
352
|
+
|
|
353
|
+
Configure your list of required 'jump to' states and factories - where no factory required simply pass nil -
|
|
354
|
+
by setting `state_jumper_states`, for example
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
config.state_jumper_states = {contact: my_contact_factory, ship_address: nil, :bill_address: nil}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The view is added to a content_for block called :datashift_journey_state_jumper
|
|
361
|
+
so you can add this somewhere in your layout.
|
|
362
|
+
|
|
363
|
+
To pull in some default styling add following to your `application.css.scss`
|
|
364
|
+
|
|
365
|
+
`@import 'datashift_journey/partials/state_jumper_toolbar';`
|
|
366
|
+
|
|
367
|
+
## License
|
|
368
|
+
|
|
369
|
+
Author :: Tom Statter
|
|
370
|
+
|
|
371
|
+
Date :: April 2016
|
|
372
|
+
|
|
373
|
+
The MIT License
|
|
374
|
+
|
|
375
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
376
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
377
|
+
in the Software without restriction, including without limitation the rights
|
|
378
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
379
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
380
|
+
furnished to do so, subject to the following conditions:
|
|
381
|
+
|
|
382
|
+
The above copyright notice and this permission notice shall be included in
|
|
383
|
+
all copies or substantial portions of the Software.
|
|
384
|
+
|
|
385
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
386
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
387
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
388
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
389
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
390
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
391
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
load 'rails/tasks/statistics.rake'
|
|
8
|
+
|
|
9
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
|
10
|
+
|
|
11
|
+
load 'rails/tasks/engine.rake'
|
|
12
|
+
|
|
13
|
+
Bundler::GemHelper.install_tasks
|
|
14
|
+
|
|
15
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
|
|
16
|
+
|
|
17
|
+
require 'rspec/core'
|
|
18
|
+
require 'rspec/core/rake_task'
|
|
19
|
+
|
|
20
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
|
21
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
|
22
|
+
|
|
23
|
+
task :default => :spec
|
|
24
|
+
|
|
25
|
+
# Added as this was quick way to make tasks like rswag:specs:swaggerize available to the Engine
|
|
26
|
+
Rails.application.load_tasks
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* A simple toolbar for navigating states in DEV only - NOT FOR PRODUCTION */
|
|
2
|
+
|
|
3
|
+
<% unless(Rails.env.production?) %>
|
|
4
|
+
|
|
5
|
+
#datashift_journey_state_jumper {
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#datashift_journey_state_jumper ul
|
|
9
|
+
{
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
list-style-type: none;
|
|
13
|
+
text-align: center;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#datashift_journey_state_jumper ul li { display: inline; }
|
|
17
|
+
|
|
18
|
+
#datashift_journey_state_jumper ul li a
|
|
19
|
+
{
|
|
20
|
+
font-size: 10px;
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
padding: .2em 1em;
|
|
23
|
+
color: #fff;
|
|
24
|
+
background-color: #036;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
<% end %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module DatashiftJourney
|
|
2
|
+
|
|
3
|
+
module ReviewRenderer
|
|
4
|
+
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
def render_state_under_review(params)
|
|
8
|
+
# We need to track the state we are reviewing across calls.
|
|
9
|
+
# From first edit via the review page itself, this may come as :state
|
|
10
|
+
#
|
|
11
|
+
@rendered_state = params[:rendered_state] || params[:state]
|
|
12
|
+
|
|
13
|
+
# Once in review mode, we don't mess with the current state
|
|
14
|
+
# so use the over ride member @render_state_partial to drive which view to render
|
|
15
|
+
@render_state_partial = view_context.journey_plan_partial_location(@rendered_state)
|
|
16
|
+
|
|
17
|
+
render template: '/datashift_journey/journey_plans/edit'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module DatashiftJourney
|
|
2
|
+
module TokenBasedAccess
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
# Front end journey_plan urls use the journey_plan's token as the id param
|
|
5
|
+
# (see #to_param in journey_plan_decorator.rb).
|
|
6
|
+
# A token is only really valid while the journey_plan is being created; once submitted
|
|
7
|
+
# it should not be possible to reference an journey_plan by token or id from the front end.
|
|
8
|
+
included do
|
|
9
|
+
def set_journey_plan
|
|
10
|
+
token = params[:id] || params[:journey_plan_id]
|
|
11
|
+
|
|
12
|
+
# https://github.com/robertomiranda/has_secure_token
|
|
13
|
+
# TODO: how to auto insert has_secure_token into the underlying journey plan model
|
|
14
|
+
# and add in migration thats adds the token column
|
|
15
|
+
# @journey_plan = DatashiftJourney.journey_plan_class.find_by_token!(token)
|
|
16
|
+
|
|
17
|
+
@journey_plan = DatashiftJourney.journey_plan_class.find(token)
|
|
18
|
+
|
|
19
|
+
logger.debug("Processing Journey: #{@journey_plan.inspect}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module DatashiftJourney
|
|
2
|
+
|
|
3
|
+
module ValidateState
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
# Needs further investigation into which situations require this
|
|
7
|
+
|
|
8
|
+
# http://jacopretorius.net/2014/01/force-page-to-reload-on-browser-back-in-rails.html
|
|
9
|
+
def back_button_cache_buster
|
|
10
|
+
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
|
11
|
+
response.headers['Pragma'] = 'no-cache'
|
|
12
|
+
response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Currently the following Situations need special processing, carried out here.
|
|
16
|
+
#
|
|
17
|
+
# * REFRESH - users hits refresh & resubmits a form - check state associated with view
|
|
18
|
+
#
|
|
19
|
+
# It is not expected to redirect or halt processing chain - it is simply to ensure state is manged correctly,
|
|
20
|
+
# for situations outside standard state flow/processing
|
|
21
|
+
#
|
|
22
|
+
# rubocop:disable Style/GuardClause
|
|
23
|
+
def validate_state
|
|
24
|
+
current_index = journey_plan.current_state_index
|
|
25
|
+
|
|
26
|
+
view_state = params[:rendered_state]
|
|
27
|
+
|
|
28
|
+
view_state_idx = journey_plan.state_index(view_state) # nil for bad states
|
|
29
|
+
|
|
30
|
+
logger.debug 'STATE and Param INFO:'
|
|
31
|
+
logger.debug " Current State \t[#{journey_plan.state.inspect}] IDX [#{current_index}]"
|
|
32
|
+
logger.debug " Rendered State \t[#{view_state}] IDX [#{view_state_idx}]"
|
|
33
|
+
logger.debug " Redirect to \t[#{params[:redirect_to]}]"
|
|
34
|
+
|
|
35
|
+
if view_state && view_state_idx && (view_state_idx < current_index)
|
|
36
|
+
logger.info("Probable User refresh, resetting state #{view_state} [IDX (#{view_state_idx})]")
|
|
37
|
+
journey_plan.state = view_state
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if view_state.nil? && params[:state] && back_button_param_list.all? { |p| params.key?(p) }
|
|
41
|
+
|
|
42
|
+
transitions = journey_plan.transitions_for
|
|
43
|
+
|
|
44
|
+
back_event = transitions.find { |t| t.event == :back }
|
|
45
|
+
|
|
46
|
+
if back_event && back_event.from == journey_plan.state && back_event.to == params[:state]
|
|
47
|
+
logger.debug("User CLICKED back Button - resetting state to [#{params[:state]}] from #{journey_plan.state}")
|
|
48
|
+
journey_plan.update!(state: params[:state])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def back_button_param_list
|
|
55
|
+
@back_button_param_list ||= [:state, :id, :action, :controller]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module DatashiftJourney
|
|
2
|
+
class AbandonEnrollmentsController < DatashiftJourney::AbandonmentsController
|
|
3
|
+
# journey_plan object should been selected from the DB
|
|
4
|
+
prepend_before_filter :set_journey_plan, only: [:show]
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
logger.info("User has abandoned #{params}")
|
|
8
|
+
|
|
9
|
+
# high voltage expects id => name of page
|
|
10
|
+
params[:id] = params['page']
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module DatashiftJourney
|
|
2
|
+
|
|
3
|
+
class AbandonmentsController < ApplicationController
|
|
4
|
+
|
|
5
|
+
include HighVoltage::StaticPage
|
|
6
|
+
|
|
7
|
+
# idea is that here was can log/update DB with any useful info on
|
|
8
|
+
# where/why user has abandoned, before we fwd them onto a relevant
|
|
9
|
+
# static helper page (N.B served up from views/pages)
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
logger.info('User has abandoned')
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module DatashiftJourney
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
|
|
5
|
+
class StatesController < ActionController::API
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
|
|
9
|
+
state_paths = []
|
|
10
|
+
|
|
11
|
+
DatashiftJourney.journey_plan_class.new.state_paths.each_with_index do |s, i|
|
|
12
|
+
state_paths << {
|
|
13
|
+
index: i, events: s.events, from_name: s.from_name, to_name: s.to_name
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
states = DatashiftJourney.journey_plan_class.state_machine.states.collect do |s|
|
|
18
|
+
{
|
|
19
|
+
name: s.name, value: s.value, initial: s.initial, final: s.final?
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
events = DatashiftJourney.journey_plan_class.state_machine.events.collect do |e|
|
|
24
|
+
{
|
|
25
|
+
#machine: s.machine,
|
|
26
|
+
name: e.name,
|
|
27
|
+
qualified_name: e.qualified_name,
|
|
28
|
+
human_name: e.human_name,
|
|
29
|
+
branches: e.branches,
|
|
30
|
+
known_states: e.known_states,
|
|
31
|
+
|
|
32
|
+
transitions: e.branches.map do |branch|
|
|
33
|
+
branch.state_requirements.map do |state_requirement|
|
|
34
|
+
{state_requirement: state_requirement, from: state_requirement[:from].class, to: state_requirement[:to]}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
render json: { data:
|
|
41
|
+
{
|
|
42
|
+
states: states, state_paths: state_paths, events: events
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|