summaryrefslogtreecommitdiffstats
path: root/eliss.el
blob: fbc948ecd0923bb42f0f8585ab2b139ba59859c1 (plain)
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
;;; eliss --- Issue tracking with elnode and org-mode

;;; Commentary:

;; eliss is a web interface for issue tracking with `org-mode'.  It
;; uses elnode to provide the web interface and edits a number of org
;; files to keep track of state and such.

(require 'cl-lib)
(require 'elnode)
(require 'esxml)
(require 'org)

;;; Code:

(defvar eliss-data-directory "~/projects/eliss/projects"
  "The location of the project org files.")

(defmacro with-eliss-http-params (params httpcon &rest body)
  "Bind parameters PARAMS from HTTPCON and execute BODY."
  `(let (,@(mapcar (lambda (p)
                     `(,p (elnode-http-param ,httpcon ,(symbol-name p))))
                   params))
     ,@body))
(put 'with-eliss-http-params 'lisp-indent-function 2)

(defun project-row (project)
  "Print a table row for PROJECT."
  (let ((pname (substring project 0 -4)))
    `(tr (td (a (@ (href ,(concat "/" pname "/"))) ,pname)))))

(defun eliss-page (title &rest body)
  "Create a page with some default necessities.

Set the page's title and brand to TITLE and add BODY in the
container div."
  (concat
   "<!DOCTYPE html>"
   (sxml-to-xml
    `(html
      (head (link (@ (href "http://ryuslash.org/bootstrap2/css/bootstrap.min.css")
                     (type "text/css") (rel "stylesheet")))
            (title ,title))
      (body (div (@ (class "navbar navbar-static-top"))
                 (div (@ (class "navbar-inner"))
                      (div (@ (class "container"))
                           (a (@ (href "/") (class "brand")) ,title))))
            (div (@ (class "container"))
                 ,@body))))))

(defun eliss-project-list (httpcon)
  "Send a list of known projects over HTTPCON."
  (elnode-http-start httpcon 200 '("Content-Type" . "text/html"))
  (elnode-http-return
   httpcon
   (eliss-page
    "project list"
    `(table (@ (class "table"))
            ,@(mapcar #'project-row
                      (directory-files eliss-data-directory nil
                                       "\\.org$"))))))

(defun eliss-control-group (name label &rest plist)
  "Return an often-used common HTML structure.

Set the name and id attributes of the input to NAME, give it the
label LABEL.  The property list PLIST can be used to define
certain other properties.

- `:default' may specify a default value to appear in the input.

- `:placeholder' may specify a placeholder for the input, which
  appears if no value has been given."
  (let ((default (plist-get plist :default))
        (placeholder (plist-get plist :placeholder)))
    `(div (@ (class "control-group"))
          (label (@ (class "control-label")
                    (for ,name)) ,label)
          (div (@ (class "controls"))
               (input (@ (type "text")
                         (name ,name)
                         (id ,name)
                         ,(when placeholder `(placeholder ,placeholder))
                         ,(when default `(value ,default))))))))

(defun eliss-project-page (httpcon)
  "Send a list of issues and an issue-creation form over HTTPCON."
  (let* ((project (match-string 1 (elnode-http-mapping httpcon)))
         (filename (concat eliss-data-directory "/" project ".org"))
         (buffer (find-file-noselect filename)))
    (elnode-http-start httpcon 200 '("Content-Type" . "text/html"))
    (elnode-http-return
     httpcon
     (eliss-page
      project
      `(table (@ (class "table"))
              ,@(with-current-buffer buffer
                  (org-map-entries
                   (lambda ()
                     `(tr (td ,(org-entry-get (point) "TODO"))
                          (td ,(org-entry-get (point) "CATEGORY"))
                          (td (a (@ (href ,(concat "/" project "/"
                                                   (org-id-get) "/")))
                                 ,(nth 4 (org-heading-components))))
                          (td ,(org-entry-get (point) "TAGS")))) nil nil 'comment)))
      `(form (@ (action ,(concat "/" project "/new/"))
                (method "POST")
                (class "form-horizontal"))
             (fieldset
              (legend "New issue")
              (div (@ (class "row"))
                   (div (@ (class "span5"))
                        ,(eliss-control-group
                          "subject" "Subject"
                          :placeholder "I wanna say...")
                        ,(eliss-control-group "category" "Category"
                                              :default "bug")
                        ,(eliss-control-group "tags" "Tags"
                                              :placeholder "tag1:tag2")
                        ,(eliss-control-group "name" "Name"
                                              :placeholder "John Doe")
                        ,(eliss-control-group
                          "email" "Email"
                          :placeholder "john@example.com")
                        (input (@ (type "hidden")
                                  (name "i-m-human")
                                  (value "t")))
                        (input (@ (type "hidden")
                                  (name "i-r-bot")))
                        (input (@ (type "submit")
                                  (value "Complain")
                                  (class "pull-right"))))
                   (div (@ (class "span7"))
                        (label (@ (for "content")) "Issue:")
                        (textarea (@ (name "content")
                                     (id "content")
                                     (class "span7")
                                     (rows "15")) "")))))))))

(defun eliss-current-entry-content ()
  "Get the content of the org entry currently at point."
  (buffer-substring-no-properties (org-end-of-meta-data-and-drawers)
                                  (org-entry-end-position)))

(defun eliss-issue-page (httpcon)
  "Send an issue-detail page over HTTPCON."
  (let* ((project (match-string 1 (elnode-http-mapping httpcon)))
         (issue (match-string 2 (elnode-http-mapping httpcon)))
         (filename (concat eliss-data-directory "/" project ".org"))
         (buffer (find-file-noselect filename))
         (entry (cdr (org-id-find issue)))
         title)
    (elnode-http-start httpcon 200 '("Content-Type" . "text/html"))
    (with-current-buffer buffer
      (goto-char entry)
      (set 'title (nth 4 (org-heading-components)))
      (elnode-http-return
       httpcon (eliss-page
                project
                `(h1 ,title)
                `(div (@ (class "row"))
                      (div (@ (class "span6"))
                           (dl (@ (class "dl-horizontal"))
                               (dt "Status:")
                               (dd ,(org-entry-get entry "TODO"))))
                      (div (@ (class "span6"))
                           (dl (@ (class "dl-horizontal"))
                               (dt "Tags:")
                               (dd ,(org-entry-get entry "TAGS"))
                               (dt "Category:")
                               (dd ,(org-entry-get entry "CATEGORY")))))
                `(pre ,(eliss-current-entry-content))
                `(div ,@(org-map-entries
                         (lambda ()
                           `(div (h2 ,(nth 4 (org-heading-components)))
                                 (pre ,(eliss-current-entry-content))))
                         "/+COMMENT" 'tree))
                `(form (@ (action ,(concat "/" project "/" issue
                                           "/comment/"))
                          (method "POST")
                          (class "form-horizontal"))
                       (fieldset
                        (legend "New comment")
                        (div (@ (class "row"))
                             (div (@ (class "span5"))
                                  ,(eliss-control-group
                                    "subject" "Subject"
                                    :default (concat "RE: " title)
                                    :required t)
                                  ,(eliss-control-group
                                    "name" "Name"
                                    :placeholder "John Doe"
                                    :required t)
                                  ,(eliss-control-group
                                    "email" "Email"
                                    :placeholder "john@example.com"
                                    :required t)
                                  (input (@ (type "hidden")
                                            (name "i-m-human")
                                            (value "t")))
                                  (input (@ (type "hidden")
                                            (name "i-r-bot")))
                                  (input (@ (type "submit")
                                            (value "Comment")
                                            (class "pull-right"))))
                             (div (@ (class "span7"))
                                  (label (@ (for "content")) "Comment:")
                                  (textarea (@ (name "content")
                                               (id "content")
                                               (class "span7")
                                               (rows "15")) ""))))))))))

(defun eliss-new-issue (httpcon)
  "Parse data from HTTPCON and write a new issue using it."
  (elnode-method httpcon
    (POST
     (let* ((project (match-string 1 (elnode-http-mapping httpcon)))
            (filename (concat eliss-data-directory "/" project ".org"))
            (buffer (find-file-noselect filename)))
       (with-eliss-http-params (subject tags i-m-human i-r-bot content
                                        category name email) httpcon
         (when (and (equal i-m-human "t") (equal i-r-bot ""))
           (with-current-buffer buffer
             (goto-char (point-min))
             (unless (org-at-heading-p)
               (org-forward-heading-same-level nil))
             (org-insert-todo-heading t)
             (insert subject)
             (let ((text-start (point)))
               (insert "\n\n  " content)
               (fill-region text-start (point)))
             (org-set-tags-to tags)
             (org-set-property "CATEGORY" category)
             (org-set-property "ID" (org-id-new project))
             (org-set-property "AuthorName" name)
             (org-set-property "AuthorEmail" email)
             (save-buffer))))
       (elnode-send-redirect httpcon (concat "/" project "/"))))))

(defun eliss-new-comment (httpcon)
  "Parse data from HTTPCON and write a new issue comment using it."
  (elnode-method httpcon
    (POST
     (let* ((project (match-string 1 (elnode-http-mapping httpcon)))
            (issue (match-string 2 (elnode-http-mapping httpcon)))
            (entry (cdr (org-id-find issue)))
            (filename (concat eliss-data-directory "/" project ".org"))
            (buffer (find-file-noselect filename)))
       (with-eliss-http-params (subject name email i-m-human i-r-bot
                                        content) httpcon
         (when (and (equal i-m-human "t") (equal i-r-bot ""))
           (with-current-buffer buffer
             (goto-char entry)
             (org-end-of-subtree)
             (org-insert-heading-after-current)
             (org-demote)
             (insert subject)
             (org-set-property "TODO" "COMMENT")
             (let ((text-start (point)))
               (insert "\n\n" content)
               (fill-region text-start (point)))
             (org-set-property "AuthorName" name)
             (org-set-property "AuthorEmail" email)
             (save-buffer))))
       (elnode-send-redirect httpcon (concat "/" project "/" issue "/"))))))

(defun eliss-handler (httpcon)
  "Send the right requests in HTTPCON to the right functions."
  (elnode-dispatcher
   httpcon
   '(("^/$" . eliss-project-list)
     ("^/\\([-a-zA-Z0-9]+\\)/$" . eliss-project-page)
     ("^/\\([-a-zA-Z0-9]+\\)/new/$" . eliss-new-issue)
     ("^/\\([-a-zA-Z0-9]+\\)/\\([-a-z0-9:]+\\)/$" . eliss-issue-page)
     ("^/\\([-a-zA-Z0-9]+\\)/\\([-a-z0-9:]+\\)/comment/$" . eliss-new-comment))))

(elnode-start 'eliss-handler :port 8028 :host "localhost")

(provide 'eliss)

;;; eliss.el ends here