#lang scheme/base
(require (file "util.scm")
(file "repository.scm")
(file "record.scm")
(file "closures.scm")
"settings.scm"
(planet "web.scm" ("soegaard" "web.plt" 2 1))
(lib "url.ss" "net"))
(provide form
form-id
form-markup
grab-user-input
make-field-type
field-value-lift
)
(define-struct form-obj (markup id))
(define form-id form-obj-id)
(define form-markup form-obj-markup)
(define (grab-user-input fields call-back
#:submit-label (submit-label "Submit")
#:init (init '())
#:stay-on-same-page (stay-on-same-page #f))
(form fields
#:on-done (lambda (r) (call-back (rec-data r)))
#:stay-on-same-page stay-on-same-page
#:submit-label submit-label
#:skip-save #t
#:init init))
(define (form-aux fields
#:recur recur #:init (init '())
#:submit-label (submit-label "Save")
#:before-save (before-save (lambda (r) 'done))
#:skip-save (skip-save #f)
#:stamp-user (stamp-user #f)
#:stamp-time (stamp-time #t)
#:stay-on-same-page (stay-on-same-page #f)
#:fail (fail (lambda (rec) #f))
#:validate (validate (lambda (rec) #f))
#:error-wrapper (error-wrapper (lambda (f)
(if (form-obj? f) (form-markup f) f)))
#:error-msg (error-msg #f)
#:on-submit (on-submit #f) #:use-if-exists (use-if-exists #f)
#:skip-br (skip-br #f)
#:auto-submit (auto-submit #f)
#:return-form-obj (return-form-obj #f)
#:on-done (on-done (lambda (rec) "Record saved.")))
(let ((init-data (if (rec? init) (rec-data init) init))
(is-upload (has-upload-field? fields)))
(define (store-form-rec! req)
(let* ((bindings (request-bindings req))
(relevant-req-bindings
(map (match-lambda ((list name label type)
(cons name (field-value-lift (assoc-val name bindings)
type))))
fields))
(data (alist-merge init-data relevant-req-bindings))
(a-rec (if (rec? init)
(rec-set-data! init data)
(fresh-rec-from-data data #:stamp-time stamp-time)))
(the-rec (or (and use-if-exists
(load-one-where
`((,use-if-exists . ,(rec-prop a-rec use-if-exists)))))
a-rec)))
(when stamp-user (rec-set-rec-prop! the-rec 'created_by stamp-user))
(or (fail the-rec)
(aand (validate the-rec)
(error-wrapper (recur #:init (append relevant-req-bindings init)
#:error-msg it)))
(begin (before-save the-rec)
(unless skip-save (store-rec! the-rec))
(let ((finally (on-done the-rec)))
(if stay-on-same-page
(e "feature missing")
finally))))))
(let* ((form-id (number->string (random 1000000)))
(f `(form
((action "")
(id ,form-id)
(method "post")
,@(if is-upload '((enctype "multipart/form-data")) '())
,@(if on-submit `((onsubmit ,on-submit)) '()))
,@(splice-if error-msg `(div ((class "errors")) ,error-msg))
(input ((type "hidden")
(name ,(symbol->string (setting *CLOSURE_URL_KEY*)))
(value ,(body-as-closure-key req => (store-form-rec! req)))))
,@(form-body fields submit-label init-data form-id
#:skip-br skip-br #:auto-submit auto-submit))))
(if return-form-obj (make-form-obj f form-id) f))))
(define form (make-recursive-keyword-version-of-fn form-aux "recur"))
(define (has-upload-field? fields)
(any (lambda (f) (eq? (last f) 'image)) fields))
(define (paint-field field-name field-type form-id
#:field-value (field-value #f) #:auto-submit (auto-submit #f))
(let ((field-name (symbol->string field-name))
(field-type-name (if (field-type? field-type)
(field-type-name field-type)
field-type))
(auto '(onchange "this.form.submit();")))
(case field-type-name
((text)
`(input ((type "text") (name ,field-name) (class "text-input") (size "40")
(value ,(or field-value "")))))
((long-text)
`(textarea ((name ,field-name) (class "text-input")
(cols "20") (rows "4")) ,(or field-value "")))
((number)
`(input ((type "text") (name ,field-name) (size "5") (class "text-input")
(value ,(or (and field-value (number->string field-value)) "")))))
((password)
`(input ((type "password") (class "text-input") (name ,field-name))))
((image)
`(input ((type "file") (name ,field-name))))
((checkbox)
(if field-value `(span (input ((type "checkbox") (checked "yup") (name ,field-name)
(class "checkbox")
,@(splice-if auto-submit auto)))
(input ((type "hidden") (name ,field-name) (value "off"))))
`(input ((type "checkbox") (name ,field-name) (class "checkbox")
,@(splice-if auto-submit auto)))))
((radio)
(generic-picker (field-type-params field-type)
(lambda (val disp is-selected)
`(tr (td (input ((type "radio") (name ,field-name) (value ,val)
,@(if is-selected `((checked "yup")) '()))))
(td ,@disp)))
(lambda (elts) `(table ((class "big-radio")) ,@elts))
#:current-pick field-value))
((drop-down)
`(group ,(generic-picker (field-type-params field-type)
(lambda (val disp is-selected)
`(option ((value ,val)
,@(if is-selected `((selected "yup")) '()))
,disp))
(lambda (elts) `(select ((name ,field-name)) ,@elts))
#:current-pick field-value)
(br)))
(else (error (format "Field type '~A' for field '~A' not understood."
field-type field-name))))))
(define (generic-picker sym.=>display elt-wrapper whole-wrapper
#:current-pick (current-pick #f))
(whole-wrapper (map (match-lambda ((list-rest sym disp)
(elt-wrapper (symbol->string sym)
disp
(eq? sym current-pick))))
sym.=>display)))
(define (field-value-lift field-val field-type)
(case field-type
((checkbox)
(if (equal? field-val "on") #t #f))
((number)
(if (string=? field-val "") #f (string->number field-val)))
((image)
(save-uploaded-file! field-val))
(else (if (and (string? field-val) (string=? field-val ""))
#f
field-val))))
(define (save-uploaded-file! raw-file-bytes)
(let ((filename (format "~A.jpg" (random-key-string 10))))
(write-bytes raw-file-bytes
(open-output-file (format "../htdocs/img/usr/~A" filename) 'error))
filename))
(define (paint-rich-text-editor field-name field-value form-id)
`(div ((class "yui-skin-sam"))
(textarea ((name ,field-name) (id ,field-name) (cols "50") (rows "10"))
,field-value)
(script ,(format "render_rich_text_editor('~A', '~A')" field-name form-id))))
(define (form-body fields submit-label init-data form-id
#:skip-br (skip-br #f) #:auto-submit (auto-submit #f))
(define (paint-segment field-name display-name field-type)
(let* ((is-checkbox (eq? field-type 'checkbox))
(lbl-inp-lst (list (if is-checkbox
display-name
`(label ,display-name))
(paint-field field-name field-type form-id
#:field-value (assoc-val field-name init-data)
#:auto-submit auto-submit)
(if skip-br "" '(br)))))
(when (and is-checkbox display-name (or (not (string? display-name))
(not (string=? display-name ""))))
(set! lbl-inp-lst (cons-to-end '(br) (reverse lbl-inp-lst))))
`(group ,@lbl-inp-lst)))
(append
(map (match-lambda ((list field-name display-name field-type)
(paint-segment field-name display-name field-type)))
fields)
(if submit-label `((button ((type "submit")) ,submit-label)) '())))
(define-struct field-type (name params))