numerus/pkg/form.go

354 lines
8.4 KiB
Go
Raw Normal View History

package pkg
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"github.com/jackc/pgtype"
"html/template"
"net/http"
"net/mail"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
var tagsRegex = regexp.MustCompile("[^a-z0-9-]+")
type Attribute struct {
Key, Val string
}
type InputField struct {
Name string
Label string
Type string
Val string
Required bool
Attributes []template.HTMLAttr
Errors []error
}
func (field *InputField) Scan(value interface{}) error {
if value == nil {
field.Val = ""
return nil
}
switch v := value.(type) {
case time.Time:
if field.Type == "date" {
field.Val = v.Format("2006-01-02")
} else if field.Type == "time" {
field.Val = v.Format("15:04")
} else {
field.Val = v.Format(time.RFC3339)
}
default:
field.Val = fmt.Sprintf("%v", v)
}
return nil
}
func (field *InputField) Value() (driver.Value, error) {
return field.Val, nil
}
func (field *InputField) FillValue(r *http.Request) {
field.Val = strings.TrimSpace(r.FormValue(field.Name))
}
func (field *InputField) Integer() int {
value, err := strconv.Atoi(field.Val)
if err != nil {
panic(err)
}
return value
}
func (field *InputField) Float64() float64 {
value, err := strconv.ParseFloat(field.Val, 64)
if err != nil {
panic(err)
}
return value
}
2023-03-13 13:55:10 +00:00
func (field *InputField) String() string {
return field.Val
}
type SelectOption struct {
Value string
Label string
Group string
}
type SelectField struct {
Name string
Label string
Selected []string
Options []*SelectOption
Attributes []template.HTMLAttr
Required bool
Multiple bool
EmptyLabel string
Errors []error
}
func (field *SelectField) Scan(value interface{}) error {
if value == nil {
field.Selected = append(field.Selected, "")
return nil
}
if str, ok := value.(string); ok {
if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
for _, element := range array.Elements {
field.Selected = append(field.Selected, element)
}
return nil
}
}
field.Selected = append(field.Selected, fmt.Sprintf("%v", value))
return nil
}
func (field *SelectField) Value() (driver.Value, error) {
if field.Selected == nil {
return "", nil
}
return field.Selected[0], nil
}
func (field *SelectField) FillValue(r *http.Request) {
field.Selected = r.Form[field.Name]
}
func (field *SelectField) HasValidOptions() bool {
for _, selected := range field.Selected {
if !field.isValidOption(selected) {
return false
}
}
return true
}
func (field *SelectField) IsSelected(v string) bool {
for _, selected := range field.Selected {
if selected == v {
return true
}
}
return false
}
func (field *SelectField) FindOption(value string) *SelectOption {
for _, option := range field.Options {
if option.Value == value {
return option
}
}
return nil
}
func (field *SelectField) isValidOption(selected string) bool {
return field.FindOption(selected) != nil
}
func (field *SelectField) Clear() {
field.Selected = []string{}
}
func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
rows, err := conn.Query(ctx, sql, args...)
if err != nil {
panic(err)
}
defer rows.Close()
var options []*SelectOption
for rows.Next() {
option := &SelectOption{}
err = rows.Scan(&option.Value, &option.Label)
if err != nil {
panic(err)
}
options = append(options, option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
func MustGetGroupedOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
rows, err := conn.Query(ctx, sql, args...)
if err != nil {
panic(err)
}
defer rows.Close()
var options []*SelectOption
for rows.Next() {
option := &SelectOption{}
err = rows.Scan(&option.Value, &option.Label, &option.Group)
if err != nil {
panic(err)
}
options = append(options, option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
}
type TagsField struct {
Name string
Label string
Tags []string
Attributes []template.HTMLAttr
Required bool
Errors []error
}
func (field *TagsField) Value() (driver.Value, error) {
if field.Tags == nil {
return []string{}, nil
}
return field.Tags, nil
}
func (field *TagsField) Scan(value interface{}) error {
if value == nil {
return nil
}
if str, ok := value.(string); ok {
if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
for _, element := range array.Elements {
field.Tags = append(field.Tags, element)
}
return nil
}
}
field.Tags = append(field.Tags, fmt.Sprintf("%v", value))
return nil
}
func (field *TagsField) FillValue(r *http.Request) {
field.Tags = strings.Split(tagsRegex.ReplaceAllString(r.FormValue(field.Name), ","), ",")
if len(field.Tags) == 1 && len(field.Tags[0]) == 0 {
field.Tags = []string{}
}
}
func (field *TagsField) String() string {
return strings.Join(field.Tags, ",")
}
type FormValidator struct {
Valid bool
}
func newFormValidator() *FormValidator {
return &FormValidator{true}
}
func (v *FormValidator) AllOK() bool {
return v.Valid
}
func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool {
return v.checkInput(field, field.Val != "", message)
}
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
_, err := mail.ParseAddress(field.Val)
return v.checkInput(field, err == nil, message)
}
func (v *FormValidator) CheckValidVATINInput(field *InputField, country string, message string) bool {
// TODO: actual VATIN validation
return v.checkInput(field, true, message)
}
func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, message string) bool {
// TODO: actual phone validation
return v.checkInput(field, true, message)
}
func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
return v.checkInput(confirm, password.Val == confirm.Val, message)
}
func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool {
return v.checkSelect(field, field.HasValidOptions(), message)
}
func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
repeated := false
groups := map[string]bool{}
for _, selected := range field.Selected {
option := field.FindOption(selected)
if exists := groups[option.Group]; exists {
repeated = true
break
}
groups[option.Group] = true
}
return v.checkSelect(field, !repeated, message)
}
func (v *FormValidator) CheckValidURL(field *InputField, message string) bool {
_, err := url.ParseRequestURI(field.Val)
return v.checkInput(field, err == nil, message)
}
func (v *FormValidator) CheckValidDate(field *InputField, message string) bool {
_, err := time.Parse("2006-01-02", field.Val)
return v.checkInput(field, err == nil, message)
}
func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$"
match, err := regexp.MatchString(pattern, field.Val)
if err != nil {
panic(err)
}
return v.checkInput(field, match, message)
}
func (v *FormValidator) CheckValidInteger(field *InputField, min int, max int, message string) bool {
value, err := strconv.Atoi(field.Val)
return v.checkInput(field, err == nil && value >= min && value <= max, message)
}
func (v *FormValidator) CheckValidDecimal(field *InputField, min float64, max float64, message string) bool {
value, err := strconv.ParseFloat(field.Val, 64)
return v.checkInput(field, err == nil && value >= min && value <= max, message)
}
func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool {
if !ok {
field.Errors = append(field.Errors, errors.New(message))
v.Valid = false
}
return ok
}
func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string) bool {
if !ok {
field.Errors = append(field.Errors, errors.New(message))
v.Valid = false
}
return ok
}