-
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
base: main
Are you sure you want to change the base?
Conversation
class RevertDraftFormService | ||
attr_reader :form | ||
|
||
# A list of attributes on the Form model that should be reverted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be easier, and shorter, to maintain a list of exclusions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I think that's a fair question. Thats how we started out yesterday with the except code.
I think a list of things to include is more explicit. The real difference is how we want it in the future when we add attributes to form. Do we want to include them by default or not? I'm not sure either way 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then can we make that list a module that we mix-in and only update once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The as_form_document
method uses a list of exceptions - I think it could potentially be confusing if the attributes are implicit when creating a form document from the current state, but explicit when doing the reverse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've switched this to use a list of exceptions rather than explicitly stating what should be copied.
I've pulled out the list of exceptions in Form into a constant and use that combined with another list of attributes which shouldn't be reverted.
Overall, I think we'd be better off making it more explicit about what goes in a form document. I think that's a bigger conversation though so this brings it in line with how we are creating the form_document in Form.
declaration_text | ||
external_id | ||
form_slug | ||
language |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about including language
and available_languages
in this list... I don't understand yet how the Welsh language feature is going to work, but it seems surprising behaviour that deleting the draft for one language could delete the draft for another, which is what this implies to me...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm not sure how this will work for welsh either.
I think if you create a draft form and add welsh to it, then delete the draft, you would expect the welsh version to be deleted as well.
In that case language should be included.
This will need to be updated when we add translations because we'll need to take some measures to use the right set of formDocuments when we update the form. I think.
3be1f7d
to
0e70f95
Compare
Add a new service which will take a live form with a draft and revert the changes to match the live form.
0e70f95
to
9c90cbc
Compare
# 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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also have an archived_with_draft
status, I'm not sure whether restoring from an archived version is something we should cover as part of this card or make a new one for it, but it feels like a valid thing to do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the service to make it more generic so we can add deleting drafts from archived forms in the future.
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"] || []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm not fully understanding the difference between RoutingConditions and the other Conditions...? How come we only copy over
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Condition is the ActiveRecord model. "routing_conditions" is how they are stored in form_document. Take a look at https://github.com/alphagov/forms/blob/main/ADR/ADR034-api-v2-for-forms.md#form-document-response to see an example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do check_conditions
and goto_conditions
not go into the FormDocument? Also asking in relation to copying a Form, and whether to also ignore them or if they need to be copied as I'm not clear what they're for
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All conditions for a form go into the document. Conditions in a form document are stored once under each ["step"]["routing_condition"].
Here is a section from an example in the above ADR:
{
"step": {
id: 18
/* ..other steps attributes... */
"routing_conditions": [
{
"id": 3,
"check_step_id": 18,
"routing_step_id": 18,
"goto_step_id": null,
"answer_value": "Yes",
"created_at": "2024-08-02T08:54:07.479Z",
"updated_at": "2024-08-02T08:54:07.479Z",
"skip_to_end": true,
"validation_errors": []
}
]
}
}
The step.id
, is the id of the step (which is a Page in active record but stored as step in the form document).
step.routing_conditions
contains an array of Conditions
linked to the step through routing_step_id
. check_step_id
and goto_step_id
are references to other steps in the form. These are just references, not extra conditions. All Conditions
have a routing_step_id
which links them to a Step
.
In this PR we are extracting values from the json form document, so we go through each "step" and create them, setting the references as we go.
In your PR, you will go through each Condition
in Page.routing_conditions
setting the values directly from the Condition
. This will ensure you have all of Conditions.
# to the state of the form_document with the given tag | ||
# Returns true on success, false on failure | ||
def revert_draft_to_live | ||
def revert_draft_to_form_document(tag) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if revert_draft_from_form_document(tag)
is a little better, as to mean it would read as revert draft from form document (that's live or archived)
# effectively discarding all draft changes | ||
class RevertDraftFormService | ||
attr_reader :form | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please can we reword the commit message from blacklist
to blocklist
🙏
To allow reverting to an archived form document as well as the live form document we pass the tag of the form document to revert to as a parameter. We also rename the method and change any references to live to match the new functionality.
Move the list of attributes excluded from Form#as_form_document to a constant so it can be referenced elsewhere.
Previously, the list of attributes to copy from a from_document when reverting a draft form was explicitly defined. This commit changes the list to be dynamically generated from the Form model minus some attributes we know we don't want to copy.
9f3cd47
to
3a9f861
Compare
Previously conditions were recreated as part of the page creation. This meant that conditions could be created with validation errors if the page hadn't been created yet. This commit fixes that by reverting conditions after pages are created.
Previously, drafts could only be discarded from :live_with_draft. This commit adds the ability to discard drafts from :live too. This has been added because it's possible to delete all a forms pages without creating a new draft.
dbbb281
to
af49a36
Compare
|
🎉 A review copy of this PR has been deployed! You can reach it at: https://pr-2258.admin.review.forms.service.gov.uk/ It may take 5 minutes or so for the application to be fully deployed and working. If it still isn't ready For the sign in details and more information, see the review apps wiki page. |
Add a new service to revert the changes in a draft form to the live version
Trello card: https://trello.com/c/tP5Fd96d/2610-add-the-ability-to-delete-a-draft-of-a-live-form-with-a-confirmation-page
Motivation
Form creators with a live form can make changes without altering the live form. We call this form the draft form and the live form is not affected. Form creators can make a series of updates to the form, check it and then make the new version live.
Sometimes a form creator makes changes but then doesn't want to make them live. Instead they want to start the editing again from the current live version. This is thought of as deleting the draft version. Again the live version isn't changed.
How are live and draft forms implemented?
See ADR17 and ADR25 for more context.
We have one model which represents the form. It holds the current edited state of the form. It has an association to pages, which themselves can contain conditions.
A state machine is used to model a Form's current publishing lifecycle status.
Form's start off in the
draft
state. When they are ready to be made live they transition to:live
and a FormDocument is created. The FormDocument contains a snapshot of the forms data to be used by the runner to display the form to form fillers.When a form creator views a form in the admin interface they are redirected to different views based on the form's state. There is a live view and view for editing drafts (the tasklist).
When a form creator wants to make changes to live form, they follow a link to the tasklist view. If they make any changes to a form in the
:live
state, it's transitioned into the:live_with_draft
state.Deleting drafts
Because
:draft
and:live_with_draft
are just states on the form there is not technically anything to delete. Instead we must reset everything in the Form to match the snapshot taken when the form was made live.Because the only information about what the contained is in the live FormDocument, we need to pick through it and find the data we need there.
This PR adds a new service which takes this approach. It contains a whitelist of attributes to copy from the form and then goes through the pages of the form, known as steps in the FormDocument, making sure they match.
I don't think there is a definite check to say that the form matches the live version. In the tests we rely on the idea that if we were to make the draft version live again it should match the live FormDocument.
Things to consider when reviewing