package app import ( "embed" "encoding/csv" "encoding/json" "fmt" "html/template" "log" "net/http" "path" "slices" "strings" "gopkg.in/yaml.v3" ) //go:embed embeded/form/*.yaml var formFS embed.FS const formEmbedPath = "embeded/form" const formTemplate = "form.html" type FormField struct { ID string `yaml:"id"` Label string `yaml:"label"` Placeholder string `yaml:"placeholder"` Description string `yaml:"description"` Type string `yaml:"type"` Choices []string `yaml:"choices"` Required bool `yaml:"required"` ChoicesFrom string `yaml:"choices_from"` Validators []string `yaml:"validators"` Values []string `yaml:"-"` Error string `yaml:"-"` } func (f *FormField) HasValue(value string) bool { return slices.Contains(f.Values, value) } func (f *FormField) IsEmpty() bool { return len(f.Values) == 0 || f.Values[0] == "" } func (f *FormField) GetStringValue() string { if !f.IsEmpty() { return f.Values[0] } return "" } type Form struct { Name string Title string `yaml:"title"` Header string `yaml:"header"` Footer string `yaml:"footer"` SubmitMessage string `yaml:"submit_message"` SubmitHandlers []string `yaml:"submit_handlers"` Fields []*FormField `yaml:"fields"` PermissionViewSubmissions []int64 `yaml:"permission_view_submissions"` } func (f *Form) GetFieldByName(name string) *FormField { name, _ = strings.CutSuffix(name, "[]") for _, field := range f.Fields { if name == field.ID { return field } } return nil } func (f *Form) FieldHasValue(name string) bool { field := f.GetFieldByName(name) return field != nil && !field.IsEmpty() } func (f *Form) Render(w http.ResponseWriter, r *http.Request) { hasSubmit := r.Method == "POST" hasError := false // check fields for errors and inject choices for _, field := range f.Fields { if field.Error != "" { hasError = true } if field.ChoicesFrom != "" { choices, err := getFormChoices(field.ChoicesFrom) if err != nil { httpHandleError(w, err) return } field.Choices = choices } } hasSuccess := hasSubmit && !hasError // render form template header := markdownToHTML([]byte(f.Header)) footer := markdownToHTML([]byte(f.Footer)) submitMessage := markdownToHTML([]byte(f.SubmitMessage)) httpHandleTemplate(w, formTemplate, map[string]any{ "name": f.Name, "title": f.Title, "fields": f.Fields, "url": fmt.Sprintf("//%s%s", r.Host, r.URL.String()), "header": template.HTML(header), "footer": template.HTML(footer), "submitMessage": template.HTML(submitMessage), "hasSubmit": hasSubmit, "hasError": hasError, "hasSuccess": hasSuccess, }) } /* Read form from YAML. */ func readFormYAML(name string) (Form, error) { rawYaml, err := formFS.ReadFile(path.Join(formEmbedPath, name+".yaml")) if err != nil { return Form{}, err } form := Form{Name: name} return form, yaml.Unmarshal(rawYaml, &form) } /* Download saved form submissions as CSV. */ func httpHandleFormSubmissionCSV(w http.ResponseWriter, r *http.Request, name string) { form, err := readFormYAML(name) if err != nil { httpHandleErrorText(w, err) return } // check permissions if len(form.PermissionViewSubmissions) > 0 { user, err := getCurrentUserFromSession(r) if err != nil || !user.HasID(form.PermissionViewSubmissions) { log.Printf(" >> ERROR: user %d does not have permission to view form submissions", user.ID) httpHandleErrorText(w, errAuthError) return } } records, err := fetchFormSubmissionRecordsByFormName(db, name) if err != nil { httpHandleErrorText(w, err) return } writer := csv.NewWriter(w) fieldNames := make([]string, 0) for _, field := range form.Fields { fieldNames = append(fieldNames, field.Label) } if err := writer.Write(fieldNames); err != nil { httpHandleErrorText(w, err) return } for _, record := range records { fieldValues := map[string][]string{} if err := json.Unmarshal([]byte(record.Data), &fieldValues); err != nil { httpHandleErrorText(w, err) return } csvValues := make([]string, 0) for _, field := range form.Fields { value := "" if fieldValues[field.ID] != nil { value = strings.Join(fieldValues[field.ID], ", ") } csvValues = append(csvValues, value) } if err := writer.Write(csvValues); err != nil { httpHandleErrorText(w, err) return } } w.Header().Add("Content-Type", "text/csv") w.WriteHeader(200) writer.Flush() } /* Handle form. */ func httpBaseHandleForm(w http.ResponseWriter, r *http.Request, path []string) bool { // ensure form route if path[0] != "f" && path[0] != "form" { return false } // get form name name := path[len(path)-1] // download submission csv if strings.HasSuffix(name, ".csv") { name, _ = strings.CutSuffix(name, ".csv") httpHandleFormSubmissionCSV(w, r, name) return true } // read form config form, err := readFormYAML(name) if err != nil { httpHandleError(w, err) return true } // render + handle form if _, err := form.Submit(w, r); err != nil { httpHandleError(w, err) return true } form.Render(w, r) return true }