web.lisp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. (in-package :cl-user)
  2. (restas:define-module #:assboard.web
  3. (:use :cl :hunchentoot :assboard.utils :assboard.engine))
  4. (in-package #:assboard.web)
  5. ;; Parameters
  6. (setf (who:html-mode) :html5)
  7. (restas:define-route static/css ("css/:file" :content-type "text/css")
  8. (let ((css-path (merge-pathnames "css/" +project-path+)))
  9. (merge-pathnames file css-path)))
  10. (defclass renderer () ())
  11. (restas::register-pkgmodule-traits
  12. 'assboard.web
  13. :render-method (lambda () (make-instance 'assboard.web::renderer)))
  14. ;; Rendering
  15. (defgeneric finalize-page (data)
  16. (:documentation "Final rendering step"))
  17. (defgeneric render-route-data (route data)
  18. (:documentation "Process route-specific data"))
  19. (defmethod finalize-page ((data list))
  20. (who:with-html-output-to-string (out nil :prologue t)
  21. (:html
  22. (:head
  23. (:meta :charset "utf-8")
  24. (:meta :http-equiv "X-UA-Compatible" :content "IE=edge")
  25. (:meta :name "viewport" :content "width=device-width, initial-scale=1")
  26. (:title (who:str (getf data :title)))
  27. ;;(:link :rel "stylesheet" :href (restas:genurl 'static/css :file "styles.css"))
  28. )
  29. (:body
  30. (who:str (getf data :menu))
  31. (who:str (getf data :content))))))
  32. (defmethod render-route-data (route (data list))
  33. (list*
  34. :title (getf data :title "AssBoard")
  35. data))
  36. (defmethod restas:render-object ((designer renderer) (object list))
  37. (let ((full-data (render-route-data (restas:route-symbol restas:*route*)
  38. object)))
  39. (finalize-page full-data)))
  40. (defun posts-list (out posts &key (author t) (status nil))
  41. (who:with-html-output (out)
  42. (if posts
  43. (who:htm
  44. (:ul
  45. (loop for post in posts
  46. :do (who:htm
  47. (:li
  48. (:a :href (restas:genurl 'posting :hash (data::posting-hash post))
  49. (who:esc (data::posting-title post)))
  50. (when status
  51. (who:fmt
  52. " [~A]" (data::posting-status post)))
  53. (when author
  54. (who:htm
  55. " by "
  56. (:a :href (restas:genurl 'author
  57. :author (data::posting-author post))
  58. (who:esc (data::posting-author post))))))))))
  59. (who:htm (:ul (:li "No posts yet"))))))
  60. (defmethod render-route-data ((route (eql 'index)) (posts list))
  61. (list
  62. :title "AssBoard"
  63. :content
  64. (who:with-html-output-to-string (out)
  65. (:h1 "AssBoard open postings")
  66. (posts-list out posts)
  67. (:h2 "Add new posting")
  68. (:form :method "POST" :action (restas:genurl 'posting/post)
  69. (:input :type "text" :name "title" :placeholder "Title") (:br) (:br)
  70. (:textarea :name "body" :cols 80 :rows 10 :placeholder "Body") (:br)
  71. (:button :type "submit" "Prepare")))))
  72. (restas:define-route index ("")
  73. (pomo:query-dao
  74. 'data::posting
  75. (:select 'hash 'author 'status 'title 'maintainer 'created-on
  76. :from 'posting
  77. :where (:= 'status "open"))))
  78. (defmethod render-route-data ((route (eql 'posting/post)) (data list))
  79. (list
  80. :title "Add new posting"
  81. :content
  82. (who:with-html-output-to-string (out)
  83. (:h1 "Add new posting")
  84. (if (getf data :error)
  85. (who:htm (:h2 "Error adding post:")
  86. (:p (who:str (getf data :error))))
  87. (who:htm
  88. (:p "To add new posting, clearsign (`gpg --clearsign`) the following text and paste results from '-----BEGIN PGP SIGNED MESSAGE-----' up to '-----END PGP SIGNATURE-----'")
  89. (:textarea :disabled t :cols 80 :rows 10
  90. (who:str (getf data :to-sign)))
  91. (:form :method "POST"
  92. (:textarea :name "clearsigned" :cols 80 :rows 20 :placeholder "clearsigned") (:br)
  93. (:button :type "submit" "Post")))))))
  94. (restas:define-route posting/post ("posting/" :method :post)
  95. (let ((clearsigned (hunchentoot:post-parameter "clearsigned")))
  96. (if clearsigned
  97. (handler-case
  98. (let ((posting (process-command clearsigned)))
  99. (restas:redirect 'posting :hash (data::posting-hash posting)))
  100. (command-error (err)
  101. (log:info "Error processing command ~A" err)
  102. (list :error err
  103. :clearsigned clearsigned)))
  104. (let ((title (hunchentoot:post-parameter "title"))
  105. (body (hunchentoot:post-parameter "body")))
  106. (if (or (equal title "") (equal body ""))
  107. (restas:redirect 'index)
  108. (list
  109. :to-sign (format-new-posting title body)))))))
  110. (defmethod render-route-data ((route (eql 'author)) (data list))
  111. (list
  112. :title (format nil "~A's postings @ AssBoard" (getf data :author))
  113. :content
  114. (who:with-html-output-to-string (out)
  115. (:h1 (who:str (getf data :author)) "'s postings")
  116. (who:str (posts-list out (getf data :posts) :author nil :status t)))))
  117. (restas:define-route author ("author/:author/")
  118. (let ((posts (pomo:query-dao
  119. 'data::posting
  120. (:select 'hash 'author 'status 'title 'maintainer 'created-on
  121. :from 'posting
  122. :where (:and
  123. (:= 'author author)
  124. (:!= 'status "deleted"))))))
  125. (if posts
  126. (list :author author :posts posts)
  127. hunchentoot:+HTTP-NOT-FOUND+)))
  128. (defun render/post-subheader (out post)
  129. (who:with-html-output (out)
  130. (:h2
  131. "by "
  132. (:a :href (restas:genurl 'author :author (data::posting-author post))
  133. (who:esc (data::posting-author post)))
  134. (who:fmt " [~A]" (data::posting-status post)))))
  135. (defmethod restas:render-object ((designer renderer) (object data::posting))
  136. (if (data::posting-deletedp object)
  137. (finalize-page
  138. (list
  139. :title "Posting deleted"
  140. :content (who:with-html-output-to-string (out)
  141. (:h1 "Posting deleted")))
  142. )
  143. (finalize-page
  144. (list
  145. :title (format nil "~A by ~A @ AssBoard"
  146. (data::posting-title object)
  147. (data::posting-author object))
  148. :content
  149. (who:with-html-output-to-string (out)
  150. (:h1 (who:esc (data::posting-title object)))
  151. (render/post-subheader out object)
  152. (:div (cl-markdown:markdown (data::posting-body object) :stream out))
  153. (:hr)
  154. (:h2 "Edit post")
  155. (:form :method "POST" :action (restas:genurl 'posting-edit/post
  156. :hash (data::posting-hash object))
  157. (:input :type "text" :name "title" :size 80
  158. :value (data::posting-title object))
  159. (:br) (:br)
  160. (:textarea
  161. :name "body" :cols 80 :rows 10
  162. (who:esc (data::posting-body object))) (:br)
  163. (:input :type "submit" :name "action" :value "Update") " "
  164. (:input :type "submit" :name "action" :value "Delete") " "
  165. (:input :type "submit" :name "action" :value "Close")))))))
  166. (restas:define-route posting ("posting/:hash/")
  167. (let ((post (first (pomo:select-dao 'data::posting
  168. (:and
  169. (:= 'hash hash))))))
  170. (if post post
  171. hunchentoot:+HTTP-NOT-FOUND+)))
  172. (defmethod render-route-data ((route (eql 'posting-edit/post)) (data list))
  173. (let ((post (getf data :post)))
  174. (list
  175. :title (format nil "Update '~A' by ~A @ AssBoard"
  176. (data::posting-title post)
  177. (data::posting-author post))
  178. :content
  179. (who:with-html-output-to-string (out)
  180. (:h1 (who:esc (format nil "Update '~A'" (data::posting-title post))))
  181. (render/post-subheader out post)
  182. (if (getf data :error)
  183. (who:htm (:h2 "Error editing post:")
  184. (:p (who:str (getf data :error))))
  185. (who:htm
  186. (:p (who:esc (format nil "To ~a posting, clearsign (`gpg --clearsign`) the following text and paste results from '-----BEGIN PGP SIGNED MESSAGE-----' up to '-----END PGP SIGNATURE-----'"
  187. (getf data :action))))
  188. (:textarea :disabled t :cols 80 :rows 10
  189. (who:str (getf data :to-sign)))
  190. (:form :method "POST"
  191. (:input :type "hidden" :name "action" :value (getf data :action))
  192. (:textarea :name "clearsigned" :cols 80 :rows 20 :placeholder "clearsigned") (:br)
  193. (:button :type "submit" "Perform"))))))))
  194. (restas:define-route posting-edit/post ("posting/:hash/" :method :post)
  195. (let ((post (first (pomo:select-dao 'data::posting (:= 'hash hash))))
  196. (clearsigned (hunchentoot:post-parameter "clearsigned"))
  197. (action (hunchentoot:post-parameter "action")))
  198. (if post
  199. (if clearsigned
  200. (handler-case
  201. (let ((posting (process-command clearsigned)))
  202. (restas:redirect 'posting :hash (data::posting-hash posting)))
  203. (command-error (err)
  204. (log:warn "Error processing command ~A" err)
  205. (list :error err
  206. :post post
  207. :action action
  208. :clearsigned clearsigned)))
  209. (let ((title (hunchentoot:post-parameter "title"))
  210. (body (hunchentoot:post-parameter "body")))
  211. (if (and (equal action "Edit")
  212. (or (equal title "") (equal body "")))
  213. (restas:redirect 'posting :hash hash)
  214. (list
  215. :post post
  216. :action action
  217. :to-sign (cond
  218. ((equal action "Update")
  219. (format-update-posting post title body))
  220. ((equal action "Close")
  221. (format-close-posting post))
  222. ((equal action "Delete")
  223. (format-delete-posting post)))))))
  224. hunchentoot:+HTTP-NOT-FOUND+)))