File: esxml-form.el

package info (click to toggle)
esxml 0.3.8-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 196 kB
  • sloc: lisp: 2,122; makefile: 2
file content (301 lines) | stat: -rw-r--r-- 10,259 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
;;; esxml-form.el --- HTML Forms with EmacsLisp  -*- lexical-binding: t -*-

;; Copyright (C) 2012  Nic Ferrier

;; Author: Nic Ferrier <nferrier@ferrier.me.uk>
;; Maintainer: Nic Ferrier <nferrier@ferrier.me.uk>
;; Keywords: data, lisp
;; Created: 23rd September 2012
;; Package-Requires: ((esxml "0.0.7") (db "0.0.1"))
;; Version: 0.0.1

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This is an HTML Form processing library for ESXML. It ties together
;; the things in Lisp you need to make Forms work; like validation,
;; database validation and rendering.


;;; Code:

(require 'cl-lib)
(require 'esxml)
(require 'esxml-html)

(defconst esxml-form-field-defn
  '(name
    &key
    (type :regex) ; or :password, :email
    (regex ".+")
    ;; :html is one of :text :textarea :password
    ;; :checkbox :radio :select
    ;;
    ;; Further options should deal with the extra
    ;; data required by some of those types, for
    ;; example, :checkbox_selected could be used
    ;; for the checkbox
    (html :text)
    (check-failure "the content of the field was wrong")
    (type-check-failure "the content of the field was wrong")
    db-key
    db-check)
  "The Lisp definition used for a field.")

(cl-defmacro esxml-form ((&key db db-key) &rest field-args)
  "Make a field set.

A field set binds some field parameters together with some other
data, for example, a database."
  (declare (indent 0))
  (let ((fields (make-symbol "fieldsv")))
    `(let ((,fields
            (mapcar
             (lambda (field-arg)
               (cl-destructuring-bind ,esxml-form-field-defn field-arg
                 (list name
                       :type type
                       :regex regex
                       :check-failure check-failure
                       :type-check-failure type-check-failure
                       :html html
                       :db-check db-check
                       :db-key db-key)))
             (quote ,field-args))))
       (list :db (quote ,db)
             :db-key (quote ,db-key)
             :fields ,fields))))

(defun esxml-form-fields (fs)
  (plist-get fs :fields))

(defun esxml-form-db (fs)
  (symbol-value (plist-get fs :db)))

(defun esxml-form-db-key (fs)
  (plist-get fs :db-key))

(defmacro esxml-form-bind (body form)
  "Bind BODY successively to FORMS fields."
  `(mapcar
    (lambda (form-field)
      (cl-destructuring-bind ,esxml-form-field-defn form-field
        ,body))
    (esxml-form-fields ,form)))


;; Verification stuff

(defconst esxml-form-field-set-email-verify-re
  (concat
   "[a-zA-Z0-9-]+@[a-zA-Z0-9.-]+"
   "\\.\\(com\\|net\\|org\\|gov\\|[A-Za-z]+\\.[A-Za-z]+\\)$"))

(defun esxml--field-check (field value &optional db query)
  "Do a validity check on the FIELD.

Return the type of validation failure or `nil' for no failure.

The type of validation failure can be used as a key into the
field's `:check-failure' alist (if it is a list).  This means the
form can respond differently about database validation or other
types of validation."
  (let* ((field-type (plist-get field :type))
         (valid
          (cl-case field-type
            (:regex
             (equal
              0
              (string-match
               (plist-get field :regex)
               (or value ""))))
            (:email
             (string-match esxml-form-field-set-email-verify-re value))
            (:password
             ;; really? is this a verification?
             t))))
    (if (and valid db query)
        (when (db-query db query) :db-check)
        (unless valid field-type))))

(cl-defun esxml-field-set-check (fs params &key onerror onsuccess)
  "Check field set FS against the PARAMS values.

Checks that ALL the required fields are there and that any field
that is there is correctly specified.

Returns the empty list when it passes and an alist of field-name,
field-value and validation error message if it fails."
  (cl-labels ((subs-all (new old lst)
                (let ((l (lambda (e) (if (listp e) (subs-all new old e) e))))
                  (cl-substitute new old (mapcar l lst)))))
    (let* (last-check
           (db (esxml-form-db fs))
           (fields-set (esxml-form-fields fs))
           (errors
            (cl-loop with field-value
                     for (field-name . field-plist) in fields-set
                     do
                     (setq field-value (cdr (assoc field-name params
                                                   #'string-equal)))
                     when
                     (setq
                      last-check
                      (esxml--field-check
                       field-plist field-value
                       db (when db
                            (subs-all field-value '$
                                      (plist-get field-plist :db-check)))))
                     collect (list ; return the error structure
                              field-name
                              field-value
                              (let ((check-msg
                                     (plist-get field-plist :check-failure)))
                                (if (listp check-msg)
                              (car (cdr (assoc last-check check-msg)))
                              check-msg))))))
      (cond
       ((and errors (functionp onerror))
        (funcall onerror params errors))
       ((and (not errors) (functionp onsuccess))
        (funcall onsuccess params))
       (t errors)))))

(cl-defun esxml-field-set/label-style (&key html name value err)
  (esxml-label
   name
   nil
   (cons
    'div
    (cons
     '()
     (cons
      (cl-case html
        (:text (esxml-input name "text" value))
        (:password (esxml-input name "password" value))
        (:checkbox (esxml-input name "checkbox" value))
        (:radio (esxml-input name "radio" value))
        ;;(:select (esxml-select (symbol-name name)))
        (:textarea (esxml-textarea name value)))
      (when err
        (list
         `(div
           ((class . "error"))
           ,(elt err 1)))))))))

(cl-defun esxml-field-set/bootstrap-style (&key html name value err)
  "Produce a field in twitter bootstrap style."
  `(div
    ((class . ,(concat
                "control-group"
                (when err " error"))))
    ,(esxml-label name '((class . "control-label")))
    (div
     ((class . "controls"))
     ,@(let ((ctrl
              (cl-case html
                (:text (esxml-input name "text" value))
                (:password (esxml-input name "password" value))
                (:checkbox (esxml-input name "checkbox" value))
                (:radio (esxml-input name "radio" value))
                ;;(:select (esxml-select (symbol-name name)))
                (:textarea (esxml-textarea name (or value ""))))))
         (if err
               (list ctrl
                     `(span ((class . "help-inline"))
                            ,(elt err 1)))
               (list ctrl))))))

(defvar esxml-field-style :label
  "Style used for making form fields.")

(defun esxml-field-set->esxml (form &optional params errors style)
  "Fieldset of FORM to ESXML description of fields.

PARAMS, if supplied, is an ALIST of field-names -> value bindings
which are used to validate the fields and assigned to the
respective fields in the output.

The output is an ESXML representation of a form in label
style (an HTML LABEL element contains the controls).

If validation errors occur they are output as a DIV with class
\"error\", again, inside the LABEL.

STYLE, if specified is a either `:label' or `:bootstrap' to
indicate the style of form output used."
  (let ((form-style (or style esxml-field-style :label)))
    `(fieldset
      ()
      ,@(esxml-form-bind
         (let* ((symname (symbol-name name))
                (value (cdr (assoc symname params)))
                (err (cdr (assoc name errors))))
           (funcall
            (cl-case form-style
              (:label 'esxml-field-set/label-style)
              (:bootstrap 'esxml-field-set/bootstrap-style))
            :html html
            :name symname
            :value value
            :err err))
         form))))

(cl-defun esxml-form-save (form params &key db-data)
  "Save the specified PARAMS in the FORM in the attached DB.

If DB-DATA is a function it is called to filter the data going
into the DB."
  (let ((db (esxml-form-db form))
        (db-key (esxml-form-db-key form)))
    (when (and db db-key)
      (let ((key-value (cdr (assoc db-key params)))
            (form-data
             (esxml-form-bind (assoc (symbol-name name) params) form)))
        (db-put key-value
                (if (functionp db-data)
                    (funcall db-data form-data)
                    form-data)
                db)))))


;;; This isn't right yet. needs to be more generic.
(defun esxml-form-handle (form httpcon page handler &optional extra-data)
  "Handle the FORM on the HTTPCON.

PAGE is the file you will send.

HANDLER is a function that takes the DATA from the POST that has
been validated by the FORM for saving it.

EXTRA-DATA is passed to the PAGE as extra `replacements'."
  (cl-flet ((send (&optional data errors)
              (let ((esxml (esxml-field-set->esxml form data errors)))
                (elnode-send-file
                 httpcon page
                 :replacements `(("form" . ,(esxml-to-xml esxml))
                                 ,@extra-data)))))
    (elnode-method httpcon
      (GET (send))
      (POST
       (esxml-field-set-check
        form (elnode-http-params httpcon)
        :onerror 'send
        :onsuccess handler)))))


(provide 'esxml-form)

;;; esxml-form.el ends here