seekia/gui/questionnaireGui.go

530 lines
19 KiB
Go
Raw Permalink Normal View History

package gui
// questionnaireGui.go implements pages to view and take a user's questionnaire
// Pages to build a user's questionnaire exist in buildProfileGui_General.go
// TODO: We need to add pages to view users who have taken a user's questionnaire, and statistics about those users and their responses
import "fyne.io/fyne/v2"
import "fyne.io/fyne/v2/widget"
import "fyne.io/fyne/v2/theme"
import "fyne.io/fyne/v2/container"
import "fyne.io/fyne/v2/layout"
import "fyne.io/fyne/v2/dialog"
import "seekia/internal/allowedText"
import "seekia/internal/helpers"
import "seekia/internal/mateQuestionnaire"
import "seekia/internal/myIdentity"
import "strings"
import "errors"
// This page is used to take a questionnaire
// Submit page is used to submit the completed questionnaire
//Inputs:
// -fyne.Window
// -[]mateQuestionnaire.QuestionObject: Input questionnaire
// -int: Current viewed question index
// -map[string]string: Current questionnaire response map to add responses to. Is submitted once questionnaire is completed.
// -Structure: Question Identifier -> Response
// -func(): Previous page
// -func(response string, previousPage func()): Submit page
func setTakeQuestionnairePage(window fyne.Window, inputQuestionnaire []mateQuestionnaire.QuestionObject, currentIndex int, myResponsesMap map[string]string, previousPage func(), submitPage func(string, func())){
currentPage := func(){setTakeQuestionnairePage(window, inputQuestionnaire, currentIndex, myResponsesMap, previousPage, submitPage)}
title := getPageTitleCentered("Take Questionnaire")
backButton := getBackButtonCentered(previousPage)
if (len(inputQuestionnaire) == 0){
setErrorEncounteredPage(window, errors.New("setTakeQuestionnairePage called with empty questionnaire."), previousPage)
return
}
if (currentIndex < 0 || currentIndex > (len(inputQuestionnaire)-1) ){
setErrorEncounteredPage(window, errors.New("setTakeQuestionnairePage called with invalid questionnaire index."), previousPage)
return
}
getNavigateQuestionnaireButtons := func()(*fyne.Container, error){
getNavigatePreviousButton := func()fyne.Widget{
if (currentIndex == 0){
emptyButton := widget.NewButton("", nil)
return emptyButton
}
previousButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
setTakeQuestionnairePage(window, inputQuestionnaire, currentIndex-1, myResponsesMap, previousPage, submitPage)
})
return previousButton
}
navigatePreviousButton := getNavigatePreviousButton()
navigateNextButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
if (currentIndex == len(inputQuestionnaire)-1){
// No questions are left to view. We submit the questionnaire.
if (len(myResponsesMap) == 0){
dialogTitle := translate("No Questions Answered")
dialogMessageA := getBoldLabelCentered(translate("You have not answered any questions."))
dialogMessageB := getLabelCentered(translate("You must answer at least 1 question."))
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
return
}
newResponse, err := mateQuestionnaire.CreateQuestionnaireResponse(myResponsesMap)
if (err != nil){
setErrorEncounteredPage(window, err, currentPage)
return
}
submitPage(newResponse, currentPage)
return
}
setTakeQuestionnairePage(window, inputQuestionnaire, currentIndex+1, myResponsesMap, previousPage, submitPage)
})
navigateButtonsGrid := container.NewGridWithColumns(2, navigatePreviousButton, navigateNextButton)
return navigateButtonsGrid, nil
}
navigateQuestionnaireButtons, err := getNavigateQuestionnaireButtons()
if (err != nil) {
setErrorEncounteredPage(window, err, previousPage)
return
}
currentQuestionViewIndex := helpers.ConvertIntToString(currentIndex + 1)
totalQuestionsString := helpers.ConvertIntToString(len(inputQuestionnaire))
currentIndexLabel := getBoldLabelCentered("Question " + currentQuestionViewIndex + " of " + totalQuestionsString)
navigateQuestionnaireButtonsCentered := getContainerCentered(navigateQuestionnaireButtons)
currentQuestionMap := inputQuestionnaire[currentIndex]
currentQuestionContainer, err := getViewQuestionnaireQuestionContainer(window, currentPage, currentQuestionMap, myResponsesMap)
if (err != nil){
setErrorEncounteredPage(window, err, previousPage)
return
}
page := container.NewVBox(title, backButton, widget.NewSeparator(), currentIndexLabel, navigateQuestionnaireButtonsCentered, widget.NewSeparator(), currentQuestionContainer)
setPageContent(page, window)
}
func setSubmitQuestionnairePage(window fyne.Window, recipientIdentityHash [16]byte, questionnaireResponse string, previousPage func(), onCompletePage func()){
currentPage := func(){setSubmitQuestionnairePage(window, recipientIdentityHash, questionnaireResponse, previousPage, onCompletePage)}
title := getPageTitleCentered("Submit Questionnaire")
backButton := getBackButtonCentered(previousPage)
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate")
if (err != nil){
setErrorEncounteredPage(window, err, currentPage)
return
}
if (myIdentityExists == false){
description1 := getBoldLabelCentered("Your Mate identity does not exist.")
description2 := getLabelCentered("You must create it before sending your questionnaire response.")
createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){
setChooseNewIdentityHashPage(window, "Mate", currentPage, currentPage)
}))
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, createIdentityButton)
setPageContent(page, window)
return
}
description1 := getBoldLabelCentered("Are you sure you want to submit your questionnaire?")
description2 := getLabelCentered("You will pay for the message on the next page.")
myResponseMap, err := mateQuestionnaire.ReadQuestionnaireResponse(questionnaireResponse)
if (err != nil) {
setErrorEncounteredPage(window, err, previousPage)
return
}
numberOfResponses := len(myResponseMap)
numberOfResponsesString := helpers.ConvertIntToString(numberOfResponses)
numberOfResponsesLabel := widget.NewLabel("Number Of Responses:")
numberOfResponsesText := getBoldLabel(numberOfResponsesString)
numberOfResponsesRow := container.NewHBox(layout.NewSpacer(), numberOfResponsesLabel, numberOfResponsesText, layout.NewSpacer())
messageCommunication := ">!>QuestionnaireResponse=" + questionnaireResponse
confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){
setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, messageCommunication, false, currentPage, onCompletePage)
}))
//TODO: After send, add to questionnaireHistory so we can keep track of our past answers and update them with new responses
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, numberOfResponsesRow, confirmButton)
setPageContent(page, window)
}
// This function returns a container containing a questionnaire choice/entry question with a submit button
// It is used on the question preview page and when a user is taking a questionnaire
func getViewQuestionnaireQuestionContainer(window fyne.Window, currentPage func(), questionObject mateQuestionnaire.QuestionObject, myResponsesMap map[string]string)(*fyne.Container, error){
questionIdentifier := questionObject.Identifier
questionType := questionObject.Type
questionContent := questionObject.Content
questionOptions := questionObject.Options
questionLabel := getBoldLabelCentered("Question:")
questionContentTrimmed, _, err := helpers.TrimAndFlattenString(questionContent, 30)
if (err != nil) { return nil, err }
questionContentLabel := getLabelCentered(questionContentTrimmed)
viewQuestionContentButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
setViewTextPage(window, "Viewing Question", questionContent, false, currentPage)
})
questionContentRow := container.NewHBox(layout.NewSpacer(), questionContentLabel, viewQuestionContentButton, layout.NewSpacer())
if (questionType == "Choice"){
//TODO: Deal with long choices
maximumAnswersAllowedString, choicesListString, delimiterFound := strings.Cut(questionOptions, "#")
if (delimiterFound == false){
return nil, errors.New("Malformed question object: Invalid choice question options: " + questionOptions)
}
maximumAnswersAllowedInt, err := helpers.ConvertStringToInt(maximumAnswersAllowedString)
if (err != nil) { return nil, err }
if (maximumAnswersAllowedInt < 1 || maximumAnswersAllowedInt > 6){
return nil, errors.New("Malformed question object: Invalid maximum answers allowed: " + maximumAnswersAllowedString)
}
choicesList := strings.Split(choicesListString, "$¥")
if (len(choicesList) < 2 || len(choicesList) > 6){
return nil, errors.New("Malformed question object: Invalid choices.")
}
getMaximumAnswersAllowedAdjusted := func()string{
if (maximumAnswersAllowedInt > len(choicesList)){
maximumAnswersAllowedAdjusted := helpers.ConvertIntToString(len(choicesList))
return maximumAnswersAllowedAdjusted
}
return maximumAnswersAllowedString
}
maximumAnswersAllowedAdjusted := getMaximumAnswersAllowedAdjusted()
getChooseChoiceLabelText := func()string{
if (maximumAnswersAllowedInt == 1){
return "Select Answer:"
}
return "Select Answer(s):"
}
chooseChoiceLabelText := getChooseChoiceLabelText()
chooseChoiceLabel := getBoldLabelCentered(chooseChoiceLabelText)
maximumAnswersLabel := getWidgetCentered(getItalicLabel("Maximum: " + maximumAnswersAllowedAdjusted))
getChoiceSelectButtons := func()(fyne.Widget, error){
if (maximumAnswersAllowedInt == 1){
questionSelector := widget.NewRadioGroup(choicesList, func(newChoice string){
if (newChoice == ""){
delete(myResponsesMap, questionIdentifier)
return
}
getNewSelectedIndex := func()(int, error){
for index, element := range choicesList{
if (element == newChoice){
return index, nil
}
}
return 0, errors.New("questionSelector onChanged function called with unknown choice: " + newChoice)
}
newSelectedIndex, err := getNewSelectedIndex()
if (err != nil){
setErrorEncounteredPage(window, err, currentPage)
return
}
// Encoded response is index+1
// For example, if we chose the first option in the list, our encoded response will contain "1"
newEncodedResponse := helpers.ConvertIntToString(newSelectedIndex+1)
myResponsesMap[questionIdentifier] = newEncodedResponse
})
myEncodedResponse, myResponseExists := myResponsesMap[questionIdentifier]
if (myResponseExists == true){
mySelectedOptionInt, err := helpers.ConvertStringToInt(myEncodedResponse)
if (err != nil){
return nil, errors.New("myResponsesMap is malformed: Contains invalid 1-selection-allowed choice question response: " + myEncodedResponse)
}
mySelectedOptionIndex := mySelectedOptionInt-1
if (mySelectedOptionIndex < 0 || mySelectedOptionIndex > (len(choicesList) - 1)){
return nil, errors.New("myResponsesMap is malformed: Contains invalid 1-selection-allowed choice question response: Item index is out of range: " + myEncodedResponse)
}
mySelectedOption := choicesList[mySelectedOptionIndex]
questionSelector.Selected = mySelectedOption
}
return questionSelector, nil
}
getMySelectionsList := func()([]string, error){
myEncodedResponse, myResponseExists := myResponsesMap[questionIdentifier]
if (myResponseExists == false){
emptyList := make([]string, 0)
return emptyList, nil
}
mySelectedIndexesList := strings.Split(myEncodedResponse, "$")
mySelectionsList := make([]string, 0, len(mySelectedIndexesList))
for _, mySelectionIndexString := range mySelectedIndexesList{
mySelectionIndexInt, err := helpers.ConvertStringToInt(mySelectionIndexString)
if (err != nil) {
return nil, errors.New("myResponsesMap is malformed: Choice response contains non-int item: " + mySelectionIndexString)
}
indexInt := mySelectionIndexInt - 1
if (indexInt < 0 || indexInt > (len(choicesList)-1)){
return nil, errors.New("myResponsesMap is malformed: MySelectionIndex is out of range.")
}
mySelectedItem := choicesList[indexInt]
mySelectionsList = append(mySelectionsList, mySelectedItem)
}
return mySelectionsList, nil
}
mySelectionsList, err := getMySelectionsList()
if (err != nil) { return nil, err }
choicesButtons := widget.NewCheckGroup(choicesList, nil)
handleChoiceSelectionFunction := func(newSelectionsList []string){
if (len(newSelectionsList) == 0){
delete(myResponsesMap, questionIdentifier)
mySelectionsList = make([]string, 0)
return
}
if (len(newSelectionsList) > maximumAnswersAllowedInt){
// We undo this selection
choicesButtons.Selected = mySelectionsList
dialogTitle := translate("Too Many Selections")
dialogMessageA := getLabelCentered(translate("You have selected too many responses."))
dialogMessageB := getLabelCentered(translate("You can only select " + maximumAnswersAllowedString + " responses."))
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
return
}
encodedSelectionsList := make([]string, 0)
for _, selectedChoice := range newSelectionsList{
getSelectedChoiceIndex := func()(int, error){
for index, element := range choicesList{
if (element == selectedChoice){
return index, nil
}
}
return 0, errors.New("Selected choice not found in choices list.")
}
selectedChoiceIndex, err := getSelectedChoiceIndex()
if (err != nil){
setErrorEncounteredPage(window, err, currentPage)
return
}
encodedSelection := helpers.ConvertIntToString(selectedChoiceIndex + 1)
encodedSelectionsList = append(encodedSelectionsList, encodedSelection)
}
encodedResponse := strings.Join(encodedSelectionsList, "$")
myResponsesMap[questionIdentifier] = encodedResponse
mySelectionsList = newSelectionsList
}
choicesButtons.OnChanged = handleChoiceSelectionFunction
choicesButtons.Selected = mySelectionsList
return choicesButtons, nil
}
choiceSelectButtons, err := getChoiceSelectButtons()
if (err != nil) { return nil, err }
choicesButtonsCentered := getWidgetCentered(choiceSelectButtons)
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){
_, exists := myResponsesMap[questionIdentifier]
if (exists == false){
// No response exists
return
}
delete(myResponsesMap, questionIdentifier)
currentPage()
}))
questionContainer := container.NewVBox(questionLabel, questionContentRow, widget.NewSeparator(), chooseChoiceLabel, maximumAnswersLabel, choicesButtonsCentered, noResponseButton)
return questionContainer, nil
}
if (questionType != "Entry"){
return nil, errors.New("getViewQuestionnaireQuestionContainer called with invalid questionObject: Invalid question type: " + questionType)
}
if (questionOptions != "Numeric" && questionOptions != "Any"){
return nil, errors.New("getViewQuestionnaireQuestionContainer called with invalid questionObject: Invalid Entry question options: " + questionOptions)
}
enterResponseLabel := getBoldLabelCentered("Enter " + questionOptions + " Response:")
responseEntry := widget.NewMultiLineEntry()
responseEntry.Wrapping = 3
myResponse, myResponseExists := myResponsesMap[questionIdentifier]
if (myResponseExists == true){
responseEntry.TextStyle = getFyneTextStyle_Bold()
responseEntry.SetText(myResponse)
} else {
responseEntry.SetPlaceHolder("Enter response...")
}
responseEntry.OnChanged = func(newResponse string){
if (myResponseExists == true && newResponse == myResponse){
responseEntry.TextStyle = getFyneTextStyle_Bold()
} else {
responseEntry.TextStyle = getFyneTextStyle_Standard()
}
}
responseEntryBoxed := getWidgetBoxed(responseEntry)
submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){
newResponse := responseEntry.Text
if (newResponse == ""){
delete(myResponsesMap, questionIdentifier)
currentPage()
return
}
if (len(newResponse) > 2000){
currentResponseCharacterCountString := helpers.ConvertIntToString(len(newResponse))
title := translate("Response Is Too Long.")
dialogMessageA := getLabelCentered("The longest response allowed is 2000 bytes.")
dialogMessageB := getLabelCentered("Your response bytes count: " + currentResponseCharacterCountString)
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
return
}
if (questionOptions == "Numeric"){
responseIsNumeric := helpers.VerifyStringIsFloat(newResponse)
if (responseIsNumeric == false){
title := translate("Response Is Not Numeric.")
dialogMessageA := getLabelCentered("You must respond with a number.")
dialogMessageB := getLabelCentered("Change your response to a number.")
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
return
}
}
responseIsAllowed := allowedText.VerifyStringIsAllowed(newResponse)
if (responseIsAllowed == false){
dialogTitle := translate("Response Is Invalid.")
dialogMessageA := getLabelCentered(translate("Response contains a prohibited character."))
dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8."))
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
return
}
isContained := strings.Contains(newResponse, "+&")
if (isContained == true){
dialogTitle := translate("Response Is Invalid.")
dialogMessageA := getLabelCentered(translate("Question contains prohibited string: ") + "+&")
dialogMessageB := getLabelCentered(translate("Remove this string and resubmit."))
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
return
}
myResponsesMap[questionIdentifier] = newResponse
currentPage()
}))
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){
delete(myResponsesMap, questionIdentifier)
currentPage()
}))
widener := widget.NewLabel(" ")
heightener := widget.NewLabel("")
buttonsWithWidener := container.NewVBox(submitButton, noResponseButton, widener, heightener)
entryWithButtons := getContainerCentered(container.NewGridWithColumns(1, responseEntryBoxed, buttonsWithWidener))
questionContainer := container.NewVBox(questionLabel, questionContentRow, widget.NewSeparator(), enterResponseLabel, entryWithButtons)
return questionContainer, nil
}