Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion app/controllers/pages/exit_pages_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Pages::ExitPagesController < PagesController
before_action :check_multiple_branches_enabled
before_action :check_user_has_permission
before_action :check_multiple_branches_enabled
before_action :check_page_can_have_exit_pages

def new
exit_page_input = Pages::ExitPageInput.new(page: page)
Expand All @@ -18,6 +19,46 @@ def create
end
end

def edit
exit_page_input = Pages::UpdateExitPageInput.new(exit_page:).assign_exit_page_values

render locals: { exit_page_input:, preview_html: preview_html(exit_page_input), check_preview_validation: false }
end

def update
exit_page_input = Pages::UpdateExitPageInput.new(update_exit_page_input_params.merge(exit_page:))

if exit_page_input.submit
redirect_to routes_path(form_id: current_form.id), success: t("banner.success.exit_page_saved")
else
render :edit, locals: { exit_page_input:, preview_html: preview_html(exit_page_input), check_preview_validation: true }, status: :unprocessable_content
end
end

def delete
delete_confirmation_input = Pages::DeleteExitPageInput.new

render locals: { delete_confirmation_input:, page:, exit_page: }
end

def destroy
delete_confirmation_input = Pages::DeleteExitPageInput.new(params.require(:pages_delete_exit_page_input).permit(:confirm))

unless delete_confirmation_input.valid?
return render :delete, locals: { delete_confirmation_input:, page:, exit_page: }, status: :unprocessable_content
end

unless delete_confirmation_input.confirmed?
return redirect_to edit_exit_page_path(@current_form.id, page.id, exit_page.id)
end

current_form.save_question_changes! do
exit_page.destroy!
end

redirect_to routes_path(@current_form.id), success: t("banner.success.exit_page_deleted")
end

def render_preview
exit_page_input = Pages::ExitPageInput.new(markdown: params[:markdown])
exit_page_input.validate if params[:check_preview_validation] == "true"
Expand All @@ -27,10 +68,18 @@ def render_preview

private

def exit_page
@exit_page ||= page.exit_pages.find(params.require(:id))
end

def exit_page_input_params
params.require(:pages_exit_page_input).permit(:heading, :markdown).merge(page:)
end

def update_exit_page_input_params
params.require(:pages_update_exit_page_input).permit(:heading, :markdown).merge(page:)
end

def check_user_has_permission
authorize current_form, :can_edit_form?
end
Expand All @@ -41,6 +90,12 @@ def check_multiple_branches_enabled
render "errors/not_found", status: :not_found, formats: :html
end

def check_page_can_have_exit_pages
return if Forms::RoutesInput.route_with_selection_options?(page)

render "errors/not_found", status: :not_found, formats: :html
end

def preview_html(exit_page_input_object)
return t("exit_page.no_content_added_html") if exit_page_input_object.markdown.blank?

Expand Down
2 changes: 2 additions & 0 deletions app/input_objects/pages/delete_exit_page_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Pages::DeleteExitPageInput < DeleteConfirmationInput
end
8 changes: 4 additions & 4 deletions app/input_objects/pages/exit_page_input.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
class Pages::ExitPageInput < BaseInput
attr_accessor :page, :markdown, :heading

validates :heading, :markdown, presence: true
validates :heading, length: { maximum: 250 }
validates :markdown, markdown: { allow_headings: true }
include ExitPageValidation

def submit
return false if invalid?

ExitPage.create!(question_page: page, heading:, markdown:)
page.form.save_question_changes! do
ExitPage.create!(question_page: page, heading:, markdown:)
end
end
end
21 changes: 21 additions & 0 deletions app/input_objects/pages/update_exit_page_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Pages::UpdateExitPageInput < BaseInput
attr_accessor :page, :markdown, :heading, :exit_page

include ExitPageValidation

def assign_exit_page_values
self.page = exit_page.question_page
self.heading = exit_page.heading
self.markdown = exit_page.markdown

self
end

def submit
return false if invalid?

page.form.save_question_changes! do
exit_page.update!(heading: heading, markdown: markdown)
end
end
end
9 changes: 9 additions & 0 deletions app/models/concerns/exit_page_validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module ExitPageValidation
extend ActiveSupport::Concern

included do
validates :heading, :markdown, presence: true
validates :heading, length: { maximum: 250 }
validates :markdown, markdown: { allow_headings: true }
end
end
6 changes: 6 additions & 0 deletions app/models/exit_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ def self.positions_for_page(question_page)
[exit_page_id, index + 1]
end
end

def options_to_this_exit_page
return [] if conditions.nil?

conditions.pluck(:answer_value)
end
end
17 changes: 12 additions & 5 deletions app/models/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,18 @@ class Form < ApplicationRecord

attr_accessor :task_status_service

def save_question_changes!
self.question_section_completed = false
# Make sure the updated_at is updated as we use this to determine if the form has changed in forms-runner.
touch unless changed?
save_draft!
# Takes an optional blocl which will be called in the same transaction
# as the save.
def save_question_changes!(&block)
ActiveRecord::Base.transaction do
self.question_section_completed = false

block.call if block_given?

# Make sure the updated_at is updated as we use this to determine if the form has changed in forms-runner.
touch unless changed?
save_draft!
end
end

def save_draft!
Expand Down
13 changes: 13 additions & 0 deletions app/views/pages/exit_pages/delete.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% set_page_title(title_with_error_prefix(t("page_titles.delete_exit_page"), delete_confirmation_input.errors&.any?)) %>
<% content_for :back_link, govuk_back_link_to(edit_exit_page_path(form_id: @current_form.id, page_id: page.id, id: exit_page.id), t(".back_link")) %>

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= render(
delete_confirmation_input,
url: exit_page_path(@current_form.id, page.id, exit_page.id),
caption_text: t(".caption", exit_page_number: exit_page.position, heading: exit_page.heading),
legend_text: t(".title"),
) %>
</div>
</div>
58 changes: 58 additions & 0 deletions app/views/pages/exit_pages/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<% set_page_title(title_with_error_prefix(t("page_titles.exit_page_edit"), exit_page_input.errors&.any?)) %>
<% content_for :back_link, govuk_back_link_to(routes_path(@current_form.id), t(".back_link")) %>

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">

<%= form_with model: exit_page_input, url: exit_page_path(@current_form.id, exit_page_input.exit_page.question_page.id, exit_page_input.exit_page.id), method: 'PATCH' do |f| %>
<%= f.govuk_error_summary %>
<h1 class="govuk-heading-l">
<span class="govuk-caption-l"><%= t("page_titles.exit_page_new_caption", question_number: exit_page_input.page.position) %></span>
<span class="govuk-visually-hidden"> - </span>
<%= t("page_titles.exit_page_edit") %>
</h1>

<% options = exit_page_input.exit_page.options_to_this_exit_page %>

<%= govuk_summary_list(actions: false) do |summary_list|
summary_list.with_row do |row|
row.with_key { t("page_route_card.question_name_short", question_number: exit_page_input.page.position) }
row.with_value { "#{exit_page_input.page.question_text}" }
end;

summary_list.with_row do |row|
row.with_key { t(".options_to_this_exit_page") }
row.with_value do
if options.empty?
t(".no_options_to_this_exit_page")
elsif options.one?
options.first
else
content_tag(:ul, class: "govuk-list govuk-list--bullet") do
options.collect { |option| concat(content_tag(:li, option)) }
end
end
end
end;
end %>

<%= f.govuk_text_field( :heading, label: { size: 'm' } ) %>

<%= render MarkdownEditorComponent::View.new(:markdown,
form_builder: f,
render_preview_path: render_preview_exit_pages_path(form_id: @current_form.id, page_id: exit_page_input.exit_page.question_page.id, check_preview_validation:),
preview_html: preview_html,
form_model: exit_page_input,
label: t("helpers.label.pages_exit_page_input.markdown"),
hint: t("helpers.hint.pages_exit_page_input.markdown"),
allow_headings: true) %>

<div class="govuk-button-group">
<%= f.govuk_submit t("save_and_continue") %>
<%= govuk_button_link_to t('.delete_exit_page'), delete_exit_page_path(@current_form.id, exit_page_input.exit_page.question_page.id, exit_page_input.exit_page.id), warning: true %>
<%= govuk_link_to t('cancel'), routes_path(@current_form.id) %>
</div>
<% end %>
</div>
</div>

11 changes: 11 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1741,10 +1741,12 @@ en:
copy_of_answers: Give people the option to get a copy of their answers by email
date_settings: Are you asking for someone’s date of birth?
declaration: Add a declaration for people to agree to
delete_exit_page: Are you sure you want to delete this exit page?
delete_secondary_skip: Are you sure you want to delete the route for any other answer?
edit_exit_page: Edit exit page
email_code_sent: Confirmation code sent
error_prefix: 'Error: '
exit_page_edit: Edit exit page
exit_page_new: Add exit page
exit_page_new_caption: Question %{question_number}’s exit pages
forbidden: You cannot view this page
Expand Down Expand Up @@ -1897,6 +1899,15 @@ en:
destroy:
success: The question, ‘%{question_text}’, has been deleted
exit_pages:
delete:
back_link: Back to edit exit page
caption: 'Exit page %{exit_page_number}: %{heading}'
title: Are you sure you want to delete this exit page?
edit:
back_link: Back to edit question routes
delete_exit_page: Delete exit page
no_options_to_this_exit_page: No route to this exit page
options_to_this_exit_page: Options that go to this exit page
new:
back_link: Back to edit question routes
heading: Edit question
Expand Down
23 changes: 23 additions & 0 deletions config/locales/input_objects/update_exit_pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
en:
activemodel:
errors:
models:
pages/update_exit_page_input:
attributes:
heading:
blank: Enter a page heading
too_long: Page heading must be %{count} characters or less
markdown:
blank: Enter page content
too_long: Page content must be %{count} characters or less
unsupported_markdown_syntax: Page content can only contain formatting for links, subheadings (##), bulleted lists (*), or numbered lists (1.)
helpers:
hint:
pages_update_exit_page_input:
heading: Use a heading that summarises why someone cannot continue with the form. For example, ‘You’re not eligible for this service’.
markdown: Explain why they cannot continue to use the form and, if possible, tell them what they should do instead.
label:
pages_update_exit_page_input:
heading: Page heading
markdown: Page content
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@
end
end

resources :exit_pages, only: %i[new create], path: "exit-pages", module: :pages do
resources :exit_pages, except: %i[index show], path: "exit-pages", module: :pages do
member do
get "/delete" => "exit_pages#delete", as: :delete
end

collection do
post "/preview" => "exit_pages#render_preview", as: :render_preview
end
Expand Down
35 changes: 9 additions & 26 deletions spec/input_objects/pages/exit_page_input_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,8 @@
let(:heading) { "the heading" }
let(:markdown) { "some markdown" }

describe "validations" do
it "is invalid if heading is nil" do
error_message = I18n.t("activemodel.errors.models.pages/exit_page_input.attributes.heading.blank")
exit_page_input.heading = nil
expect(exit_page_input).to be_invalid
expect(exit_page_input.errors.full_messages_for(:heading)).to include("Heading #{error_message}")
end

it "is invalid if markdown is nil" do
error_message = I18n.t("activemodel.errors.models.pages/exit_page_input.attributes.markdown.blank")
exit_page_input.markdown = nil
expect(exit_page_input).to be_invalid
expect(exit_page_input.errors.full_messages_for(:markdown)).to include("Markdown #{error_message}")
end

it "is invalid if heading is too long" do
error_message = I18n.t("activemodel.errors.models.pages/exit_page_input.attributes.heading.too_long", count: 250)
exit_page_input.heading = "a" * 251
expect(exit_page_input).to be_invalid
expect(exit_page_input.errors.full_messages_for(:heading)).to include("Heading #{error_message}")
end

it_behaves_like "a markdown field with headings allowed", :mark_complete do
let(:model) { exit_page_input }
let(:attribute) { :markdown }
end
it_behaves_like "validates exit pages" do
let(:model) { exit_page_input }
end

describe "#submit" do
Expand All @@ -40,6 +16,7 @@
let(:heading) { "the heading" }
let(:markdown) { "some markdown" }
let(:page) { create :page }
let(:form) { page.form }

it "returns a truthy value" do
expect(exit_page_input.submit).to be_truthy
Expand All @@ -55,6 +32,12 @@
expect(page.exit_pages.first.markdown).to eq(markdown)
end

it "sets question_section_completed to false and updates the draft" do
form.question_section_completed = true
expect { exit_page_input.submit }.to change(form, :question_section_completed).from(true).to(false)
.and(change { form.draft_form_document.reload.updated_at })
end

context "when the exit page is invalid" do
let(:heading) { nil }

Expand Down
Loading