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
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
|
+
[![Build Status](https://travis-ci.org/autotelik/datashift_journey.svg?branch=master)](https://travis-ci.org/autotelik/datashift_journey)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/autotelik/datashift_journey/badges/gpa.svg)](https://codeclimate.com/github/autotelik/datashift_journey)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/autotelik/datashift_journey/badges/coverage.svg)](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
|