Skip to content
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

Add confirmation and validation to survey templates #156

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
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
25 changes: 16 additions & 9 deletions src/rpft/parsers/creation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ class SkipOption(ParserModel):
The value that is stored in the question variable if the question is skipped.
"""

class Confirmation(ParserModel):
condition: Condition = Condition()
question: str = ""
confirm_option: str = ""
back_option: str = ""


class SurveyQuestionModel(ParserModel):
"""
Expand Down Expand Up @@ -193,27 +199,28 @@ class SurveyQuestionModel(ParserModel):

skipoption: SkipOption = SkipOption()
"""
Make question skippable. Adds an additional choice allowing use to skip the
Make question skippable. Adds an additional choice allowing user to skip the
question.
"""

confirmation: ConditionsWithMessage = ConditionsWithMessage()
# confirmation: ConditionsWithMessage = ConditionsWithMessage()
confirmation: Confirmation = Confirmation()
"""
Conditional Answer confirmation. Condition for sending a message asking the user to
confirm their answer. Comes with a Yes/No choice for the user. 'No' repeats the
question; 'Yes' stores the answer and proceeds.
Conditional Answer confirmation. If a condition holds, send a message asking the
user to confirm their answer. Comes with a Yes/No choice for the user.
'No' repeats the question; 'Yes' stores the answer and proceeds.
"""

stop: ConditionsWithMessage = ConditionsWithMessage()
"""
Conditional premature end of survey (later: forward skip?). Condition that ends the
survey (with message to user).
Conditional premature end of survey (later: forward skip?).
If ANY of the conditions hold, ends the survey (with message to user).
"""

validation: ConditionsWithMessage = ConditionsWithMessage()
"""
Validation / conditional repetition of question. If condition holds, message is
printed and question is repeated.
Validation / conditional repetition of question. If a condition holds,
its associated message is printed and the question is repeated.
"""

postprocessing: PostProcessing = PostProcessing()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"row_id","type","from","loop_variable","condition","condition_var","condition_type","condition_name","include_if","message_text","choices","save_name","attachments","image","audio","video"
,"save_flow_result",,,,,,,,"Block can’t start with for loop, see issue #47",,"dummy",,,,
,"begin_for",,"message",,,,,,"{@messages@}",,,,,,
,"send_message",,,,,,,"{{message.text}}","{{message.text}}",,,"{{message.attachments}}","{{message.image}}","{{message.audio}}","{{message.video}}"
,"end_for",,,,,,,,,,,,,,
,"send_message",,,,,,,"{@skipoption.text!=''@}","{{skipoption.text}}",,,,,,
,"wait_for_response",,,,,,,,,,"input",,,,
row_id,type,from,loop_variable,condition,condition_var,condition_type,condition_name,include_if,message_text,choices,save_name,attachments,image,audio,video
,save_flow_result,,,,,,,,"Block can’t start with for loop, see issue #47",,dummy,,,,
,begin_for,,message,,,,,,{@messages@},,,,,,
,send_message,,,,,,,{{message.text}},{{message.text}},,,{{message.attachments or ''}},{{message.image}},{{message.audio}},{{message.video}}
,end_for,,,,,,,,,,,,,,
,send_message,,,,,,,{@skipoption.text!=''@},{{skipoption.text}},,,,,,
,wait_for_response,,,,,,,,,,input,,,,
34 changes: 25 additions & 9 deletions tests/input/survey_templates/template_survey_question_wrapper.csv
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
"row_id","type","from","loop_variable","condition","condition_var","condition_type","condition_name","include_if","message_text","template_arguments","choices","save_name"
,"insert_as_block","start",,,,,,,"template_survey_question_type_{{type}}",,,
,"save_value",,,,,,,,"@results.variable",,,"{{variable}}"
0,"save_value",,,,,,,,"yes",,,"{{completion_variable}}"
,"begin_for",,"cond;i",,,,,"{@stop.conditions|selectattr('message')|list@}","{@stop.conditions|selectattr('message')|list@}",,,
"{{i+1}}","split_by_value","{{i}}",,,,,,,"{{cond.condition.variable}}",,,
,"save_flow_result","{{i+1}}",,"{{cond.condition.value}}","{{cond.condition.variable}}","{{cond.condition.type}}","{{cond.condition.name}}",,"yes",,,"stop"
,"send_message",,,,,,,,"{{cond.message}}",,,
,"end_for",,,,,,,,,,,
row_id,type,from,loop_variable,condition,condition_var,condition_type,condition_name,include_if,message_text,template_arguments,choices,save_name
ask_question,insert_as_block,start,,,,,,,template_survey_question_type_{{type}},,,
vali0,save_value,,,,,,,,@results.variable,,,{{variable}}
,begin_for,,cond;i,,,,,{@validation.conditions|selectattr('message')|list@},{@validation.conditions|selectattr('message')|list@},,,
vali{{i+1}},split_by_value,vali{{i}},,,,,,,{{cond.condition.variable}},,,
,send_message,vali{{i}},,{{cond.condition.value}},{{cond.condition.variable}},{{cond.condition.type}},{{cond.condition.name}},,{{cond.message}},,,
,go_to,,,,,,,,ask_question,,,
,end_for,,,,,,,,,,,
conf0,save_flow_result,,,,,,,{@confirmation.conditions|selectattr('message')|list@},Need something to apply this label to,,,dummy
,begin_for,,cond;i,,,,,{@confirmation.conditions|selectattr('message')|list@},{@confirmation.conditions|selectattr('message')|list@},,,
conf{{i+1}},split_by_value,conf{{i}},,,,,,,{{cond.condition.variable}},,,
conf_msg{{i+1}},send_message,conf{{i+1}},,{{cond.condition.value}},{{cond.condition.variable}},{{cond.condition.type}},{{cond.condition.name}},,{{cond.message}},,Yes;No,
conf_yn{{i+1}},wait_for_response,,,,,,,,conf_yn,,,
,send_message,conf_yn{{i+1}},,n no,,,,,"Ok, repeating question.",,,
,go_to,,,,,,,,ask_question,,,
,send_message,conf_yn{{i+1}},,,,,,,"Sorry, I don’t understand",,,
,go_to,,,,,,,,conf_msg{{i+1}},,,
,send_message,conf_yn{{i+1}},,y yes,,,,,"Ok, answer confirmed",,,
,end_for,,,,,,,,,,,
stop0,save_value,,,,,,,,yes,,,{{completion_variable}}
,begin_for,,cond;i,,,,,{@stop.conditions|selectattr('message')|list@},{@stop.conditions|selectattr('message')|list@},,,
stop{{i+1}},split_by_value,stop{{i}},,,,,,,{{cond.condition.variable}},,,
,save_flow_result,stop{{i+1}},,{{cond.condition.value}},{{cond.condition.variable}},{{cond.condition.type}},{{cond.condition.name}},,yes,,,stop
,send_message,,,,,,,,{{cond.message}},,,
,end_for,,,,,,,,,,,
26 changes: 13 additions & 13 deletions tests/input/survey_templates/template_survey_wrapper.csv
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"row_id","type","from","loop_variable","condition","condition_var","condition_type","condition_name","include_if","message_text","data_sheet","data_row_id","choices","save_name","image","audio","video","obj_name","obj_id","node_name","_nodeId","no_response"
,"begin_for",,"question;i",,,,,,"{@questions@}",,,,,,,,,,,,
"q{{i}}start","split_by_value",,,,,,,"{@i==0@}","{{question.completion_variable}}",,,,,,,,,,,,
"q{{i}}start","split_by_value","q{{i-1}}start;q{{i-1}}end",,"yes;",,,,"{@i!=0@}","{{question.completion_variable}}",,,,,,,,,,,,
"q{{i}}flow","start_new_flow","q{{i}}start",,,,,,,"survey - {{survey_name}} - question - {{question.ID}}",,,,,,,,,,,,
,"send_message","q{{i}}flow",,"Expired",,,,"{@question.expiration.message!=""""@}","{{question.expiration.message}}",,,,,,,,,,,,
,"save_flow_result",,,,,,,"{@question.expiration.message!=""""@}","yes",,,,"expired",,,,,,,,
,"hard_exit",,,,,,,"{@question.expiration.message!=""""@}",,,,,,,,,,,,,
,"hard_exit","q{{i}}flow",,"Expired",,,,"{@question.expiration.message==""""@}",,,,,,,,,,,,,
"q{{i}}end","split_by_value","q{{i}}flow",,"Completed",,,,,"@child.results.stop",,,,,,,,,,,,
,"hard_exit","q{{i}}end",,"yes",,,,,,,,,,,,,,,,,
,"end_for",,,,,,,,,,,,,,,,,,,,
,"save_flow_result","q{{questions|length -1}}start;q{{questions|length -1}}end",,"yes;",,,,,"yes",,,,"proceed",,,,,,,,
row_id,type,from,loop_variable,condition,condition_var,condition_type,condition_name,include_if,message_text,data_sheet,data_row_id,choices,save_name,image,audio,video,obj_name,obj_id,node_name,_nodeId,no_response
,begin_for,,question;i,,,,,,{@questions@},,,,,,,,,,,,
q{{i}}start,split_by_value,,,,,,,{@i==0@},@fields.{{question.completion_variable}},,,,,,,,,,,,
q{{i}}start,split_by_value,q{{i-1}}start;q{{i-1}}end,,yes;,,,,{@i!=0@},@fields.{{question.completion_variable}},,,,,,,,,,,,
q{{i}}flow,start_new_flow,q{{i}}start,,,,,,,survey - {{survey_name}} - question - {{question.ID}},,,,,,,,,,,,
,save_flow_result,q{{i}}flow,,Expired,,,,,yes,,,,expired,,,,,,,,
,send_message,,,,,,,"{@question.expiration.message!=""""@}",{{question.expiration.message}},,,,,,,,,,,,
,hard_exit,,,,,,,,,,,,,,,,,,,,
q{{i}}end,split_by_value,q{{i}}flow,,Completed,,,,,@child.results.stop,,,,,,,,,,,,
,hard_exit,q{{i}}end,,yes,,,,,,,,,,,,,,,,,
,end_for,,,,,,,,,,,,,,,,,,,,
,save_flow_result,q{{questions|length -1}}start;q{{questions|length -1}}end,,yes;,,,,,yes,,,,proceed,,,,,,,,
,save_value,,,,,,,,,,,,s_{{survey_id}}_complete,,,,,,,,
140 changes: 139 additions & 1 deletion tests/test_surveyparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ def test_basic_survey(self):
[
("enter_flow", "survey - Survey Name - question - first"),
("enter_flow", "survey - Survey Name - question - second"),
("send_msg", "You waited too long"),
("set_run_result", "expired"),
("send_msg", "You waited too long"),
],
Context(inputs=["completed", "expired"]),
)
Expand All @@ -132,6 +132,7 @@ def test_basic_survey(self):
("enter_flow", "survey - Survey Name - question - second"),
("enter_flow", "survey - Survey Name - question - third"),
("set_run_result", "proceed"),
("set_contact_field", "s_surveyname_complete"),
],
Context(inputs=["completed", "completed", "completed"]),
)
Expand Down Expand Up @@ -222,6 +223,143 @@ def test_stop_condition(self):
Context(inputs=["My name"]),
)

def test_confirmation_conditions(self):
ci_sheet = csv_join(
"type,sheet_name,data_sheet,data_row_id,new_name,data_model",
"data_sheet,survey_data,,,,SurveyQuestionRowModel",
"survey,,survey_data,,Survey Name,",
)
survey_data = csv_join(
"ID,type,question,variable,completion_variable,confirmation.conditions.1.condition,confirmation.conditions.1.message,confirmation.conditions.2.condition,confirmation.conditions.2.message", # noqa: E501
"age,text,Enter your age,,,18|@answer|has_number_lt|,Are you sure you're under 19?,25|@answer|has_number_gt|,Are you sure you're over 24?", # noqa: E501
)

output = (
ContentIndexParser(
CompositeSheetReader(
[
CSVSheetReader(TESTS_ROOT / "input/survey_templates"),
MockSheetReader(ci_sheet, {"survey_data": survey_data}),
]
)
)
.parse_all()
.render()
)

self.assertFlowMessages(
output,
"survey - Survey Name - question - age",
[
("set_run_result", "dummy"),
("send_msg", "Enter your age"),
("set_contact_field", "sq_surveyname_age"),
("set_run_result", "dummy"),
("set_contact_field", "sq_surveyname_age_complete"),
],
Context(inputs=["20"], variables={"@fields.sq_surveyname_age": "20"}),
)

self.assertFlowMessages(
output,
"survey - Survey Name - question - age",
[
("set_run_result", "dummy"),
("send_msg", "Enter your age"),
("set_contact_field", "sq_surveyname_age"),
("set_run_result", "dummy"),
("send_msg", "Are you sure you're under 19?"),
("send_msg", "Sorry, I don’t understand"),
("send_msg", "Are you sure you're under 19?"),
("send_msg", "Ok, answer confirmed"),
("set_contact_field", "sq_surveyname_age_complete"),
],
Context(
inputs=["15", "x", "y"], variables={"@fields.sq_surveyname_age": "15"}
),
)

self.assertFlowMessages(
output,
"survey - Survey Name - question - age",
[
("set_run_result", "dummy"),
("send_msg", "Enter your age"),
("set_contact_field", "sq_surveyname_age"),
("set_run_result", "dummy"),
("send_msg", "Are you sure you're over 24?"),
("send_msg", "Ok, repeating question."),
("set_run_result", "dummy"),
("send_msg", "Enter your age"),
("set_contact_field", "sq_surveyname_age"),
("set_run_result", "dummy"),
("send_msg", "Are you sure you're over 24?"),
("send_msg", "Ok, answer confirmed"),
("set_contact_field", "sq_surveyname_age_complete"),
],
Context(
inputs=["30", "n", "30", "y"],
variables={"@fields.sq_surveyname_age": "30"},
),
)

def test_validation_conditions(self):
ci_sheet = csv_join(
"type,sheet_name,data_sheet,data_row_id,new_name,data_model",
"data_sheet,survey_data,,,,SurveyQuestionRowModel",
"survey,,survey_data,,Survey Name,",
)
survey_data = csv_join(
"ID,type,question,variable,completion_variable,validation.conditions.1.condition,validation.conditions.1.message,validation.conditions.2.condition,validation.conditions.2.message", # noqa: E501
"age,text,Enter your age,,,18|@answer|has_number_lt|,You are too young,25|@answer|has_number_gt|,You are too old", # noqa: E501
)

output = (
ContentIndexParser(
CompositeSheetReader(
[
CSVSheetReader(TESTS_ROOT / "input/survey_templates"),
MockSheetReader(ci_sheet, {"survey_data": survey_data}),
]
)
)
.parse_all()
.render()
)

self.assertFlowMessages(
output,
"survey - Survey Name - question - age",
[
("set_run_result", "dummy"),
("send_msg", "Enter your age"),
("set_contact_field", "sq_surveyname_age"),
("set_contact_field", "sq_surveyname_age_complete"),
],
Context(inputs=["20"], variables={"@fields.sq_surveyname_age": "20"}),
)

# This is a limitation of the flow simulation: We cannot reassign context
# variables, i.e. not simulate the user input changing the value of a variable.
# A validation check failing causes the flow to repeat the input/validation
# loop indefinitely, resulting in an IndexError after popping all the user
# inputs from the context.
with self.assertRaises(IndexError):
self.assertFlowMessages(
output,
"survey - Survey Name - question - age",
[],
Context(inputs=["15"], variables={"@fields.sq_surveyname_age": "15"}),
)

with self.assertRaises(IndexError):
self.assertFlowMessages(
output,
"survey - Survey Name - question - age",
[],
Context(inputs=["30"], variables={"@fields.sq_surveyname_age": "30"}),
)

def test_template_overwrite(self):
ci_sheet = csv_join(
"type,sheet_name,data_sheet,new_name,data_model,template_arguments",
Expand Down
Loading