Skip to content
Draft
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
23 changes: 17 additions & 6 deletions app/controllers/forms/delete_confirmation_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,25 @@ def destroy
return redirect_to @back_url
end

success_url = current_form.group.present? ? group_path(current_form.group) : root_path
if current_form.draft?
success_url = current_form.group.present? ? group_path(current_form.group) : root_path

unless FormRepository.destroy(current_form)
flash[:message] = "Deletion unsuccessful"
return redirect_to @back_url
end
unless FormRepository.destroy(current_form)
flash[:message] = "Deletion unsuccessful"
return redirect_to @back_url
end

redirect_to success_url, status: :see_other, success: t(".success", form_name: current_form.name)
elsif current_form.live_with_draft?
success_url = live_form_path(current_form.id)

redirect_to success_url, status: :see_other, success: t(".success", form_name: current_form.name)
unless RevertDraftFormService.new(current_form).revert_draft_to_live
flash[:message] = "Deletion unsuccessful"
return redirect_to @back_url
end

redirect_to success_url, status: :see_other, success: t(".success", form_name: current_form.name)
end
end

private
Expand Down
124 changes: 124 additions & 0 deletions app/services/revert_draft_form_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# This service reverts a draft form to its last live version,
# effectively discarding all draft changes.
class RevertDraftFormService
attr_reader :form

# A list of attributes on the Form model that should be reverted
FORM_ATTRIBUTES_TO_REVERT = %w[
available_languages
declaration_section_completed
declaration_text
external_id
form_slug
language
name
payment_url
privacy_policy_url
question_section_completed
s3_bucket_aws_account_id
s3_bucket_name
s3_bucket_region
share_preview_completed
submission_email
submission_type
support_email
support_phone
support_url
support_url_text
what_happens_next_markdown
].freeze

def initialize(form)
@form = form
end

# Discards draft changes by reverting the form and its associations
# to the state of the last live version
# Returns true on success, false on failure
def revert_draft_to_live
# Return early if there's no draft to discard
return true unless form.live_with_draft?

live_form_data = form.live_form_document.content

ActiveRecord::Base.transaction do
revert_form_attributes(live_form_data)
revert_pages_and_nested_associations(live_form_data["steps"])

form.delete_draft_from_live_form
form.save!
end

true
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error("Failed to discard draft for form #{form.id}: #{e.message}")
false
end

private

# revert the top-level attributes of the Form object
def revert_form_attributes(live_data)
attributes_to_update = live_data.slice(*FORM_ATTRIBUTES_TO_REVERT)
form.assign_attributes(attributes_to_update)
end

# synchronize all pages and their nested routing conditions
def revert_pages_and_nested_associations(live_steps_data)
# ensure we have the latest version of pages
form.pages.reload

live_step_ids = live_steps_data.pluck("id")

# delete any pages on the form that are not present in the live version
form.pages.where.not(id: live_step_ids).destroy_all

# iterate through the live data to create or update pages
live_steps_data.each do |step_data|
page = form.pages.find_or_initialize_by(id: step_data["id"])

assign_page_attributes(page, step_data)
synchronize_routing_conditions_for_page(page, step_data["routing_conditions"] || [])

page.save!
end
end

# steps in a formDocument store the page attributes under "data"
def assign_page_attributes(page, step_data)
page_data = step_data["data"]
page.assign_attributes(
position: step_data["position"],
question_text: page_data["question_text"],
hint_text: page_data["hint_text"],
answer_type: page_data["answer_type"],
is_optional: page_data["is_optional"],
answer_settings: page_data["answer_settings"],
page_heading: page_data["page_heading"],
guidance_markdown: page_data["guidance_markdown"],
is_repeatable: page_data["is_repeatable"],
)
end

# synchronize the routing conditions for a single page
def synchronize_routing_conditions_for_page(page, live_conditions_data)
live_condition_ids = live_conditions_data.pluck("id")

# remove any conditions which have been added to the draft but are not in the live data
page.routing_conditions.where.not(id: live_condition_ids).destroy_all

# create or update conditions from the live data
live_conditions_data.each do |condition_data|
condition = Condition.find_by(id: condition_data["id"]) ||
page.routing_conditions.build(id: condition_data["id"])

condition.assign_attributes(
answer_value: condition_data["answer_value"],
routing_page_id: condition_data["routing_page_id"],
check_page_id: condition_data["check_page_id"],
goto_page_id: condition_data["goto_page_id"],
)
condition.save!
end
end
end
4 changes: 4 additions & 0 deletions app/state_machines/form_state_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ module FormStateMachine
transitions from: :live, to: :archived
transitions from: :live_with_draft, to: :archived_with_draft
end

event :delete_draft_from_live_form do
transitions from: :live_with_draft, to: :live
end
end
end
end
2 changes: 2 additions & 0 deletions app/views/forms/delete_confirmation/delete.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<% hint_text = t(".hint_for_live_draft") if @current_form&.live_with_draft? %>
<%= render(
@delete_confirmation_input,
url: @url,
caption_text: @item_name,
legend_text: t(".title"),
hint_text: hint_text,
) %>
</div>
</div>
4 changes: 3 additions & 1 deletion app/views/forms/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
sections: @task_list)
%>

<%= govuk_button_link_to(t("forms.delete_form"), delete_form_path(current_form.id), warning: true) if current_form.state.to_sym == :draft %>
<% if current_form.draft? or current_form.live_with_draft? %>
<%= govuk_button_link_to(t("forms.delete_form"), delete_form_path(current_form.id), warning: true) %>
<% end %>
</div>
</div>
11 changes: 10 additions & 1 deletion app/views/input_objects/_delete_confirmation_input.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
<%= f.govuk_error_summary %>
<% end %>
<span class="govuk-caption-l"><%= caption_text %></span>
<%
options = {
legend: { text: legend_text, size: 'l', tag: 'h1' }
}

if local_assigns.key?(:hint_text) && hint_text.present?
options[:hint] = { text: hint_text }
end
%>
<%= f.govuk_collection_radio_buttons :confirm,
delete_confirmation_input.values, ->(option) { option }, ->(option) { t('helpers.label.confirm_action_input.options.' + "#{option}") },
legend: { text: legend_text, size: 'l', tag: 'h1' }
**options
%>

<%= f.govuk_submit local_assigns.fetch(:submit_text, t(:continue)) %>
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ en:
forms:
delete_confirmation:
delete:
hint_for_live_draft: Deleting this draft will not remove the live form.
title: Are you sure you want to delete this draft?
destroy:
success: The draft form, ‘%{form_name}’, has been deleted
Expand Down
134 changes: 134 additions & 0 deletions spec/services/revert_draft_form_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
require "rails_helper"
# require "helpers/revert_to_live_state_matcher"

RSpec::Matchers.define :be_reverted_to_live_state do
match do |form|
# reload the form to get the latest state from the database
reloaded_form = form.reload

# the form should be live
is_live = reloaded_form.live?

# We convert the form to a form document and compare the content
# to the live form document content and check they match baring the live_at times
# this is the closest we can get to saying there is no changes to the form
document_matches = reloaded_form.as_form_document.except("live_at") == form.live_form_document.content.except("live_at")

is_live && document_matches
end
end

describe RevertDraftFormService do
subject(:revert_draft_to_live_form_service) { described_class.new(form) }

let(:form) { create(:form, :live_with_draft) }

delegate :revert_draft_to_live, to: :revert_draft_to_live_form_service

# we use `freeze_time` to freeze the timestamps of the form and its pages
# reverting a draft will not keep the timestamps from the live version
around { |example| freeze_time { example.run } }

context "when the draft has no changes" do
it "reverts the form to its live state" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "when a form attribute is changed in the draft" do
before do
form.update!(name: "A new draft name")
end

it "reverts the attribute change" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "when a page attribute is changed in the draft" do
before do
form.pages.first.update!(question_text: "A new draft question text")
end

it "reverts the page change" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "when a page is added to the draft" do
before do
form.pages.create!(answer_type: "text", question_text: "A new page added to the draft", is_optional: false)
end

it "removes the added page" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "when a page is removed from the draft" do
before do
form.pages.last.destroy!
end

it "re-adds the removed page" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "with routing conditions" do
let(:form) { create(:form, :ready_for_live, pages_count: 2) }

before do
# live version with a routing condition
form.pages.first.routing_conditions.create!(
answer_value: "Yes",
goto_page_id: form.pages.last.id,
routing_page_id: form.pages.first.id,
)
FormDocument.create!(form:, tag: "live", content: form.as_form_document(live_at: form.updated_at))
form.update!(state: :live_with_draft)
end

context "when a routing condition is added to the draft" do
before do
form.pages.first.routing_conditions.create!(
answer_value: "No",
goto_page_id: form.pages.last.id,
routing_page_id: form.pages.first.id,
)
end

it "removes the added routing condition" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "when a routing condition is removed from the draft" do
before do
form.pages.first.routing_conditions.first.destroy!
end

it "re-adds the removed routing condition" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end

context "when a routing condition is changed in the draft" do
before do
form.pages.first.routing_conditions.first.update!(answer_value: "Maybe")
end

it "reverts the changed routing condition" do
revert_draft_to_live
expect(form).to be_reverted_to_live_state
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
require "rails_helper"

RSpec.describe "input_objects/_delete_confirmation_input" do
let(:hint_text) { nil }

before do
delete_confirmation_input = DeleteConfirmationInput.new
url = "/delete"
caption_text = "Thing"
legend_text = "Are you sure you want to delete this thing?"

render locals: { delete_confirmation_input:, url:, caption_text:, legend_text: }
render locals: { delete_confirmation_input:, url:, caption_text:, legend_text:, hint_text: }
end

it "posts the confirm value to the destroy action" do
Expand All @@ -28,4 +30,18 @@
it "has a submit button" do
expect(rendered).to have_button "Continue", class: "govuk-button"
end

context "when no hint_text is provided" do
it "doesn't have a hint" do
expect(rendered).not_to have_css "fieldset legend:has(~ .govuk-hint)"
end
end

context "when hint_text is provided" do
let(:hint_text) { "This is a hint" }

it "has a hint" do
expect(rendered).to have_css ".govuk-hint", text: "This is a hint"
end
end
end