Skip to content

Commit bf43277

Browse files
authored
Merge pull request #2838 from govuk-forms/backward-route-validate
Backward route validate
2 parents 203da13 + 345256d commit bf43277

14 files changed

Lines changed: 339 additions & 32 deletions

File tree

app/components/page_list_component/error_summary/view.rb

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ def self.error_id(number)
1010
"condition_#{number}"
1111
end
1212

13+
def self.page_error_id(page)
14+
"page-#{page.id}"
15+
end
16+
1317
def self.generate_error_message(error_name, condition:, page:)
1418
# TODO: route_number is hardcoded as 1 here because we know there can be only two conditions. It will need to change in future
1519
# https://trello.com/c/BfkZEIgM/3446-set-route-count-dynamically-instead-of-hard-coding-it
@@ -24,6 +28,18 @@ def self.generate_error_message(error_name, condition:, page:)
2428
I18n.t(defaults.first, default: defaults.drop(1), scope:, **interpolation_variables)
2529
end
2630

31+
def self.multiple_branch_error_message(page)
32+
page_errors = page.routing_conditions.filter(&:warning_goto_page_before_routing_page)
33+
34+
return if page_errors.blank?
35+
36+
if Forms::RoutesInput.route_with_selection_options?(page)
37+
I18n.t("errors.routes.page_list.cannot_route_backwards_from_selection_page", question_number: page.position, count: page_errors.count)
38+
else
39+
I18n.t("errors.routes.page_list.cannot_route_backwards_from_generic_page", question_number: page.position)
40+
end
41+
end
42+
2743
def error_object(error_name:, condition:, page:)
2844
OpenStruct.new(
2945
message: self.class.generate_error_message(error_name, condition:, page:),
@@ -32,15 +48,28 @@ def error_object(error_name:, condition:, page:)
3248
end
3349

3450
def errors_for_summary
35-
@form.conditions.map { |condition|
36-
condition.validation_errors.map do |error|
37-
error_object(
38-
error_name: error.name,
39-
page: condition.check_page,
40-
condition: condition,
51+
if FeatureService.new(group: @form.group).enabled?(:multiple_branches)
52+
@form.pages.map { |page|
53+
error_message = self.class.multiple_branch_error_message(page)
54+
55+
next if error_message.blank?
56+
57+
OpenStruct.new(
58+
message: error_message,
59+
link: "##{self.class.page_error_id(page)}",
4160
)
42-
end
43-
}.flatten
61+
}.compact
62+
else
63+
@form.conditions.map { |condition|
64+
condition.validation_errors.map do |error|
65+
error_object(
66+
error_name: error.name,
67+
page: condition.check_page,
68+
condition: condition,
69+
)
70+
end
71+
}.flatten
72+
end
4473
end
4574
end
4675
end

app/components/page_list_component/view.html.erb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,21 @@
3333
</div>
3434

3535
<% if FeatureService.new(group: @form.group).enabled?(:multiple_branches) && page.routing_conditions.present? %>
36-
<div class="govuk-summary-list__row">
36+
<% error_message = PageListComponent::ErrorSummary::View.multiple_branch_error_message(page) %>
37+
<div id="<%= PageListComponent::ErrorSummary::View.page_error_id(page) %>" class="govuk-summary-list__row app-page-list__row <%= class_names( "govuk-form-group--error": error_message.present?) %>">
3738
<dt class="govuk-summary-list__key app-page-list__key">
3839
<%= t("page_conditions.condition_list_key", count: page.routing_conditions.count, question_number: page.position) %>
3940
</dt>
4041
<dd class="govuk-summary-list__value">
42+
43+
<% if error_message.present? %>
44+
<ul class="govuk-list govuk-!-margin-0">
45+
<li class="app-page_list__route-text--error">
46+
<%= error_message %>
47+
</li>
48+
</ul>
49+
<% end %>
50+
4151
<% if page.routing_conditions.first.answer_value.present? %>
4252
<% answer_value_groups(page).each do |_, group| %>
4353
<p><%= condition_group_description(group) %></p>

app/controllers/routes_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class RoutesController < FormsController
55
def show
66
authorize current_form, :can_view_form?
77
@routes_input = Forms::RoutesInput.new(form: form_with_pages_and_conditions).assign_form_values
8+
@routes_input.validate
89
end
910

1011
def create

app/input_objects/forms/route_input.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ class Forms::RouteInput < BaseInput
99
attribute :goto
1010
attribute :answer_value
1111

12-
attr_accessor :page, :goto_options, :label
12+
attr_accessor :page, :goto_page, :goto_options, :label
13+
14+
validate :route_is_not_backwards
1315

1416
def goes_to_default_next_page?
1517
goto == DEFAULT_VALUE
@@ -28,4 +30,27 @@ def condition_attributes
2830
{ goto_page_id: goto, skip_to_end: false, check_page_id: page.id }
2931
end
3032
end
33+
34+
private
35+
36+
def route_is_not_backwards
37+
return if skippable_for_backwards_validation?
38+
39+
return if goto_page.position > page.position
40+
41+
errors.add(:goto, error_message_for_backwards_route)
42+
end
43+
44+
def skippable_for_backwards_validation?
45+
goes_to_default_next_page? || goes_to_end_of_form? || goto_page.nil?
46+
end
47+
48+
def error_message_for_backwards_route
49+
if Forms::RoutesInput.route_with_selection_options?(page)
50+
option_index = page.answer_settings.selection_options.find_index { |option| option["value"] == answer_value }
51+
I18n.t("errors.routes.cannot_route_backwards_from_selection_page", question_number: page.position, option_number: option_index + 1)
52+
else
53+
I18n.t("errors.routes.cannot_route_backwards_from_generic_page", question_number: page.position)
54+
end
55+
end
3156
end

app/input_objects/forms/routes_input.rb

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
class Forms::RoutesInput < BaseInput
2+
include ActionView::Helpers::FormTagHelper
3+
24
attr_accessor :form, :routes
35

6+
validate :routes_are_valid
7+
48
def self.too_many_selection_options?(page)
59
page.answer_settings["selection_options"].length > 10
610
end
711

12+
def self.route_with_selection_options?(page)
13+
page.answer_type == "selection" && page.answer_settings.only_one_option == "true" && !Forms::RoutesInput.too_many_selection_options?(page)
14+
end
15+
816
def initialize(attributes = {})
917
@form = attributes.delete(:form)
1018
super
1119
end
1220

1321
def submit
14-
# TODO: Add validations - this is here so we don't forget it but the model
15-
# can't be invalid yet
1622
return false if invalid?
1723

1824
Routes::SyncService.new(form:, routes:).sync_conditions_from_routes
@@ -36,12 +42,30 @@ def routes_attributes=(attributes)
3642
page = pages_by_id[route_attrs["page_id"].to_i]
3743
next unless page # Skip if page not found or doesn't belong to form
3844

45+
goto_page = pages_by_id[route_attrs["goto"].to_i]
46+
3947
Forms::RouteInput.new(
4048
route_attrs.symbolize_keys.merge(
4149
page:,
50+
goto_page:,
4251
goto_options: route_build_service.options_for_goto_page(page, route_attrs["goto"]),
4352
),
4453
)
4554
}.compact
4655
end
56+
57+
def routes_are_valid
58+
return if routes.nil?
59+
60+
routes.each.with_index do |route, index|
61+
next if route.valid?
62+
63+
route.errors.each do |error|
64+
attribute_key = "routes_attributes[#{index}][#{error.attribute}]"
65+
error_field_id = field_id(:forms_routes_input_routes_attributes, error.attribute, index: index)
66+
67+
errors.add(attribute_key, error.message, url: "##{error_field_id}")
68+
end
69+
end
70+
end
4771
end

app/services/routes/build_service.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ def build_routes
1717
end
1818

1919
form.pages.flat_map do |page|
20-
if page.answer_type == "selection" &&
21-
page.answer_settings.only_one_option == "true" &&
22-
!Forms::RoutesInput.too_many_selection_options?(page)
20+
if Forms::RoutesInput.route_with_selection_options?(page)
2321
build_routes_for_selection_page(page, conditions_by_key)
2422
else
2523
build_route_for_generic_page(page, conditions_by_key)
@@ -75,6 +73,7 @@ def build_routes_for_selection_page(page, conditions_by_key)
7573
page:,
7674
answer_value:,
7775
goto: goto_value_for(condition),
76+
goto_page: condition&.goto_page,
7877
goto_options: options_for_goto_page(page, condition&.goto_page_id),
7978
label: { text: "If option #{index} (#{answer_value_label}), go to:" },
8079
)
@@ -91,6 +90,7 @@ def build_route_for_generic_page(page, conditions_by_key)
9190
page_id: page.id,
9291
page:,
9392
goto: goto_value_for(condition),
93+
goto_page: condition&.goto_page,
9494
goto_options: options_for_goto_page(page, condition&.goto_page_id),
9595
label: { text: "After question #{page.position}, go to:" },
9696
),

app/views/pages/index.html.erb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
<div class="govuk-grid-row">
66
<div class="govuk-grid-column-two-thirds">
7-
<% unless FeatureService.new(group: current_form.group).enabled?(:multiple_branches) %>
8-
<%= render PageListComponent::ErrorSummary::View.new(current_form) %>
9-
<% end %>
7+
<%= render PageListComponent::ErrorSummary::View.new(current_form) %>
108

119
<h1 class="govuk-heading-l govuk-!-margin-bottom-2">
1210
<span class="govuk-caption-l"><%= current_form.name %></span><span class="govuk-visually-hidden"> - </span>

app/views/routes/show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
route_form.hidden_field(:id) +
3434
route_form.hidden_field(:page_id) +
3535
route_form.hidden_field(:answer_value) +
36-
route_form.govuk_select(:goto, options_for_select(route_form.object.goto_options, route_form.object.goto), label: route_form.object.label)
36+
route_form.govuk_select(:goto, options_for_select(route_form.object.goto_options, route_form.object.goto), label: route_form.object.label, id: route_form.field_id(:goto))
3737
end %>
3838
</li>
3939
<% end %>

config/locales/en.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,14 @@ en:
365365
goto_page_doesnt_exist: The question you’re taking the person to no longer exists - edit question %{question_number}’s route
366366
route_number_for_any_other_answer: for any other answer
367367
prefix: 'Error:'
368+
routes:
369+
cannot_route_backwards_from_generic_page: The route from question %{question_number} cannot go to a previous question - edit this route
370+
cannot_route_backwards_from_selection_page: The route from question %{question_number}, option %{option_number} cannot go to a previous question - edit this route
371+
page_list:
372+
cannot_route_backwards_from_generic_page: The route from question %{question_number} cannot go to a previous question - edit this route
373+
cannot_route_backwards_from_selection_page:
374+
one: A route from question %{question_number} is going to a previous question - edit this route
375+
other: Routes from question %{question_number} are going to a previous question - edit this question’s routes
368376
exit_page:
369377
no_content_added_html: "<p>No content added</p>"
370378
feedback_link:

spec/components/page_list_component/error_summary/error_summary_component_preview.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ class PageListComponent::ErrorSummary::ErrorSummaryComponentPreview < ViewCompon
22
include FactoryBot::Syntax::Methods
33

44
def default
5-
form = build(:form, id: 1, pages: [])
5+
form = build(:form, :with_group, id: 1, pages: [])
66

77
render(PageListComponent::ErrorSummary::View.new(form))
88
end
@@ -13,7 +13,7 @@ def error_component_without_errors
1313
pages = [(build :page, id: 1, position: 1, question_text: "Enter your name", routing_conditions:),
1414
(build :page, id: 2, position: 2, question_text: "What is your pet's phone number?", routing_conditions: []),
1515
(build :page, id: 3, position: 3, question_text: "How many pets do you own?", routing_conditions: [])]
16-
form = build(:form, id: 0, pages:)
16+
form = build(:form, :with_group, id: 0, pages:)
1717

1818
# We need to build the records rather than create them so that we don't save them to the database when we view the
1919
# preview. However, this means that the associations aren't available so we need to manually set the associations
@@ -35,7 +35,7 @@ def error_component_with_errors
3535
pages = [(build :page, id: 1, position: 1, question_text: "Enter your name", routing_conditions: routing_conditions_page_1),
3636
(build :page, id: 2, position: 2, question_text: "What is your pet's phone number?", routing_conditions: routing_conditions_page_2),
3737
(build :page, id: 3, position: 3, question_text: "How many pets do you own?", routing_conditions: [])]
38-
form = build(:form, id: 0, pages:)
38+
form = build(:form, :with_group, id: 0, pages:)
3939

4040
# We need to build the records rather than create them so that we don't save them to the database when we view the
4141
# preview. However, this means that the associations aren't available so we need to manually set the associations

0 commit comments

Comments
 (0)