11(in-package :formlets )
22
3- ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Predicates
4- ; ;;;;;;;;;;;;;;basic field predicates
5- (defun longer-than? (num) (lambda (f) (> (length f) num)))
6- (defun shorter-than? (num) (lambda (f) (< (length f) num)))
7- (defun matches? (regex) (lambda (f) (scan regex f)))
8- (defun mismatches? (regex) (lambda (f) (not (scan regex f))))
9-
10- ; ;;;;;;;;;;;;;;file-related
11- ; ; a hunchentoot file tuple is '(temp-filename origin-filename file-mimetype)
12- (defun file-type? (&rest accepted-types)
13- (lambda (hunchentoot-file-tuple)
14- (member (third hunchentoot-file-tuple) accepted-types :test #' equal )))
15-
16- (defun file-smaller-than? (byte-size )
17- (lambda (hunchentoot-file-tuple)
18- (> byte-size (file-size (car hunchentoot-file-tuple)))))
19-
20- ; ;;;;;;;;;;;;;;recaptcha
21- (defun validate-recaptcha ()
22- (recaptcha-passed? (post-parameter " recaptcha_challenge_field" ) (post-parameter " recaptcha_response_field" ) (real-remote-addr)))
23-
24- ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;View related functions
25- (defun show-form-field (name type form-values form-errors)
26- (let* ((s-name (string name)) (l-name (string-downcase s-name)))
3+ ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; CLASS DECLARATIONS
4+ (defclass formlet ()
5+ ((name :reader name :initarg :name )
6+ (fields :reader fields :initarg :fields )
7+ (validation-functions :accessor validation-functions :initarg :validation-functions :initform nil )
8+ (error-messages :reader error-messages :initarg :error-messages :initform nil )
9+ (submit-caption :reader submit :initarg :submit :initform " Submit" )
10+ (enctype :accessor enctype :initarg :enctype :initform " application/x-www-form-urlencoded" )
11+ (on-success :reader on-success :initarg :on-success )))
12+
13+ (defclass formlet-field ()
14+ ((name :reader name :initarg :name )
15+ (validation-functions :accessor validation-functions :initarg :validation-functions :initform nil )
16+ (default-value :reader default-value :initarg :default-value :initform nil )
17+ (error-messages :accessor error-messages :initarg :error-messages :initform nil )))
18+
19+ (defclass text (formlet-field) ())
20+ (defclass textarea (formlet-field) ())
21+ (defclass password (formlet-field) ())
22+ (defclass file (formlet-field) ())
23+ (defclass checkbox (formlet-field) ())
24+
25+ (defclass formlet-field-set (formlet-field)
26+ ((value-set :reader value-set :initarg :value-set :initform nil ))
27+ (:documentation " This class is for fields that show the user a list of options" ))
28+
29+ (defclass select (formlet-field-set) ())
30+ (defclass radio-set (formlet-field-set) ())
31+
32+ (defclass formlet-field-return-set (formlet-field-set) ()
33+ (:documentation " This class is specifically for fields that return multiple values from the user" ))
34+
35+ (defclass multi-select (formlet-field-return-set) ())
36+ (defclass checkbox-set (formlet-field-return-set) ())
37+
38+ ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; METHODS
39+ ; ;;;;;;;;;post-value
40+ ; ;;;NOTE: This section exists because Hunchentoots' (post-parameter [field-name])
41+ ; ; returns a single value. This is problematic for multi-select boxes and checkbox sets
42+ ; ; (both of which potentially return multiple values from the user).
43+ ; ; post-value is not necessarily Hunchentoot specific, but it does expect values in the form of an alist
44+
45+ (defmethod post-value ((formlet formlet) post-alist)
46+ (mapcar (lambda (f) (post-value f post-alist)) (fields formlet)))
47+
48+ (defmethod post-value ((field formlet-field) post-alist)
49+ (cdr (assoc (name field) post-alist :test #' string= )))
50+
51+ (defmethod post-value ((field formlet-field-return-set) post-alist)
52+ (loop for (k . v) in post-alist
53+ if (string= k (name field)) collect v))
54+
55+ ; ;;;;;;;;;validate
56+ ; ;;;;NOTE: The validate methods each return (values [validation result] [errors]).
57+ ; ; [validation result] is a boolean
58+ ; ; [errors] can be either a list or tree of strings
59+
60+ (defmethod validate ((formlet formlet) form-values)
61+ (let ((errors (if (validation-functions formlet)
62+ (loop for f in (validation-functions formlet)
63+ for msg in (error-messages formlet)
64+ unless (apply f form-values) collect msg)
65+ (loop for f in (fields formlet)
66+ for v in form-values
67+ collect (multiple-value-bind (result error ) (validate f v) (unless result error ))))))
68+ (values (every #' null errors) errors)))
69+
70+ (defmethod validate ((field formlet-field) value)
71+ " Returns (values T NIL) if there are no errors, and (values NIL list-of-errors).
72+ By default, a formlet-field passes only its own value to its validation functions."
73+ (let ((errors (loop for fn in (validation-functions field)
74+ for msg in (error-messages field)
75+ unless (funcall fn value) collect msg)))
76+ (values (every #' null errors) errors)))
77+
78+ ; ;;;;;;;;;show
79+ ; ;;; The show functions just take a formlet/(-field)?/ (along with its value/s?/ and error/s?/)
80+ ; ; and output the corresponding HTML. This part is cl-who specific, but it could be easily made portable
81+ ; ; by replacing html-to-stout and html-to-str with raw format calls
82+
83+ (defmethod show ((formlet formlet) &optional values errors)
84+ (with-slots (error-messages name enctype) formlet
2785 (html-to-stout
28- (:li (:span :class " label" (str (name->label name)))
29- (case type
30- (:textarea (htm (:textarea :name l-name (str (getf form-values (sym->keyword name))))))
31- (:password (htm (:input :name l-name :class " text-box" :type (string type ))))
32- (:file (htm (:input :name l-name :class " file" :type (string type ))))
33- (:recaptcha (htm (recaptcha)))
34- (otherwise (htm (:input :name l-name
35- :value (getf form-values (sym->keyword name))
36- :class " text-box" :type (string type )))))
37- (show-error form-errors (sym->keyword name))))))
38-
39- (defmacro show-form ((form-name values errors &key (submit " Submit" ) (enctype " application/x-www-form-urlencoded" )) &body fields)
40- (let ((n (string-downcase (string form-name))))
41- ` (html-to-stout
42- (show-general-error , errors)
43- (:form :name , n :id , n :action , (string-downcase (format nil " /validate-~a " n)) :enctype , enctype :method " post"
44- (:ul :class " form-fields"
45- ,@ (mapcar (lambda (field)
46- ` (show-form-field ' ,(car field) ' ,(cadr field) , values , errors))
47- fields)
48- (:li (:span :class " label" ) (:input :type " submit" :class " submit" :value , submit)))))))
49-
50- ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Validation related functions
51- (defmacro validate-form ((origin-fn &key fields general) &body on-success)
52- ` (let ((results (list ,@ (loop for (field-name field-type . fn/message-list) in fields
53- collect (sym->keyword field-name)
54- collect (validate-field field-name field-type fn/message-list general)))))
55- (if (all-valid? results)
56- (progn ,@ on-success)
57- (, origin-fn :form-values (list ,@ (loop for field in fields
58- unless (member (cadr field) ' (:password :file :recaptcha ))
59- collect (sym->keyword (car field)) and collect (car field)))
60- :form-errors , (if general ` (list :general-error , general) ' results)))))
61-
62- (defun validate-field (field-value field-type fn/message-list general-error-message)
63- (cond ((equalp :recaptcha field-type) ` (unless (validate-recaptcha) " You seem to have mistyped the recaptcha" ))
64- ((null fn/message-list) nil )
65- (general-error-message ` (unless (funcall , (car fn/message-list) , field-value) , general-error-message))
66- ((= 2 (length fn/message-list)) ` (unless (funcall , (car fn/message-list) , field-value) , (cadr fn/message-list)))
67- (t ` (combine-errors (loop for (val-fn error-message) on (list ,@ fn/message-list) by #' cddr
68- collect (unless (funcall val-fn , field-value) error-message))))))
69-
70- (defun combine-errors (list-of-errors)
71- (when (remove-if #' null list-of-errors)
72- (html-to-str
73- (dolist (e list-of-errors)
74- (when e (htm (:p (str e))))))))
75-
76- ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Formlet definition
77- (defmacro def-formlet (formlet-name (source-fn fields &key general submit) &body on-success)
78- (let* ((enctype " application/x-www-form-urlencoded" )
79- (name+type (loop for f in fields collecting (list (car f) (cadr f)) when (equalp (cadr f) :file ) do (setf enctype " multipart/form-data" )))
80- (f-names (mapcar #' car fields)))
81- ` (progn (defun , (intern (format nil " SHOW-~a -FORM" formlet-name)) (values errors)
82- (show-form (, formlet-name values errors :submit , submit :enctype , enctype) ,@ name+type))
83- (define-easy-handler (, (intern (format nil " VALIDATE-~a " formlet-name)) :uri , (format nil " /validate-~( ~a ~) " formlet-name)) , f-names
84- (validate-form (, source-fn :fields , fields :general , general) ,@ on-success)))))
86+ (when (and (not (every #' null errors)) error-messages)
87+ (htm (:span :class " general-error" (str (show error-messages)))))
88+ (:form :name (name formlet) :id name :action (format nil " /validate-~( ~a ~) " name) :enctype enctype :method " post"
89+ (:ul :class " form-fields"
90+ (loop for a-field in (fields formlet)
91+ for e in errors
92+ for v in values
93+ do (htm (:li (:span :class " label" (str (string-capitalize (regex-replace-all " -" (name a-field) " " ))))
94+ (str (show a-field v (when (and e (not error-messages)) e))))))
95+ (:li (:span :class " label" ) (:input :type " submit" :class " submit" :value (submit formlet))))))))
96+
97+ (defmethod show ((list-of-string list ) &optional v e)
98+ " A method for showing error output in the Formlets module"
99+ (declare (ignore v e))
100+ (when list-of-string
101+ (html-to-str (:span :class " formlet-error" (dolist (s list-of-string) (htm (:p (str s))))))))
102+
103+ (defmethod show ((field formlet-field) &optional value error )
104+ (html-to-str (:input :name (name field) :value value :class " text-box" ) (str (show error ))))
105+
106+ (defmethod show ((field textarea) &optional value error )
107+ (html-to-str (:textarea :name (name field) (str value)) (str (show error ))))
108+
109+ (defmethod show ((field password) &optional value error )
110+ (html-to-str (:input :name (name field) :type " password" :class " text-box" ) (str (show error ))))
111+
112+ (defmethod show ((field file) &optional value error )
113+ (html-to-str (:input :name (name field) :type " file" :class " file" ) (str (show error ))))
114+
115+ (defmethod show ((field select) &optional value error )
116+ (html-to-str (:select :name (name field)
117+ (loop for v in (value-set field)
118+ do (htm (:option :value v :selected (when (string= v value) " selected" ) (str v)))))
119+ (str (show error ))))
120+
121+ (defmethod show ((field checkbox) &optional value error )
122+ (html-to-str (:input :type " checkbox" :name (name field) :value (name field)
123+ :checked (when (string= (name field) value) " checked" ))
124+ (str (show error ))))
125+
126+ (defmethod show ((field radio-set) &optional value error )
127+ (html-to-str (loop for v in (value-set field)
128+ do (htm (:span :class " input+label"
129+ (:input :type " radio" :name (name field) :value v
130+ :checked (when (string= v value) " checked" ))
131+ (str v))))
132+ (str (show error ))))
133+
134+
135+ (defmethod show ((field multi-select) &optional value error )
136+ (html-to-str (:select :name (name field) :multiple " multiple" :size 5
137+ (loop for v in (value-set field)
138+ do (htm (:option :value v
139+ :selected (when (member v value :test #' string= ) " selected" )
140+ (str v)))))
141+ (str (show error ))))
142+
143+ (defmethod show ((field checkbox-set) &optional value error )
144+ (html-to-str (loop for v in (value-set field)
145+ do (htm (:span :class " input+label"
146+ (:input :type " checkbox" :name (name field) :value v
147+ :checked (when (member v value :test #' string= ) " checked" ))
148+ (str v))))
149+ (str (show error ))))
150+
151+ ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; PREDICATES
152+ (defmacro define-predicate (name (&rest args) &body body)
153+ ` (defun , name , args (lambda (val) ,@ body)))
154+
155+ ; ;;;;;;;;; basic field predicates
156+ (define-predicate longer-than? (num) (> (length val) num))
157+ (define-predicate shorter-than? (num) (< (length val) num))
158+ (define-predicate matches? (regex) (scan regex val))
159+ (define-predicate mismatches? (regex) (not (scan regex val)))
160+ (define-predicate not-blank? () (or (null val) (and (stringp val) (not (string= " " val)))))
161+ (define-predicate same-as? (field-name-string) (string= val (post-parameter field-name-string)))
162+
163+ ; ;;;;;;;;; file-related
164+ ; ; a hunchentoot file tuple is '([temp filename] [origin filename] [file mimetype])
165+ (define-predicate file-type? (&rest accepted-types) (member (third val) accepted-types :test #' equal ))
166+ (define-predicate file-smaller-than? (byte-size ) (> byte-size (file-size (car val))))
167+
168+ ; ;;;;;;;;; set-related
169+ (define-predicate picked-more-than? (num) (> (length val) num))
170+ (define-predicate picked-fewer-than? (num) (< (length val) num))
171+ (define-predicate picked-exactly? (num) (= (length val) num))
0 commit comments