Replies: 9 comments 16 replies
-
|
We have a bit of time before we need to conclude this by about mid-August-ish... |
Beta Was this translation helpful? Give feedback.
-
|
Although I was involved in writing this, I should probably give my preference. I'm leaning towards option 2 because it seems like the changes will be far less drastic, so less likely to drag on and bring up major difficulties. Option 1 is conceptually quite nice, but in practice probably doesn't give us too much of an advantage over option 2. I think option 2 will also end up being easier to understand in the code, as the polymorphism introduced by option 1 isn't completely straightforward. From what I can tell for option 2, there are very few changes that need to be made that will affect forms without question sets with the exception of a small migration for the |
Beta Was this translation helpful? Give feedback.
-
|
I like the abstraction of having Would implementing steps be possible in a way that retains backwards compatibility (to ease the migration)? |
Beta Was this translation helpful? Give feedback.
-
|
In my opinion option 1 is conceptually closer to what we are actually trying to model, which is nice. I find the structure of the JSON and session data much clearer at a glance. It does sound like the migration itself would be pretty involved though, but as others have mentioned perhaps it is worth us paying that cost now to make future features easier. |
Beta Was this translation helpful? Give feedback.
-
|
According to @SamJamCul there may be changes to this needed based on changes to design. We have some time as we are going to start with Add an "answer" to a single question. |
Beta Was this translation helpful? Give feedback.
-
|
I've been messing around with another option, which I'd like to get some feedback on! Option 3: Questions and Sets are equal tier stepsThis is some sort of hybrid option, that makes use of the step by step construction - but keeps every question on the same tier, and uses sets as positioning elements for form journeys. There will be a need to be able to identify which questions are contained within a set, and this can be done either by associating them with the set, or by adding another object that represents the end of a set Option 3a: Sets belong to questionsIn this option, steps that come after a set will be connected back to that set, which can be used to determine the set details. When reaching a set, the runner can recognise that it needs to behave differently until the user progresses past the set's last step. I think the advantage here is that the runner more closely matches our mental model of how forms work, as each step should have information about what to do next. flowchart TD
Form --- Step1(Step 1)
Form --- Step2(Step 2)
Form --- Step3(Step 3)
Form --- Step4(Step 4)
Form --- Step5(Step 5)
Step1 --- Question1(Question 1)
Step2 --- Set(Set)
Step3 --- Question2(Question 2)
Step4 --- Question3(Question 3)
Step5 --- Question4(Question 4)
Mermaid diagrams unfortunately don't keep the order of step objects, so joining the set to each contained question results in a messy diagram. Each step that falls within the set will have an attribute that keys it to that set. I've draw up some extra lines in this image to show what I mean! Option 3b: Sets have a start and finish stepIn this option, the end of the set is demarcated by another object. This makes it extremely easy for the runner to recognise when it's reached the end of a set of questions, and it decouples questions within the set from the set itself. It's not completely necessary to do this though, but I like the idea of being able to shift the start and end points of the set to add and remove questions. I think this might be more palatable if the design changes to the extent that we're using sets more as marker points in the UI. flowchart TD
Form --- Step1(Step 1)
Form --- Step2(Step 2)
Form --- Step3(Step 3)
Form --- Step4(Step 4)
Form --- Step5(Step 5)
Form --- Step6(Step 6)
Step1 --- Question1(Question 1)
Step2 --- Set(Set 1)
Step3 --- Question2(Question 2)
Step4 --- Question3(Question 3)
Step5 --- Question4(Question 4)
Step6 --- EndSet(EndSet 1)
|
Beta Was this translation helpful? Give feedback.
-
|
@SamJamCul what parts of option 1 depend on the design? I.e. what changes in the design could lead to a change in the data model? |
Beta Was this translation helpful? Give feedback.
-
|
I've spent a while playing around with these options, to try and understand the pros and cons. MethodI've coded up some example transformations in https://gist.github.com/lfdebrux/e9ff91c07935302383806f065672a2f0 to try and make things concrete. I tried converting an existing form document and a potential question set record into the different formats, and I then looked at how easy it would be to get data out of the object and/or manipulate it. Note that the code in the gist is not production ready code or even particularly good; I tried as much as possible to keep all the logic in one function for each transformation because what I wanted to measure was how complicated the operation was, and being more DRY and abstract would have obscured that. After writing this code and getting it working I analysed it using some of the metrics from RuboCop, here's the results:
Discussion
In Option 1 the information about whether a question is in a set is implicit in the structure of the object. The same is true in Option 3b, but the information is a little harder to determine computationally, so we need to rely on the extra linkage more. It is important to note though that we do not necessarily have to use the same structures in forms-admin as we use for form documents sent to forms-runner; so moving questions or question sets will probably not be done in the way I've coded here. However I think it is a good proxy for the measuring the complexity of the object structure. ConclusionI think this analysis shows the advantage of making a bigger change to the form document structure; I think we should decide on either Option 1 or Option 3b, with a preference for Option 1. |
Beta Was this translation helpful? Give feedback.
-
|
This is paused as the feature that requires the next stage is being deprioritised for now. We will return to this when appropriate and leave open for further comments in the time being. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Summary
Adding another answer is an upcoming feature for Forms. This will require backend changes in order to build, run and store questions that can have multiple answers.
Problem
Currently, we only support answering a form's questions once per submission.
We want to allow forms to include
Questions will have a minimum and maximum number of answers that can be given, which the form builder will specify for each question or set of questions.
Question sets can take place at any point within a form.
We want to support routing to the start of a question set, and skipping over a question set. Routing will not lead into the middle of a question set, or from within a question set to the outside. We will likely want to also support routing between questions within a question set.
The current data structure for a form in the API is roughly:
erDiagram FORM ||--o{ PAGE : "form has many pages" FORM { integer id PK string name string form_slug } PAGE PAGE { integer id PK string question_text integer position integer next_page_id FK } CONDITION }o--|| PAGE : "page has many conditions" CONDITION { integer id PK integer check_page_id FK integer goto_page_id FK string answer_value }A
PAGEcontains question data and its position within the form.A
PAGEcan have one or moreCONDITIONs, which are used to describe conditional routing from that page.Proposal
Options
Option 1: Separate out "steps" from "questions" and "question sets"
This option separates pages into "Steps" and "Questions", with a new "QuestionSet" type representing a set of questions. A "Step" will either point to a "Question" or "QuestionSet" using the polymorphic associations feature of Rails.
flowchart TD Form --- Step1(Step) Form --- Step2(Step) Form --- Step3(Step) Step1 --- Question1(Question) Step2 --- QuestionSet(Question Set) Step3 --- Question2(Question) QuestionSet --- Step4(Step) QuestionSet --- Step5(Step) Step4 --- SetQuestion1(Question) Step5 --- SetQuestion2(Question)Forms-api database schema
erDiagram FORM ||--o{ STEP : "form has many steps" FORM { integer id PK string name string form_slug } STEP ||--|| POSITIONABLE: "has one polymorphic positionable" STEP { integer id PK integer form_id FK integer next_step_id FK string positionable_type "'Question' or 'QuestionSet'" integer positionable_id FK "FK to Question or QuestionSet" integer parent_question_set_id FK "Only has a value if step is in a set" integer position integer min_answers integer max_answers } CONDITION }o--|| STEP : "step has many conditions" CONDITION { integer id PK integer check_step_id FK integer goto_step_id FK string answer_value } QUESTION_SET ||--|| POSITIONABLE: "question set is a positionable" QUESTION_SET ||--o{ STEP : "question set has many steps" QUESTION_SET { integer id PK string name } QUESTION ||--|| POSITIONABLE: "question is a positionable" QUESTION { integer id FK string question_text string answer_type jsonb answer_settings bool is_optional }The new
STEPentity represents the position of a question or question set in a form.The new
QUESTIONentity represents the properties of a question for theSTEPthey belong to.The new
QUESTION_SETentity represents a set ofSTEPsthat can be anwered multiple times. Each of theSTEPswithin aQUESTION_SETwill have aQUESTION.We use a polymorphic association on the
STEPentity (the 'positionable') to establish a one-to-one association with either aQUESTIONor aQUESTION_SET.Steps have
min_answersandmax_answersvalues, which indicate how many times the question or set of questions can be answered.JSON representation of a form used by Runner
This is the JSON structure for storing a live form that runner retreives from forms-api.
In the proposed structure, the
pagesfield is replaced withsteps. Each "step" in thestepsarray has atypefield, which indicates whether it is a Question or a QuestionSet, and adatafield which contains either a Question or QuestionSet object.A QuestionSet object contains a
stepsfield which contains an array of Step objects for that QuestionSet. These Step objects will always have atype: "Question".{ "id": 123, "name": "All question types form", "form_slug": "all-question-types-form", "start_page": 5, ... "steps": [ { "id": 5, "next_step_id": 6, "position": 1, "min_answers": null, // would this have a value if the question can only be answered once? "max_answers": null, "type": "Question", // this tells us the "data" field contains a question object "data": { // this contains the Question attributes "question_text": "What is your name?", "is_optional": false ... }, "routing_conditions": [] }, { "id": 6, "next_step_id": 7, "position": 2, "min_answers": 1, "max_answers": 5, "type": "QuestionSet", // this tells us the "data" field contains a question set object "data": { // this contains the QuestionSet attributes "name": "What addresses have you lived at in the past 3 years?", "steps": [ { "id": 10, "next_step_id": 11, "position": 1, "min_answers": null, "max_answers": null, "type": "Question", "data": { "question_text": "What was your address?" ... } }, { "id": 11, "next_step_id": 12, "position": 2, "min_answers": null, "max_answers": null, "type": "Question", "data": { "question_text": "What date did you start living at this address?" ... } }, { "id": 12, "next_step_id": null, "position": 3, "min_answers": null, "max_answers": null, "type": "Question", "data": { "question_text": "What date did you stop living at this address?" ... } } ] } }, { "id": 7, "next_step_id": null, "position": 3, "min_answers": 1, "max_answers": 10, "type": "Question", "data": { "question_text": "What countries have you travelled to in the past year?", "is_optional": false ... } } ] }Runner session data structure
When a user is filling out a form, we store their completed answers in a session stored in a Redis database. The session currently has the following data structure:
{ "session_id": "37e7691c31a61c0169469ebcf2441f34", "answers": { // at the root level of the answers, the keys are the form IDs "123": { // at the next level, the keys are the page IDs "5": { // each page contains key value pairs for the question's form fields "date_day": "1", "date_month": "1", "date_year": "1900", "date": "1900-01-01" }, "6": { "full_name": "John Doe" } } } }For the new data model with question sets we think the data structure of the session would be something like:
{ "session_id": "37e7691c31a61c0169469ebcf2441f34", "answers": { // at the root level of the answers, the keys are the form IDs "123": { // at the next level, the keys are the step IDs for the question or question set "5": { "full_name": "John Doe" }, "6": { // question sets have a nested object with keys corresponding to the step IDs for its questions "10": [ // value for each question is an array with an entry per answer provided { "address1": "The Whitechapel Building", "postcode": "E1 8QS" }, { "address1": "Aviation House", "address2": "WC2B 6NH" } ], "11": [ { "date": "2018-01-01" }, { "date": "2015-01-01" } ], "12": [ { "date": "" }, { "date": "2018-01-01" } ] }, "7": [ // for single questions that can have multiple answers, the value is an array with an entry per answer given { "text": "Spain" }, { "text": "Italy" } ] } } }Pros
Cons
Option 2: Keep pages holding their position in a form, whilst also being able to assign them to sets
This option keeps the "Pages" model largely the same, with the position remaining a property of a page. A page can have a "question set" associated with it, which holds information about the set it is in, such as the name and minimum and maximum answers.
flowchart TD Form --- Page1(Page) Form --- Page2(Page) Form --- Page3(Page) Form --- Page4(Page) Form --- Page5(Page) Page2 --- Set1(Question Set) Page3 --- Set1(Question Set) Page4 --- Set1(Question Set)Forms-api database schema
erDiagram FORM ||--o{ PAGE : "form has many pages" FORM { integer id PK string name string form_slug } PAGE PAGE { integer id PK string question_text integer next_page_id FK integer position_in_form "Null if page is in a set and not the first page of the set?" integer position_in_set "Only has a value if page is in a set" integer min_answers integer max_answers integer question_set_id FK "Only has a value if page is in a set" } QUESTION_SET ||--o{ PAGE: "a page can belong to a question set" QUESTION_SET { integer id PK string name integer min_answers integer max_answers } CONDITION }o--|| PAGE : "page has many conditions" CONDITION { integer id PK integer check_page_id FK integer goto_page_id FK string answer_value }In this model, we add a
question_set_idforeign key to aPAGE. This is used to identify when a question is part of a question set.The
next_page_idwill point to the next question in the form regardless of whether it is in aQUESTION_SETor not. For the last page in a set, thenext_page_idwill point to the next page after the set. This indicates that once a user has finished providing as many repeat answers to the question set as they need to, they will be directed to this page next. Runner will inevitably need more logic in addition to the existing routing logic to determine which page a user is shown next when sets are introduced.When the position of a
QUESTION_SETwithin a form is changed, we'd have to update both thenext_page_idof the proceeding page, and also thenext_page_idof the final page in the set.The
postionattribute of aPAGEhas been replaced byposition_in_formandposition_in_setattributes. Theposition_in_formwill be set if the question is not in a question set, or is the first page of a question set. Theposition_in_setattribute will identifty the position of a question within a set.JSON representation of a form used by Runner
In the JSON structure, the
pagesarray holds all pages in a form. All entries are at the same level, regardless of whether they belong to a set.We identify pages in a set by the
question_set_id. There is a separatequestion_setsattribute on the form object which is an array of all the question sets in a form.An entry in the
question_setsarray has afirst_page, similar to thestart_pageon a form, to easily identify the first page in the set. We could also store thelast_pageif this makes the navigation logic in Runner easier.{ "id": 123, "name": "All question types form", "start_page": 5, "pages": [ { "id": 5, "next_page_id": 6, "position_in_form": 1, "min_answers": null, "max_answers": null, "question_text": "What is your name?" }, { // this is the first page in a set "id": 6, "next_page_id": 7, "position_in_form": 2, "position_in_set": 1, "min_answers": null, "max_answers": null, "question_text": "What was your address?", "question_set_id": 1 }, { "id": 7, "next_page_id": 8, "position_in_form": null, // this is null because it is not the first page in the set "position_in_set": 2, "min_answers": null, "max_answers": null, "question_text": "What date did you start living at this address?", "question_set_id": 1 }, { // this is the last page in a set "id": 8, "next_page_id": 9, "position_in_form": null, "position_in_set": 3, "min_answers": null, "max_answers": null, "question_text": "What date did you stop living at this address?", "question_set_id": 1 }, { "id": 9, "next_page_id": null, "position_in_form": 3, "min_answers": 1, "max_answers": 10, "question_text": "What countries have you travelled to in the past year?" } ], "question_sets": [ { "id": 1, "name": "What addresses have you lived at in the past 3 years?", "min_answers": 1, "max_answers": 5, "first_page": 6 } ] }Runner session data structure
The runner session data structure should not have to change much, except the value associated with a page will be an array when the page is either a single question that can be answered multiple times, or belongs to a question set.
If we wanted to allow for questions within a set to be themselves answered multiple times per set of answers, we might have difficulties with this data structure.
{ "session_id": "37e7691c31a61c0169469ebcf2441f34", "answers": { // at the root level of the answers, the keys are the form IDs "123": { // at the next level, the keys are the page IDs "5": { "full_name": "John Doe" }, "6": [ // value for each question in a set is an array with an entry per answer provided { "address1": "The Whitechapel Building", "postcode": "E1 8QS" }, { "address1": "Aviation House", "address2": "WC2B 6NH" } ], "7": [ { "date": "2018-01-01" }, { "date": "2015-01-01" } ], "8": [ { "date": "" }, { "date": "2018-01-01" } ], "9": [ // for single questions that can have multiple answers, the value is an array with an entry per answer given { "text": "Spain" }, { "text": "Italy" } ] } } }Pros
Cons
Beta Was this translation helpful? Give feedback.
All reactions