collect.lisp 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. ;;;; collect.lisp
  2. ;;;;
  3. ;;;; Copyright (c) 2015 Samuel W. Flint <swflint@flintfam.org>
  4. (in-package #:collect)
  5. ;;; "collect" goes here. Hacks and glory await!
  6. (defmacro define-application (name file &body body-forms)
  7. `(progn
  8. (restas:define-module ,name
  9. (:use :cl
  10. :cl-who
  11. :parenscript
  12. :sqlite
  13. :restas
  14. :iter)
  15. (:import-from #:hunchentoot
  16. #:post-parameter)
  17. (:import-from #:lass
  18. #:compile-and-write))
  19. (in-package ,name)
  20. (defvar *tables*)
  21. (defvar *db*)
  22. (defun start (port)
  23. (setq *db* (connect ,file))
  24. (iter (for (name . query))
  25. (do (execute-non-query *db* query)))
  26. (restas:start ,name :port 8080))
  27. (define-route main-css ("main.css" :type "text/css")
  28. (compile-and-write
  29. (*
  30. :margin 0
  31. :padding 0
  32. :font-family "Georgia, Palantino, Times, 'Times New Roman', sans-serif")
  33. (body
  34. :backgroud "#fff")
  35. (a
  36. :text-decoration "none")
  37. ("a:link, a:visited"
  38. :color "#f30")
  39. ("a:hover"
  40. :color "#f90")
  41. (div.main-content
  42. :position "absolute"
  43. :top "40px"
  44. :left "280px"
  45. :width "500px"
  46. (h1
  47. :font-size "40px"
  48. :font-weight "normal"
  49. :line-height "43px"
  50. :letter-spacing "-1px")
  51. (div.results
  52. :margin-top "20px"
  53. (ul
  54. :list-style-type "none")
  55. (li
  56. :font-size "18px"
  57. :line-height "24px"
  58. :margin-left "70px"))
  59. (div.page-list
  60. :text-align "center"
  61. :float "bottom")
  62. (dl
  63. :margin-top "10px")
  64. (dt
  65. :font-size "18px"
  66. :font-weight "bold")
  67. (dd
  68. :margin-left "20px"
  69. :margin-bottom "15px"))
  70. (div.nav
  71. :position absolute
  72. :top "40px"
  73. :left "20px"
  74. :width "200px"
  75. :padding "20px 20px 0 0"
  76. :border-right "1px solid #ccc"
  77. :text-align "right"
  78. (h2
  79. :text-transform "uppercase"
  80. :font-size "13px"
  81. :color "#333"
  82. :letter-spacing "1px"
  83. :line-height "20px"
  84. (a
  85. :color "#333"))
  86. (ul
  87. :list-style-type "none"
  88. :margin "20px 0")
  89. (li
  90. :font-size "14px"
  91. :line-height "20 px")
  92. (hr
  93. :height "1px"
  94. :color "#ccc"
  95. :margin-top "5px"
  96. :margin-bottom "5px"))))
  97. ,@body-forms))
  98. (defun to-sql-type (type)
  99. (cadr (assoc type '((:integer "INTEGER")
  100. (:text "TEXT")
  101. (:real "REAL")
  102. (:blob "BLOB")))))
  103. (defun relation (relationship)
  104. (if (not (null relationship))
  105. (destructuring-bind (table column) relationship
  106. (declare (string table column))
  107. (format nil " REFERENCES ~a (~a)"
  108. table
  109. column))
  110. ""))
  111. (defun col-to-clause (column)
  112. (destructuring-bind (name type &key (nullable t) (primary nil) relationship) column
  113. (declare (string name)
  114. (keyword type)
  115. (ignorable nullable relationship))
  116. (if (null primary)
  117. (if (null nullable)
  118. (format nil "~a ~a~a,"
  119. name
  120. (to-sql-type type)
  121. (relation relationship))
  122. (format nil "~a ~a~a NOT NULL,"
  123. name
  124. (to-sql-type type)
  125. (relation relationship)))
  126. (format nil "~a ~a PRIMARY KEY,"
  127. name
  128. (to-sql-type type)))))
  129. (defmacro define-table (name (&rest cols) &key &allow-other-keys)
  130. (let* ((clauses (map 'list #'col-to-clause cols))
  131. (sql-command (format nil "CREATE TABLE IF NOT EXISTS ~a~%(~%~{~a~%~});"
  132. name
  133. clauses)))
  134. `(push (cons ,name ,sql-command) *tables*)))
  135. (defun make-form-entry (entry)
  136. (destructuring-bind (label column &key (type :text) other-column options) entry
  137. (declare (string label column)
  138. (ignorable other-column options))
  139. (let ((label-name (concatenate 'string "fld_" column))
  140. (the-options (if (null options)
  141. (if (not (null other-column))
  142. `(,(car other-column) ,(cadr other-column))))))
  143. (case type
  144. (:text
  145. `(:tr
  146. (:td ,label)
  147. (:td (:input :type :text
  148. :name ,label-name))))
  149. (:drop-down
  150. `(:tr
  151. (:td ,label)
  152. (:td
  153. (:select :name ,label-name
  154. (loop for option in ,the-options
  155. collect (make-option-drop-down option))))))
  156. (:radio-buttons
  157. `(:tr
  158. (:td ,label)
  159. (:td (loop for option in ,the-options
  160. collect (make-option-radio option ,label-name)))))))))
  161. (defun make-option-drop-down (option)
  162. (let ((name (first option))
  163. (value (second option)))
  164. `(:option :value ,value ,name)))
  165. (defun make-option-radio (option label)
  166. (let ((name (first option))
  167. (value (second option)))
  168. `(:div (:input :type "radio" :name ,label :value ,value)
  169. ,name (:br))))
  170. (defun make-update-query (table columns fields)
  171. (let ((actual-query (format nil "INSERT INTO ~a (~{~a~^, ~}) values (~{~a~^, ~})"
  172. table
  173. columns
  174. (iter (for i in columns)
  175. (collect "?"))))
  176. (fields (iter (for field in fields)
  177. (collect `(post-parameter ,field)))))
  178. `(execute-non-query *db* ,actual-query ,@fields)))
  179. (defmacro define-form (name table (&rest fields) &key (title name) (description title) &allow-other-keys)
  180. (let* ((columns (map 'list #'second fields))
  181. (field-names (map 'list #'(lambda (column)
  182. (concatenate 'string "fld_" column))
  183. columns))
  184. (form-entries (map 'list #'make-form-entry fields))
  185. (update-query (make-update-query table columns field-names))
  186. (route-name (intern (string-upcase name)))
  187. (route-name-post (intern (string-upcase (concatenate 'string name "-post")))))
  188. `(progn
  189. (define-route ,route-name (,name)
  190. (with-html-output-to-string (out)
  191. (:html
  192. (:head (:title ,title)
  193. (:link :rel "style_sheet" :href (genurl 'main-css)))
  194. (:body (:div :class "nav")
  195. (:div :class "main-content"
  196. (:h1 ,title)
  197. (:p ,description)
  198. (:form :action (genurl ',route-name-post)
  199. :method :post
  200. (:table ,@form-entries
  201. (:tr (:td)
  202. (:td (:input :type :submit :value "Submit"))))))))))
  203. (define-route ,route-name-post (,name :method :post)
  204. ,update-query
  205. (redirect ',route-name)))))