Skip to content

Commit 844ea00

Browse files
author
ingram
committed
-Massive friggin changes (all old features are still supported, it has been thoroughly tested and the test facility is included, minus reCaptcha keys) check README.md for details on what's new
1 parent 7e3f64e commit 844ea00

10 files changed

Lines changed: 430 additions & 172 deletions

README.md

Lines changed: 65 additions & 58 deletions
Large diffs are not rendered by default.

formlets-test.asd

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(defpackage :formlets-test-system (:use :cl :asdf))
2+
(in-package :formlets-test-system)
3+
(defsystem formlets-test
4+
:version "0.1"
5+
:author "leo.zovic@gmail.com"
6+
:maintainer "leo.zovic@gmail.com"
7+
:licence "MIT-style"
8+
:description "Testing system to simplify development of the validating formlets system for Hunchentoot"
9+
:components ((:file "test-package")
10+
(:file "test" :depends-on ("test-package")))
11+
:depends-on (:cl-who :drakma :hunchentoot :cl-ppcre :formlets))

formlets.asd

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
;; THE SOFTWARE.
2222

23-
(defpackage "FORMLETS-SYSTEM" (:use :cl :asdf))
23+
(defpackage :formlets-system (:use :cl :asdf))
2424
(in-package :formlets-system)
2525
(defsystem formlets
2626
:version "0.1"
@@ -30,6 +30,7 @@
3030
:description "Validating formlets for Hunchentoot"
3131
:components ((:file "package")
3232
(:file "utility" :depends-on ("package"))
33-
(:file "recaptcha" :depends-on ("package" "utility"))
34-
(:file "formlets" :depends-on ("package" "utility" "recaptcha")))
33+
(:file "formlets" :depends-on ("package" "utility"))
34+
(:file "recaptcha" :depends-on ("package" "utility" "formlets"))
35+
(:file "macros" :depends-on ("package" "utility" "formlets")))
3536
:depends-on (:cl-who :drakma :hunchentoot :cl-ppcre))

formlets.lisp

Lines changed: 168 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,171 @@
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))

macros.lisp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
(in-package :formlets)
2+
3+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; HELPER MACROS
4+
;; NOTE: If you REALLY want, you can define your own raw formlet instance and validator as in define-formlet.
5+
;; The same can be said about show-formlet. If you go this route, pay attention to how they communicate;
6+
;; currently they use Hunchentoots' session (which is the main reason :formlets isn't portable)
7+
(defun define-field (field-name field-type &key size value-set default-value validation)
8+
"Takes a terse declaration and expands it into a make-instance for macro purposes"
9+
(let ((final-value-set (when value-set `(:value-set (list ,@value-set))))
10+
(final-size (when size `(:size ,size))))
11+
(multiple-value-bind (functions messages) (split-validation-list validation)
12+
`(make-instance ',field-type :name ,(format nil "~(~a~)" field-name)
13+
:default-value ,default-value ,@final-value-set ,@final-size
14+
:validation-functions (list ,@functions) :error-messages (list ,@messages)))))
15+
16+
(defmacro define-formlet ((name &key general-validation (submit "Submit")) (&rest fields) &rest on-success)
17+
"Converts a terse declaration form into the corresponding object and validation handler."
18+
;;; the flet function converts a terse declaration into the corresponding make-instance declaration
19+
(let* ((field-names (mapcar #'car fields))
20+
(field-objects (mapcar (lambda (f) (apply #'define-field f)) fields))
21+
(enctype (if (every (lambda (f) (not (eq (cadr f) 'file))) fields)
22+
"application/x-www-form-urlencoded"
23+
"multipart/form-data")))
24+
(multiple-value-bind (gen-val-fn gen-err-msg) (split-validation-list general-validation)
25+
`(progn
26+
;;; declare formlet instance
27+
(defparameter ,name
28+
(make-instance 'formlet
29+
:name ',name :submit ,submit :enctype ,enctype
30+
:validation-functions ,(when general-validation `(list ,@gen-val-fn))
31+
:error-messages ,(when general-validation `(list ,@gen-err-msg))
32+
:fields (list ,@field-objects)
33+
:on-success (lambda ,field-names (progn ,@on-success))))
34+
35+
;;; declare validation handler
36+
(define-easy-handler (,(intern (format nil "VALIDATE-~a" name)) :uri ,(format nil "/validate-~(~a~)" name)) ()
37+
(let* ((formlet-values (post-value ,name (post-parameters*)))
38+
(formlet-return-values (loop for f in (formlets::fields ,name) ;;the values list, less password values
39+
for v in formlet-values
40+
unless (eq (type-of f) 'password) collect v
41+
else collect nil)))
42+
(multiple-value-bind (result errors) (validate ,name formlet-values)
43+
(if result
44+
(apply (formlets::on-success ,name) formlet-values) ;; if everything's ok, send the user on
45+
(progn
46+
(setf (session-value :formlet-values) formlet-return-values
47+
(session-value :formlet-errors) errors
48+
(session-value :formlet-name) ',name)
49+
(redirect (referer)))))))))))
50+
51+
(defmacro show-formlet (formlet-name)
52+
"Shortcut for displaying a formlet.
53+
It outputs the formlet HTML to standard-out (with indenting).
54+
If this is the last submitted formlet in session, display field values and errors, then clear out the formlet-related session information."
55+
`(let ((val (if (eq (session-value :formlet-name) ',formlet-name)
56+
(session-value :formlet-values)
57+
(make-list (length (formlets::fields ,formlet-name)))))
58+
(err (if (eq (session-value :formlet-name) ',formlet-name)
59+
(session-value :formlet-errors)
60+
(make-list (length (formlets::fields ,formlet-name))))))
61+
(show ,formlet-name val err)
62+
(when (eq (session-value :formlet-name) ',formlet-name)
63+
(delete-session-value :formlet-name)
64+
(delete-session-value :formlet-values)
65+
(delete-session-value :formlet-errors))))

package.lisp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
(:use :cl :cl-who :hunchentoot)
33
(:import-from :cl-ppcre :regex-replace-all :split :scan)
44
(:import-from :drakma :http-request)
5-
(:export :def-formlet
6-
:recaptcha :recaptcha-passed? :combine-errors :*private-key* :*public-key*
7-
:file-type? :file-smaller-than?
8-
:longer-than? :shorter-than? :matches? :mismatches?)
5+
(:export :formlet :formlet-field
6+
:text :textarea :password :file :checkbox :select :radio-set :checkbox-set :multi-select
7+
:*public-key* :*private-key* :recaptcha
8+
:validate :show :post-value :show-formlet :define-formlet
9+
:longer-than? :shorter-than? :matches? :mismatches? :file-type? :file-smaller-than? :not-blank? :same-as? :picked-more-than? :picked-fewer-than? :picked-exactly?)
910
(:documentation "A package implementing auto-validating formlets for Hunchentoot"))

0 commit comments

Comments
 (0)