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 }