529 lines
19 KiB
Go
529 lines
19 KiB
Go
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
|
|
}
|
|
|
|
|