Skip to content

Commit c2a8ed8

Browse files
H3ll3m4njseetochrispymm
authored
Conditional content (#355)
* add method - not used yet * wip: rename fixture for some reason having an underscore means the editor can not find it * add contains to expression_item definitions * Adding show options and defaults in schemas for conditional content * Update schema for show * update show key for content component to display to avoid conflict with definition.block * update default metadata to display too * Fixing conditional evaluator logic * Adding a fixture for conditional content to fix some tests * Remove the new conditional content feature and reference the good one * Linting * Make display schema key optional for backwards ccompatibility * Add conditional api path to component dataset * Call to evaluate content conditional from engine controller * Handling of conditional content logic in the presenter view * Handling several conditional component per page * Handling display metadata to filter always or never display * Renaming for clarity * Removing unused method * Fixing old conditional tests * Fixing old engine controller tests * Linting * Adding tests for evaluate content conditional class * Removing previous automated tests from previous implementation * Adding correct test fixture * Linting * Adding some tests for evaluate content component * dummy commit * Adding tests for evaluate content conditional * Getting all components by default as always * Adding more tests and removing dummy commit * Refactoring conditional component methods moved to page model * Adding handling of AND rule for conditional content * Adding handling for contains and does not contains * Simplifying evaluate conditional content class * Linting * Refactoring conditional content evaluation in its class * Making sure it is displayed if in editable mode and linting * Update conditional.json text fixture * Refactoring * Updating tests for conditional content evaluator * Updating logic to handle or rule - part 1 before refactoring * Handling combinations of AND and OR rules * Linting * Checking all the answers * Handling the case where we preview only one page and we show all conditional content but inform the user * Using notification banner govuk design * Updating notification message and using locales * Showing the notification banner only once on the content page * Refactor to move the logic in the page controller and create a single page preview page check * Refactor and reduce lines by removing the HTML * Conditional content deletion prevention additions (#354) * Add content_expressions and content_conditionals methods to service * Add conditional content fixture * Fix page spec after merging * Only show the notification banner in single page preview, not in the editor mode * Show conditional content when preview a single page * Adding conditional content notification banner for all content on single preview page * Conditional content notification banner appears twice on cya so move this on all of pages * Refactor conditional content banner into partial * Notification banner should show only for show if and never conditional content * Adjust page_with_component to use all components * Show notification banner only when conditional content is present on the page * Ensure legacy content components without display attribute get shown * Add methods for returning pages that contain uuids used in conditional content expressions * include extra components when detemrtining if there are content components present * Don't show conditional notification in runner * Fix display when in runner * Update copy for conditional content notification banner * Removing unused methods * Add more service model unit tests * Bumping up version prior release --------- Co-authored-by: Natalie Seeto <[email protected]> Co-authored-by: Chris Pymm <[email protected]>
1 parent 67fef4c commit c2a8ed8

27 files changed

+2498
-672
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ All notable changes to this project will be documented in this file.
77
### Changed
88
### Fixed
99

10+
## [3.2.11] - 2023-10-23
11+
### Changed
12+
- Updated the presenter logic for conditional content
13+
- Added tests and tests fixtures for unit tests and editor acceptance tests
14+
1015
## [3.2.10] - 2023-08-24
1116
### Fixed
1217
- Fixed an issue causing errors when resuming progress

app/controllers/metadata_presenter/engine_controller.rb

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -113,35 +113,6 @@ def external_or_relative_link(link)
113113
end
114114
helper_method :external_or_relative_link
115115

116-
def load_conditional_content
117-
if @page.content_component_present?
118-
if components_without_conditionals(@page).present?
119-
return components_without_conditionals(@page) + show_components(@page).compact
120-
end
121-
122-
show_components(@page).compact
123-
end
124-
end
125-
helper_method :load_conditional_content
126-
127-
def components_without_conditionals(page)
128-
page.content_components.select { |component|
129-
component.conditionals.blank?
130-
}.map(&:uuid)
131-
end
132-
133-
def show_components(page)
134-
return @page.content_components.map(&:uuid) if editor_preview?
135-
136-
page.content_components.map do |content_component|
137-
EvaluateContentConditionals.new(
138-
service:,
139-
component: content_component,
140-
user_data: load_user_data
141-
).show_component
142-
end
143-
end
144-
145116
private
146117

147118
def not_found
@@ -180,5 +151,18 @@ def show_maintenance_page
180151
def editor_preview?
181152
URI(request.original_url).path.split('/').include?('preview')
182153
end
154+
155+
def single_page_preview?
156+
return false if in_runner?
157+
return true if request.referrer.blank?
158+
159+
!URI(request.referrer).path.split('/').include?('preview')
160+
end
161+
helper_method :single_page_preview?
162+
163+
def in_runner?
164+
::Rails.application.class.module_parent.name == 'FbRunner'
165+
end
166+
helper_method :in_runner?
183167
end
184168
end

app/controllers/metadata_presenter/pages_controller.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ def show
88
@page ||= service.find_page_by_url(request.env['PATH_INFO'])
99
if @page
1010
load_autocomplete_items
11-
load_conditional_content
11+
if single_page_preview?
12+
@page.load_all_content
13+
else
14+
@page.load_conditional_content(service, @user_data)
15+
end
1216

1317
@page_answers = PageAnswers.new(@page, @user_data)
1418
render template: @page.template
@@ -35,6 +39,11 @@ def answered_pages
3539
TraversedPages.new(service, load_user_data, @page).all
3640
end
3741

42+
def conditional_components_present?
43+
@page.conditional_components.any?
44+
end
45+
helper_method :conditional_components_present?
46+
3847
private
3948

4049
def set_caching_header

app/models/metadata_presenter/component.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ def conditionals
8888
end
8989
end
9090

91+
def find_conditional_content_by_uuid(uuid)
92+
metadata['conditionals'].select do |conditional|
93+
conditional['_uuid'] == uuid
94+
end
95+
end
96+
9197
private
9298

9399
def validation_bundle_key
Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,81 @@
11
module MetadataPresenter
22
class EvaluateContentConditionals
33
include ActiveModel::Model
4-
attr_accessor :service, :component, :user_data
4+
attr_accessor :service, :user_data
55

6-
def show_component
7-
component_uuid
6+
def evaluate_content_components(page)
7+
displayed_components = []
8+
page.content_components.map do |content_component|
9+
next if page.conditionals_uuids_by_type('never').include?(content_component.uuid)
10+
11+
if page.conditionals_uuids_by_type('always').include?(content_component.uuid)
12+
displayed_components << content_component.uuid
13+
end
14+
if page.conditionals_uuids_by_type('conditional').include?(content_component.uuid)
15+
displayed_components << uuid_to_include?(content_component)
16+
end
17+
end
18+
displayed_components.compact
819
end
920

10-
def component_uuid
11-
@results ||=
12-
conditionals.map do |conditional|
13-
evaluated_expressions = conditional.expressions.map do |expression|
14-
expression.service = service
15-
16-
Operator.new(
17-
expression.operator
18-
).evaluate(
19-
expression.field_label,
20-
user_data[expression.expression_component.id]
21-
)
22-
end
21+
private
2322

24-
if conditional.type == 'or' && evaluated_expressions.any?
25-
component.uuid
26-
elsif evaluated_expressions.all?
27-
component.uuid
23+
def uuid_to_include?(candidate_component)
24+
evaluation_conditions = []
25+
if candidate_component.conditionals.count == 1
26+
# if there are only and rules or the particular case where there is only one condition
27+
evaluation_conditions << evaluate_condition(candidate_component.conditionals[0])
28+
return candidate_component.uuid if evaluation_conditions.flatten.all?
29+
elsif candidate_component.conditionals.count > 1
30+
# if there are or rule
31+
if candidate_component.conditionals[0][:_type] == 'and'
32+
# if there are 'and' in conditions between the 'or' rule
33+
candidate_component.conditionals.map do |conditional|
34+
evaluation_conditions << evaluate_condition(conditional).flatten.all?
2835
end
36+
return candidate_component.uuid if evaluation_conditions.flatten.any?
2937
end
30-
@results.flatten.compact.first
38+
if candidate_component.conditionals[0][:_type] == 'if'
39+
# then this is an 'or' condition between conditionals
40+
candidate_component.conditionals.map do |conditional|
41+
evaluation_conditions << evaluate_condition(conditional)
42+
end
43+
return candidate_component.uuid if evaluation_conditions.flatten.any?
44+
end
45+
end
3146
end
3247

33-
delegate :conditionals, to: :component
48+
def evaluate_condition(conditional)
49+
evaluation_expressions = []
50+
conditional.expressions.map do |expression|
51+
expression.service = service
52+
case expression.operator
53+
when 'is'
54+
evaluation_expressions << check_answer_equal_expression(expression, user_data)
55+
when 'is_not'
56+
evaluation_expressions << !check_answer_equal_expression(expression, user_data)
57+
when 'is_answered'
58+
evaluation_expressions << user_data[expression.expression_component.id].present?
59+
when 'is_not_answered'
60+
evaluation_expressions << user_data[expression.expression_component.id].blank?
61+
when 'contains'
62+
evaluation_expressions << check_answer_include_expression(expression, user_data)
63+
when 'does_not_contain'
64+
evaluation_expressions << !check_answer_include_expression(expression, user_data)
65+
end
66+
end
67+
evaluation_expressions
68+
end
69+
70+
def check_answer_equal_expression(expression, user_data)
71+
expression.field_label == user_data[expression.expression_component.id]
72+
end
73+
74+
def check_answer_include_expression(expression, user_data)
75+
answer = user_data[expression.expression_component.id]
76+
return false if answer.blank?
77+
78+
answer.include?(expression.field_label)
79+
end
3480
end
3581
end

app/models/metadata_presenter/page.rb

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,37 @@ def placeholders
141141
end
142142
end
143143

144-
def content_component_present?
145-
components.any?(&:content?)
144+
def conditional_content_to_show
145+
@conditional_content_to_show || []
146+
end
147+
148+
def show_conditional_component?(component_id)
149+
conditional_content_to_show.include?(component_id)
150+
end
151+
152+
def load_conditional_content(service, user_data)
153+
if content_component_present?
154+
evaluator = EvaluateContentConditionals.new(service:, user_data:)
155+
items = evaluator.evaluate_content_components(self)
156+
items << legacy_content_components
157+
assign_conditional_component(items.flatten)
158+
end
159+
end
160+
161+
def content_components_by_type(*display)
162+
content_components.filter { |component| display.include? component[:display] }
163+
end
164+
165+
def conditionals_uuids_by_type(*display)
166+
content_components_by_type(*display).map { |component| component[:_uuid] }
167+
end
168+
169+
def load_all_content
170+
@conditional_content_to_show = content_components
171+
end
172+
173+
def conditional_components
174+
conditionals_uuids_by_type('never', 'conditional')
146175
end
147176

148177
private
@@ -172,5 +201,17 @@ def supported_components(page_type)
172201
def raw_type
173202
type.gsub('page.', '')
174203
end
204+
205+
def assign_conditional_component(items)
206+
@conditional_content_to_show = items
207+
end
208+
209+
def content_component_present?
210+
all_components.any?(&:content?)
211+
end
212+
213+
def legacy_content_components
214+
content_components.filter_map { |component| component[:_uuid] if component[:display].blank? }
215+
end
175216
end
176217
end

app/models/metadata_presenter/service.rb

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def conditionals
2121
branches.map(&:conditionals).flatten
2222
end
2323

24+
def content_expressions
25+
content_conditionals.flat_map(&:expressions)
26+
end
27+
28+
def content_conditionals
29+
pages.flat_map(&:content_components).flat_map(&:conditionals)
30+
end
31+
2432
def flow_object(uuid)
2533
MetadataPresenter::Flow.new(uuid, metadata.flow[uuid])
2634
rescue StandardError
@@ -78,23 +86,32 @@ def no_back_link?(current_page)
7886

7987
def page_with_component(uuid)
8088
pages.find do |page|
81-
Array(page.components).any? { |component| component.uuid == uuid }
89+
Array(page.all_components).any? { |component| component.uuid == uuid }
8290
end
8391
end
8492

85-
private
93+
def pages_with_conditional_content_for_page(uuid)
94+
pages.select do |page|
95+
uuid.in? page.content_components_by_type('conditional').flat_map(&:conditionals).flat_map(&:expressions).map(&:page)
96+
end
97+
end
8698

87-
def all_pages
88-
@all_pages ||= pages + standalone_pages
99+
def pages_with_conditional_content_for_question(uuid)
100+
pages.select do |page|
101+
uuid.in? page.content_components_by_type('conditional').flat_map(&:conditionals).flat_map(&:expressions).map(&:component)
102+
end
89103
end
90104

91-
def flow_page(current_page)
92-
page_index = pages.index(current_page)
93-
pages[page_index - 1] if page_index.present?
105+
def pages_with_conditional_content_for_question_option(uuid)
106+
pages.select do |page|
107+
uuid.in? page.content_components_by_type('conditional').flat_map(&:conditionals).flat_map(&:expressions).map(&:field)
108+
end
94109
end
95110

96-
def referrer_page(referrer)
97-
find_page_by_url(URI(referrer).path) if referrer
111+
private
112+
113+
def all_pages
114+
@all_pages ||= pages + standalone_pages
98115
end
99116

100117
def strip_slash(url)

app/views/metadata_presenter/component/_components.html.erb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<% components.each_with_index do |component, index| %>
2+
23
<% if component.type == 'content' %>
4+
<% next unless (editable? || @page.show_conditional_component?(component.uuid) || single_page_preview?) %>
5+
36
<editable-content id="<%= "page[#{component.collection}[#{index}]]" %>"
47
class="fb-editable govuk-!-margin-top-8"
58
type="<%= component.type %>"
69
default-content="<%= default_text('content') %>"
710
content="<%= component.content %>"
811
data-fb-content-id="<%= "page[#{component.collection}[#{index}]]" %>"
9-
data-config="<%= component.to_json %>">
12+
data-config="<%= component.to_json %>"
13+
data-conditional-api-path="<%= editable? ? URI.decode_www_form_component(api_service_edit_conditional_content_path(service.service_id, component.uuid)) : '' %>">
1014
<%= to_html(component.content) %>
1115
</editable-content>
1216
<% end %>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<% if single_page_preview? && !editable? && @page.conditional_components.any? %>
2+
<%= govuk_notification_banner(title_text: 'Important') do |nb|
3+
nb.with_heading(text: t('presenter.conditional_content.notification'))
4+
end %>
5+
<% end%>

app/views/metadata_presenter/page/checkanswers.html.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<div class="govuk-grid-row">
33
<div class="govuk-grid-column-two-thirds">
44

5+
<%= render partial:'metadata_presenter/component/conditional_component_banner'%>
6+
57
<h1 class="fb-editable govuk-heading-xl"
68
data-fb-content-type="element"
79
data-fb-content-id="page[heading]"
@@ -11,6 +13,7 @@
1113

1214
<%= form_for @page, url: reserved_submissions_path, authenticity_token: false, html: { id: 'answers-form' } do |f| %>
1315
<%= hidden_field_tag :authenticity_token, form_authenticity_token -%>
16+
1417
<%= render partial: 'metadata_presenter/component/components',
1518
locals: {
1619
f: f,

0 commit comments

Comments
 (0)