govuk-wizardry 0.1.0

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 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: []