govuk-wizardry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: de9d24e16cac679c4ee2c36dd8a8556dbe051ffabbfc8771c1dcddd5a00964c3
4
+ data.tar.gz: f7bf947d3cbb3e95b03c12d5f527805c1d005d09a6e12738ae7ec77fc4a644df
5
+ SHA512:
6
+ metadata.gz: 47ac4127a0b74792a27812c48c5911cea6341a7f215714a6b95003443e8237dc70a9ac85129a8b7e88b9e91e98b276cc18fb5a532ef4fcf5e25ec2419967ffa4
7
+ data.tar.gz: 98515e090121ce5703f1058190a98ff2d487fa361703bfaefd8578d0c2463621a060ac47b81ba6af566fce3412eac07d4c6ccc62ec0fcc2a03e7f771cef61c07
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Graphia Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Wizardry 🧙
2
+
3
+ This gem for Rails allows application writers to define a GOV.UK wizard
4
+ (multi-page form) in one place.
5
+
6
+ **Note this library is at the very early stages of development. It's not
7
+ properly tested and is very likely to change**.
8
+
9
+ ## Things the library does:
10
+
11
+ * renders the wizard pages based on the specification ✔
12
+ * correctly annotates the pages using Rails' I18n functionality ✔
13
+ * deals with form submissions ✔
14
+ * applies contextual validation on a per-page basis and displaying error messages ✔
15
+ * let developers override pages and questions with their own partials ✔
16
+ * allow for extra customisation over the size of labels, legends and captions ✔
17
+ * allow conditional logic to customise the path through the journey ✔
18
+ * provide a callbacks upon step and wizard completion ✔
19
+
20
+ ## How does it work?
21
+
22
+ The wizard relies on the application having a single model that will hold the
23
+ collected data. Each question in the wizard maps to a field and every page
24
+ maps to a [validation context](https://guides.rubyonrails.org/active_record_validations.html#on).
25
+
26
+ Throughout the process the last completed step is recorded in the model. It's
27
+ used to track progress and hold the state of the response. It is automatically
28
+ updated when a successful submission is made.
29
+
30
+ ### Example
31
+
32
+ Just include the `Wizardry` module in your controller and use the `wizard` macro
33
+ to define your pages and questions:
34
+
35
+ ```ruby
36
+ class RatingsController < ApplicationController
37
+ include Wizardry
38
+
39
+ wizard name: 'ratings',
40
+ class_name: 'Rating',
41
+ edit_path_helper: :ratings_page_path,
42
+ update_path_helper: :ratings_path,
43
+ pages: [
44
+ Wizardry::Pages::Page.new(
45
+ :identification,
46
+ title: 'Who are you?',
47
+ questions: [
48
+ Wizardry::Questions::ShortAnswer.new(:full_name),
49
+ Wizardry::Questions::EmailAddress.new(:email)
50
+ ]
51
+ ),
52
+ Wizardry::Pages::Page.new(
53
+ :feedback,
54
+ title: 'What did you think about our service?',
55
+ questions: [
56
+ Wizardry::Questions::ShortAnswer.new(:customer_type),
57
+ Wizardry::Questions::Date.new(:purchase_date),
58
+ Wizardry::Questions::LongAnswer.new(:feedback),
59
+ Wizardry::Questions::Radios.new(:rating, {
60
+ 1 => 'Dire', 2 => 'Alright', 3 => 'Average', 4 => 'Decent', 5 => 'Amazing'
61
+ })
62
+ ]
63
+ ),
64
+ Wizardry::Pages::CheckYourAnswersPage.new,
65
+ ]
66
+ end
67
+ ```
68
+
69
+ ### Routes
70
+
71
+ In the example above, we're providing the `edit_path_helper` and
72
+ `update_path_helper` values. These are the Rails path helpers that correspond
73
+ with the path that displays the current page of the wizard and the path that
74
+ updates the object.
75
+
76
+ ```ruby
77
+ Rails.application.routes.draw do
78
+ get %(rating/:page), to: %(ratings#edit), as: :ratings_page
79
+ patch %(rating/:page), to: %(ratings#update), as: :ratings
80
+ end
81
+ ```
82
+
83
+ ### Rendering
84
+
85
+ To add the wizard content to your `edit` template, the gem comes with a
86
+ `wizardry_content` helper. Additionally, the wizard object is available for the
87
+ developer to interact with in the `@wizard` instance variable. Here we are
88
+ using it to set the heading.
89
+
90
+ ```erb
91
+ <h1 class="govuk-heading-l"><%= @wizard.current_page.name.capitalize %></h1>
92
+
93
+ <%= wizardry_content %>
94
+ ```
95
+
96
+ ### Special pages
97
+
98
+ Most pages in a wizard are ones that ask questions. Many wizards also contain a
99
+ [check your answers](https://design-system.service.gov.uk/patterns/check-answers/) page and
100
+ a page that's displayed upon completion. These are both **in scope** for this
101
+ gem.
102
+
103
+ ### Preview
104
+
105
+ | Page one | Page two |
106
+ | -----------------| ---------- |
107
+ | ![identification page](doc/images/identification.png) | ![feedback page](doc/images/feedback.png) |
data/Rakefile ADDED
@@ -0,0 +1,27 @@
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
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Wizardry'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,121 @@
1
+ module WizardryHelper
2
+ def wizardry_content(w)
3
+ case w.current_page
4
+ when Wizardry::Pages::QuestionPage
5
+ render_form(w)
6
+ when Wizardry::Pages::CheckYourAnswersPage
7
+ safe_join([render_check_your_answers(w), render_form(w)])
8
+ when Wizardry::Pages::CompletionPage
9
+ render_completion(w)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def render_form(w)
16
+ capture do
17
+ try_partial(w, 'page') do
18
+ wizard_form(w) do |f|
19
+ try_partial(w, 'form', locals: { f: f }) do
20
+ concat(f.govuk_error_summary)
21
+
22
+ w.current_page.questions.map do |q|
23
+ concat(f.send(q.form_method, q.name, *q.extra_args, **q.extra_kwargs))
24
+ end
25
+
26
+ concat(f.govuk_submit)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def render_check_your_answers(w)
34
+ capture do
35
+ try_partial(w, 'page') do
36
+ safe_join(
37
+ [
38
+ render(GovukComponent::SummaryListComponent.new) do |summary_list|
39
+ w.route.each do |page|
40
+ page.questions.each do |question|
41
+ summary_list.row do |sl|
42
+ sl.key(text: check_your_answers_key(w.object.class.name, question.name))
43
+ sl.value(text: w.object.send(question.name))
44
+ sl.action(href: send(w.framework.edit_path_helper, page.name))
45
+ end
46
+ end
47
+ end
48
+ end
49
+ ]
50
+ )
51
+ end
52
+ end
53
+ end
54
+
55
+ def render_completion(w)
56
+ capture do
57
+ try_partial(w, 'page') do
58
+ safe_join(
59
+ [
60
+ tag.h1('Completed'),
61
+
62
+ tag.p do
63
+ [
64
+ 'Add a partial called',
65
+ tag.code(%(_completion.html.erb)),
66
+ 'to override this message'
67
+ ].join(' ').html_safe
68
+ end
69
+ ]
70
+ )
71
+ end
72
+ end
73
+ end
74
+
75
+ def wizard_form(w, turbo_frame_id: 'wizardry-form', &block)
76
+ turbo_frame_tag(turbo_frame_id) do
77
+ safe_join(
78
+ [
79
+ form_for(
80
+ w.object,
81
+ url: send(
82
+ w.framework.update_path_helper,
83
+ page: w.current_page.name
84
+ ),
85
+ method: :patch,
86
+ builder: GOVUKDesignSystemFormBuilder::FormBuilder,
87
+ &block
88
+ )
89
+ ]
90
+ )
91
+ end
92
+ end
93
+
94
+ def try_partial(w, section, locals: {}, &block)
95
+ if lookup_context.template_exists?(partial_file_path(w, section))
96
+ Rails.logger.debug("🧙 Page partial #{partial_file_path(w, section)} found; rendering entire page")
97
+
98
+ concat(render(partial: partial_render_path(w, section), locals: locals))
99
+ else
100
+ Rails.logger.debug('🧙 No overriding form partial found; automatically building form')
101
+
102
+ block.call
103
+ end
104
+ end
105
+
106
+ def check_your_answers_key(class_name, question_name)
107
+ I18n.t!("helpers.legend.#{class_name.underscore}.#{question_name}")
108
+ rescue I18n::MissingTranslationData
109
+ I18n.t("helpers.label.#{class_name.underscore}.#{question_name}")
110
+ end
111
+
112
+ def partial_file_path(w, suffix)
113
+ "#{w.framework.name}/_#{w.current_page.name}_#{suffix}"
114
+ end
115
+
116
+ def partial_render_path(w, suffix)
117
+ "#{w.framework.name}/#{w.current_page.name}_#{suffix}"
118
+ end
119
+ end
120
+
121
+ ActiveSupport.on_load(:action_view) { include WizardryHelper }
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :wizard do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,7 @@
1
+ module Wizardry
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Wizardry
4
+ end
5
+ end
6
+
7
+ Dir[Wizardry::Engine.root.join(*%w(app helpers *.rb))].each { |f| require f }
@@ -0,0 +1,56 @@
1
+ module Wizardry
2
+ # Framework holds data on how the wizard itself is constructed. It'll
3
+ # be the same for every instance
4
+ class Framework
5
+ attr_accessor :name, :pages, :class_name, :edit_path_helper, :update_path_helper, :completion_flag
6
+
7
+ def initialize(name:, pages:, class_name:, edit_path_helper:, update_path_helper:, completion_flag: 'complete')
8
+ @name = name
9
+ @pages = setup_pages(pages)
10
+ @class_name = class_name
11
+ @edit_path_helper = edit_path_helper
12
+ @update_path_helper = update_path_helper
13
+ @completion_flag = completion_flag
14
+
15
+ page_sense_check
16
+ end
17
+
18
+ def cookie_name
19
+ %(#{@name}-wizard)
20
+ end
21
+
22
+ def page_names
23
+ pages.map(&:name)
24
+ end
25
+
26
+ def trunk_pages
27
+ pages.reject(&:branch?)
28
+ end
29
+
30
+ def branch_pages
31
+ pages.select(&:branch?)
32
+ end
33
+
34
+ def page(name)
35
+ pages.detect { |p| p.name == name }
36
+ end
37
+
38
+ private
39
+
40
+ def page_sense_check
41
+ # should have no more than one check your answers page
42
+ pages.count { |page| page.is_a?(Wizardry::Pages::CheckYourAnswersPage) }.tap do |check_your_answers_pages|
43
+ Rails.logger.warn("🧙 More than one check your answers page detected") if check_your_answers_pages > 1
44
+ end
45
+
46
+ # should have no more than one completion page
47
+ pages.count { |page| page.is_a?(Wizardry::Pages::CompletionPage) }.tap do |completion_pages|
48
+ Rails.logger.warn("🧙 More than one completion page detected") if completion_pages > 1
49
+ end
50
+ end
51
+
52
+ def setup_pages(pages)
53
+ pages.map { |page| [page, page.pages.each(&:branch!)].select(&:presence) }.flatten
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,55 @@
1
+ module Wizardry
2
+ class Graph
3
+ attr_reader :graph
4
+
5
+ def initialize(framework)
6
+ @framework = framework
7
+ @graph = reticulate
8
+ end
9
+
10
+ def to_dot
11
+ <<~GRAPH
12
+ digraph name {
13
+ #{build_edges.flatten.join(';')};
14
+ }
15
+ GRAPH
16
+ end
17
+
18
+ private
19
+
20
+ def reticulate
21
+ @framework.pages.each.with_index.with_object({}) do |(current, i), g|
22
+ g[current.name] = specified_next_pages(current).merge(following_page(current, i))
23
+ end
24
+ end
25
+
26
+ def build_edges
27
+ @graph.map do |source, targets|
28
+ targets.map do |target_page, condition|
29
+ %(#{source} -> #{target_page}).tap do |edge|
30
+ if condition.present?
31
+ formatted_condition = %("#{condition}")
32
+ edge << %( [label=#{formatted_condition}])
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def specified_next_pages(page)
40
+ page.next_pages.each.with_object({}) do |np, h|
41
+ h[np.name] = np.label
42
+ end
43
+ end
44
+
45
+ def following_page(_page, index)
46
+ next_non_branch_page = @framework.pages[index.next..].detect { |p| !p.branch? }
47
+
48
+ if next_non_branch_page.present?
49
+ { next_non_branch_page.name => nil }
50
+ else
51
+ { finish: nil }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,74 @@
1
+ module Wizardry
2
+ # Instance holds data specific to this page/response
3
+ class Instance
4
+ attr_accessor :current_page, :object, :framework
5
+
6
+ def initialize(current_page:, object:, framework:)
7
+ @object = object
8
+ @framework = framework
9
+ @current_page = @framework.pages.detect { |p| p.name == current_page.to_sym }
10
+
11
+ raise(ActionController::RoutingError, %(Wizard page #{current_page} not found)) unless @current_page
12
+ end
13
+
14
+ def next_page(page = current_page)
15
+ next_branch_page(page) || next_trunk_page(page)
16
+ end
17
+
18
+ # find all the pages we've visited on our way to
19
+ # the current page
20
+ def route(from = framework.pages.first)
21
+ @route ||= route!(from)
22
+ end
23
+
24
+ def route!(from = framework.pages.first)
25
+ page = from
26
+
27
+ @route = [].tap do |completed|
28
+ until page == current_page
29
+ completed << page
30
+
31
+ page = next_page(page)
32
+ end
33
+ end
34
+ end
35
+
36
+ def valid_so_far?
37
+ route.all? { |complete_page| object.valid?(complete_page.name) }
38
+ end
39
+
40
+ # check this wizard hasn't already been completed using the
41
+ # object's :completion_flag
42
+ def ensure_not_complete
43
+ raise Wizardry::AlreadyCompletedError if complete?
44
+ end
45
+
46
+ def complete?
47
+ object.send(framework.completion_flag)
48
+ end
49
+
50
+ private
51
+
52
+ def next_branch_page(page)
53
+ next_page = page.next_pages.detect do |p|
54
+ p.condition.blank? || p.condition.call(object)
55
+ end
56
+
57
+ return unless next_page
58
+
59
+ framework.page(next_page.name)
60
+ end
61
+
62
+ # if the branch ends continue along the trunk from
63
+ # where we left off
64
+ def next_trunk_page(page)
65
+ page_index = framework.pages.index(page)
66
+
67
+ framework.pages.detect.with_index do |p, i|
68
+ next if p.branch?
69
+
70
+ i > page_index
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ module Wizardry
2
+ module Pages
3
+ class CheckYourAnswersPage < Page
4
+ attr_reader :title, :questions
5
+
6
+ def initialize(questions: [], title: 'Check your answers')
7
+ @title = title
8
+ @questions = questions
9
+
10
+ super(pages: [])
11
+ end
12
+
13
+ def name
14
+ :check_your_answers
15
+ end
16
+
17
+ def question_names
18
+ @questions.map(&:name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Wizardry
2
+ module Pages
3
+ class CompletionPage < Page
4
+ def name
5
+ :completion
6
+ end
7
+
8
+ def title
9
+ ''
10
+ end
11
+
12
+ def questions
13
+ []
14
+ end
15
+
16
+ def next_pages
17
+ []
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ module Wizardry
2
+ module Pages
3
+ class Page
4
+ attr_reader :pages
5
+
6
+ def initialize(pages: [], before_edit: nil, before_update: nil, after_update: nil)
7
+ @pages = pages
8
+ @branch = false
9
+
10
+ # callbacks
11
+ @before_edit = before_edit
12
+ @before_update = before_update
13
+ @after_update = after_update
14
+ end
15
+
16
+ def branch!
17
+ @branch = true
18
+ end
19
+
20
+ def branch?
21
+ @branch
22
+ end
23
+
24
+ def questions
25
+ []
26
+ end
27
+
28
+ def next_pages
29
+ []
30
+ end
31
+
32
+ def before_edit!(object)
33
+ return unless @before_edit
34
+
35
+ @before_edit.call(object)
36
+ end
37
+
38
+ def before_update!(object)
39
+ return unless @before_update
40
+
41
+ @before_update.call(object)
42
+ end
43
+
44
+ def after_update!(object)
45
+ return unless @after_update
46
+
47
+ @after_update.call(object)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ module Wizardry
2
+ module Pages
3
+ class QuestionPage < Page
4
+ attr_accessor :name, :questions, :title, :next_pages
5
+
6
+ def initialize(name, questions:, next_pages: {}, pages: [], title: nil, before_edit: nil, before_update: nil, after_update: nil)
7
+ Rails.logger.debug("🧙 Adding page '#{name}' with #{questions&.size || 'no'} questions")
8
+
9
+ @name = name
10
+ @title = title || name.capitalize
11
+ @questions = questions
12
+ @next_pages = next_pages
13
+
14
+ super(pages: pages, before_edit: before_edit, before_update: before_update, after_update: after_update)
15
+ end
16
+
17
+ def question_names
18
+ @questions.map(&:name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Wizardry
2
+ module Questions
3
+ class Answer
4
+ attr_reader :name
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ end
9
+
10
+ def extra_args
11
+ []
12
+ end
13
+
14
+ def extra_kwargs
15
+ {}
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Wizardry
2
+ module Questions
3
+ # Ask a date question
4
+ class Date < Wizardry::Questions::Answer
5
+ def initialize(name)
6
+ Rails.logger.debug("🧙 Adding date question '#{name}'")
7
+ super
8
+ end
9
+
10
+ def form_method
11
+ :govuk_date_field
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Wizardry
2
+ module Questions
3
+ # Ask a respondant for an email address
4
+ class EmailAddress < Wizardry::Questions::Answer
5
+ def initialize(name)
6
+ Rails.logger.debug("🧙 Adding email address question '#{name}'")
7
+ super
8
+ end
9
+
10
+ def form_method
11
+ :govuk_email_field
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ module Wizardry
2
+ module Questions
3
+ class Hidden < Answer
4
+ attr_reader :name
5
+
6
+ def initialize(name, value)
7
+ Rails.logger.debug("🧙 Adding hidden field '#{name}'")
8
+ @value = value
9
+
10
+ super(name)
11
+ end
12
+
13
+ def form_method
14
+ :hidden_field
15
+ end
16
+
17
+ def extra_kwargs
18
+ { value: @value }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Wizardry
2
+ module Questions
3
+ # Ask a question that can be answered with multiple lines of text
4
+ class LongAnswer < Wizardry::Questions::Answer
5
+ def initialize(name)
6
+ Rails.logger.debug("🧙 Adding long question '#{name}'")
7
+ super
8
+ end
9
+
10
+ def form_method
11
+ :govuk_text_area
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ module Wizardry
2
+ module Questions
3
+ class Radios < Wizardry::Questions::Answer
4
+ attr_reader :options
5
+
6
+ RadioOption = Struct.new(:value, :label, keyword_init: true)
7
+
8
+ def initialize(name, options)
9
+ Rails.logger.debug("🧙 Adding radios '#{name}' with options #{options}")
10
+
11
+ @options = options
12
+
13
+ super(name)
14
+ end
15
+
16
+ def form_method
17
+ :govuk_collection_radio_buttons
18
+ end
19
+
20
+ def extra_args
21
+ [build_options, :value, :label]
22
+ end
23
+
24
+ def build_options
25
+ case options
26
+ when Array
27
+ options.map { |v| Wizardry::Questions::Radios::RadioOption.new(value: v, label: v) }
28
+ when Hash
29
+ options.map { |k, v| Wizardry::Questions::Radios::RadioOption.new(value: k, label: v) }
30
+ else
31
+ fail ArgumentError, "Options must be an Hash or Array"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ module Wizardry
2
+ module Questions
3
+ # Ask a question that can be answered with one line of text
4
+ class ShortAnswer < Wizardry::Questions::Answer
5
+ def initialize(name)
6
+ Rails.logger.debug("🧙 Adding short question '#{name}'")
7
+ super(name)
8
+ end
9
+
10
+ def form_method
11
+ :govuk_text_field
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Wizardry
2
+ module Questions
3
+ # Ask a respondant for a telephone number
4
+ class TelephoneNumber < Wizardry::Questions::Answer
5
+ def initialize(name)
6
+ Rails.logger.debug("🧙 Adding telephone number question '#{name}'")
7
+ super
8
+ end
9
+
10
+ def form_method
11
+ :govuk_phone_field
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module Wizardry
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ module Wizardry
2
+ module Routing
3
+ class NextPage
4
+ attr_reader :name, :condition, :label
5
+
6
+ def initialize(name, condition = nil, label: nil)
7
+ @name = name
8
+ @condition = condition
9
+ @label = label
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Wizardry
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/lib/wizardry.rb ADDED
@@ -0,0 +1,115 @@
1
+ require "wizardry/engine"
2
+ require "wizardry/railtie"
3
+ require "wizardry/framework"
4
+ require "wizardry/instance"
5
+
6
+ require "wizardry/pages/page"
7
+ require "wizardry/pages/question_page"
8
+ require "wizardry/pages/check_your_answers_page"
9
+ require "wizardry/pages/completion_page"
10
+
11
+ require "wizardry/questions/answer"
12
+ require "wizardry/questions/short_answer"
13
+ require "wizardry/questions/long_answer"
14
+ require "wizardry/questions/radios"
15
+ require "wizardry/questions/telephone_number"
16
+ require "wizardry/questions/email_address"
17
+ require "wizardry/questions/date"
18
+ require "wizardry/questions/hidden"
19
+
20
+ require "wizardry/routing/next_page"
21
+
22
+ require "govuk_design_system_formbuilder"
23
+ require "govuk/components"
24
+
25
+ module Wizardry
26
+ extend ActiveSupport::Concern
27
+
28
+ class AlreadyCompletedError < StandardError; end
29
+
30
+ class_methods do
31
+ def wizard(...)
32
+ define_method(:wizard) do
33
+ @framework ||= Wizardry::Framework.new(...)
34
+ end
35
+ end
36
+ end
37
+
38
+ included do
39
+ before_action :setup_wizard, :check_wizard
40
+
41
+ def edit
42
+ Rails.logger.debug("🧙 Running before_edit callback")
43
+ @wizard.current_page.before_edit!(@wizard.object)
44
+ end
45
+
46
+ def update
47
+ Rails.logger.debug("🧙 Object valid, saving and moving on")
48
+ @wizard.object.assign_attributes(object_params.merge(last_completed_step_params))
49
+
50
+ Rails.logger.debug("🧙 Running before_update callback")
51
+ @wizard.current_page.before_update!(@wizard.object)
52
+
53
+ if @wizard.object.valid?(@wizard.current_page.name)
54
+ @wizard.object.transaction do
55
+ @wizard.object.save!
56
+ Rails.logger.debug("🧙 Object saved, trying after_update callback")
57
+
58
+ @wizard.current_page.after_update!(@wizard.object)
59
+ Rails.logger.debug("🧙 Object saved and callbacks run, moving on")
60
+
61
+ finalize_object if @wizard.complete?
62
+ end
63
+
64
+ redirect_to send(@wizard.framework.edit_path_helper, @wizard.next_page.name.to_s.dasherize)
65
+ else
66
+ Rails.logger.debug("🧙 Object not valid, try again")
67
+
68
+ render :edit
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def finalize_object(finalize: :finalize!)
75
+ Rails.logger.debug("🧙 Wizard complete, finalizing object")
76
+
77
+ if @wizard.object.respond_to?(finalize)
78
+ @wizard.object.send(finalize)
79
+ Rails.logger.debug("🧙 Wizard object finalized")
80
+ else
81
+ Rails.logger.warn("🧙 Wizard object has no #{finalize} method")
82
+ end
83
+ end
84
+
85
+ def check_wizard
86
+ @wizard.ensure_not_complete unless @wizard.current_page.is_a?(Wizardry::Pages::CompletionPage)
87
+ end
88
+
89
+ def setup_wizard
90
+ Rails.logger.debug("🧙 Finding or initialising #{wizard.class_name} with '#{identifier}'")
91
+
92
+ object = wizard.class_name.constantize.find_or_initialize_by(identifier: identifier)
93
+
94
+ Rails.logger.debug("🧙 Initialising the wizard 🪄")
95
+
96
+ @wizard = Wizardry::Instance.new(current_page: params[:page].underscore, object: object, framework: @framework)
97
+ end
98
+
99
+ def object_params
100
+ param_key = @wizard.framework.class_name.constantize.model_name.param_key
101
+
102
+ params.require(param_key).permit(@wizard.current_page.question_names)
103
+ rescue ActionController::ParameterMissing
104
+ { param_key => last_completed_step_params }
105
+ end
106
+
107
+ def last_completed_step_params
108
+ { last_completed_step: @wizard.current_page.name }
109
+ end
110
+
111
+ def identifier
112
+ cookies.fetch(wizard.cookie_name) { cookies[wizard.cookie_name] = SecureRandom.uuid }
113
+ end
114
+ end
115
+ end
metadata ADDED
@@ -0,0 +1,251 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: govuk-wizardry
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Yates
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: govuk-components
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.1.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.1.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: govuk_design_system_formbuilder
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.7.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.7.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 7.0.0a2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 7.0.0a2
55
+ - !ruby/object:Gem::Dependency
56
+ name: capybara
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rails-controller-testing
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec-expectations
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec-rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-govuk
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: shoulda-matchers
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: sqlite3
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: webrick
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ description: A gem that allows an entire GOV.UK wizard/journey to be easily defined
196
+ and built
197
+ email:
198
+ - peter.yates@graphia.co.uk
199
+ executables: []
200
+ extensions: []
201
+ extra_rdoc_files: []
202
+ files:
203
+ - MIT-LICENSE
204
+ - README.md
205
+ - Rakefile
206
+ - app/helpers/wizardry_helper.rb
207
+ - lib/tasks/wizardry_tasks.rake
208
+ - lib/wizardry.rb
209
+ - lib/wizardry/engine.rb
210
+ - lib/wizardry/framework.rb
211
+ - lib/wizardry/graph.rb
212
+ - lib/wizardry/instance.rb
213
+ - lib/wizardry/pages/check_your_answers_page.rb
214
+ - lib/wizardry/pages/completion_page.rb
215
+ - lib/wizardry/pages/page.rb
216
+ - lib/wizardry/pages/question_page.rb
217
+ - lib/wizardry/questions/answer.rb
218
+ - lib/wizardry/questions/date.rb
219
+ - lib/wizardry/questions/email_address.rb
220
+ - lib/wizardry/questions/hidden.rb
221
+ - lib/wizardry/questions/long_answer.rb
222
+ - lib/wizardry/questions/radios.rb
223
+ - lib/wizardry/questions/short_answer.rb
224
+ - lib/wizardry/questions/telephone_number.rb
225
+ - lib/wizardry/railtie.rb
226
+ - lib/wizardry/routing/next_page.rb
227
+ - lib/wizardry/version.rb
228
+ homepage: https://www.github.com/graphia/wizardry
229
+ licenses:
230
+ - MIT
231
+ metadata: {}
232
+ post_install_message:
233
+ rdoc_options: []
234
+ require_paths:
235
+ - lib
236
+ required_ruby_version: !ruby/object:Gem::Requirement
237
+ requirements:
238
+ - - ">="
239
+ - !ruby/object:Gem::Version
240
+ version: '0'
241
+ required_rubygems_version: !ruby/object:Gem::Requirement
242
+ requirements:
243
+ - - ">="
244
+ - !ruby/object:Gem::Version
245
+ version: '0'
246
+ requirements: []
247
+ rubygems_version: 3.2.22
248
+ signing_key:
249
+ specification_version: 4
250
+ summary: GOV.UK design system wizard generator
251
+ test_files: []