-
Notifications
You must be signed in to change notification settings - Fork 10
Add RevertDraftFormService #2258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+265
−1
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e965fb5
Add RevertDraftFormService
thomasiles 2f142cd
Change to revert_draft_from_form_document(tag)
thomasiles fdea5e5
Add Form::ATTRIBUTES_NOT_IN_FORM_DOCUMENT
thomasiles 35a7075
Change RevertDraftFormService blocklist attributes
thomasiles 69084a5
Fix conditions after page creation
thomasiles b6eac77
Allow drafts to be reverted from :live
thomasiles File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# This service reverts a draft form to a form_document with a given tag, | ||
# effectively discarding all draft changes | ||
class RevertDraftFormService | ||
attr_reader :form | ||
|
||
# A list of attributes on the Form model that should be not be reverted | ||
FORM_ATTRIBUTES_TO_PRESERVE = %i[id created_at updated_at creator_id].freeze | ||
ATTRIBUTES_TO_EXCLUDE = Form::ATTRIBUTES_NOT_IN_FORM_DOCUMENT + FORM_ATTRIBUTES_TO_PRESERVE | ||
|
||
def initialize(form) | ||
@form = form | ||
end | ||
|
||
# Discards draft changes by reverting the form and its associations | ||
# to the state of the form_document with the given tag | ||
# Returns true on success, false on failure | ||
def revert_draft_from_form_document(tag) | ||
# Return early if there's no draft to discard | ||
form_document = FormDocument.find_by(form_id: form.id, tag:, language: "en") | ||
return false if form_document.blank? | ||
|
||
form_document_content = form_document.content | ||
|
||
ActiveRecord::Base.transaction do | ||
revert_form_attributes(form_document_content) | ||
revert_pages_and_nested_associations(form_document_content["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(form_document_content) | ||
attributes_to_update = Form.attribute_names - ATTRIBUTES_TO_EXCLUDE.map(&:to_s) | ||
|
||
form.assign_attributes(form_document_content.slice(*attributes_to_update)) | ||
end | ||
|
||
def revert_pages_and_nested_associations(steps_data) | ||
form.pages.reload | ||
|
||
revert_pages(steps_data) | ||
|
||
# revert conditions after pages are created to make sure conditions don't | ||
# have validation errors if the page hasn't been created yet | ||
revert_routing_conditions(steps_data) | ||
end | ||
|
||
def revert_pages(steps_data) | ||
form_document_step_ids = steps_data.pluck("id") | ||
|
||
# delete any pages on the form that are not present in the form_document version | ||
form.pages.where.not(id: form_document_step_ids).destroy_all | ||
|
||
# iterate through the form_document steps data to create or update pages using the original ids | ||
steps_data.each do |step_data| | ||
page = form.pages.find_or_initialize_by(id: step_data["id"]) | ||
|
||
assign_page_attributes(page, step_data) | ||
|
||
page.save! | ||
end | ||
end | ||
|
||
# steps in a form_document 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 | ||
|
||
def revert_routing_conditions(steps_data) | ||
all_conditions_data = steps_data.flat_map { |step| step["routing_conditions"] || [] } | ||
form_document_condition_ids = all_conditions_data.pluck("id") | ||
|
||
# remove any conditions which have been added to the draft but are not in the form_document data | ||
Condition.where(routing_page_id: form.pages.select(:id)).where.not(id: form_document_condition_ids).destroy_all | ||
|
||
all_conditions_data.each do |condition_data| | ||
condition = Condition.find_or_initialize_by(id: condition_data["id"]) | ||
|
||
assign_condition_attributes(condition, condition_data) | ||
condition.save! | ||
end | ||
end | ||
|
||
def assign_condition_attributes(condition, condition_data) | ||
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"], | ||
) | ||
end | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
require "rails_helper" | ||
|
||
describe RevertDraftFormService do | ||
subject(:revert_draft_form_service) { described_class.new(form) } | ||
|
||
let(:form) { create(:form, :live_with_draft) } | ||
let(:tag) { :live } | ||
|
||
RSpec::Matchers.define :be_reverted_to_state do |expected_state| | ||
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? | ||
state_matches = reloaded_form.state == expected_state.to_s | ||
|
||
# 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 | ||
form_document = FormDocument.find_by(form_id: form.id, tag:, language: "en") | ||
document_matches = reloaded_form.as_form_document.except("live_at") == form_document.content.except("live_at") | ||
|
||
state_matches && document_matches | ||
end | ||
end | ||
|
||
def revert_draft_to_live | ||
revert_draft_form_service.revert_draft_from_form_document(tag) | ||
end | ||
|
||
# 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_state(:live) | ||
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_state(:live) | ||
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_state(:live) | ||
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_state(:live) | ||
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_state(:live) | ||
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_state(:live) | ||
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_state(:live) | ||
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_state(:live) | ||
end | ||
end | ||
end | ||
|
||
context "when reverting to an archived form_document" do | ||
let(:form) { create(:form, :archived) } | ||
let(:tag) { :archived } | ||
|
||
it "reverts the form to its live state" do | ||
expect(form).to be_reverted_to_state(:archived) | ||
end | ||
end | ||
end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.