Support follow-up posting and e-mail notification
[bpt/portal.git] / support.sml
CommitLineData
5a2812ca
AC
1structure Support :> SUPPORT =
2struct
3
4open Util Sql Init
5
6datatype status =
7 NEW
8 | PENDING
9 | CLOSED
10
11type category = { id : int, grp : int, name : string, descr : string }
12type issue = { id : int, usr : int, cat : int, title : string, priv : bool, status : status, stamp : C.timestamp }
13type post = { id : int, usr : int, iss : int, body : string, stamp : C.timestamp }
14type subscription = { usr : int, cat : int }
15
16
17(* Categories *)
18
19fun mkCatRow [id, grp, name, descr] =
20 {id = C.intFromSql id, grp = C.intFromSql grp, name = C.stringFromSql name,
21 descr = C.stringFromSql descr}
22 | mkCatRow row = rowError ("category", row)
23
24fun lookupCategory id =
25 mkCatRow (C.oneRow (getDb ()) ($`SELECT id, grp, name, descr
26 FROM SupCategory
27 WHERE id = ^(C.intToSql id)`))
28
29fun listCategories () =
30 C.map (getDb ()) mkCatRow ($`SELECT id, grp, name, descr
31 FROM SupCategory
32 ORDER BY name`)
33
34fun mkCatRow' (sub :: rest) =
35 (not (C.isNull sub), mkCatRow rest)
36 | mkCatRow' row = Init.rowError ("category'", row)
37
38fun listCategoriesWithSubscriptions usr =
39 C.map (getDb ()) mkCatRow' ($`SELECT cat, id, grp, name, descr
40 FROM SupCategory LEFT OUTER JOIN SupSubscription
41 ON (usr = ^(C.intToSql usr) AND cat = id)
42 ORDER BY name`)
43
44fun addCategory (grp, name, descr) =
45 let
46 val db = getDb ()
47 val id = nextSeq (db, "SupCategorySeq")
48 in
49 C.dml db ($`INSERT INTO SupCategory (id, grp, name, descr)
50 VALUES (^(C.intToSql id), ^(C.intToSql grp), ^(C.stringToSql name), ^(C.stringToSql descr))`);
51 id
52 end
53
54fun modCategory (cat : category) =
55 let
56 val db = getDb ()
57 in
58 ignore (C.dml db ($`UPDATE SupCategory SET
59 grp = ^(C.intToSql (#grp cat)), name = ^(C.stringToSql (#name cat)),
60 descr = ^(C.stringToSql (#descr cat))
61 WHERE id = ^(C.intToSql (#id cat))`))
62 end
63
64fun deleteCategory id =
65 ignore (C.dml (getDb ()) ($`DELETE FROM SupCategory WHERE id = ^(C.intToSql id)`))
66
67
68(* Issues *)
69
70val statusToSql =
71 fn NEW => "0"
72 | PENDING => "1"
73 | CLOSED => "2"
74
75fun statusFromSql v =
76 case C.intFromSql v of
77 0 => NEW
78 | 1 => PENDING
79 | 2 => CLOSED
80 | _ => raise Fail "Bad support issue status"
81
82fun mkIssueRow [id, usr, cat, title, priv, status, stamp] =
83 {id = C.intFromSql id, usr = C.intFromSql usr, cat = C.intFromSql cat,
84 title = C.stringFromSql title, priv = C.boolFromSql priv,
85 status = statusFromSql status, stamp = C.timestampFromSql stamp}
86 | mkIssueRow row = rowError ("issue", row)
87
88fun lookupIssue id =
89 mkIssueRow (C.oneRow (getDb ()) ($`SELECT id, usr, cat, title, priv, status, stamp
90 FROM SupIssue
91 WHERE id = ^(C.intToSql id)`))
92
93fun listIssues () =
94 C.map (getDb ()) mkIssueRow ($`SELECT id, usr, cat, title, priv, status, stamp
95 FROM SupIssue
96 ORDER BY stamp DESC`)
97
1cb3df3f
AC
98fun listCategoryIssues cat =
99 C.map (getDb ()) mkIssueRow ($`SELECT id, usr, cat, title, priv, status, stamp
100 FROM SupIssue
101 WHERE cat = ^(C.intToSql cat)
102 ORDER BY stamp DESC`)
103
104fun listOpenCategoryIssues (cat, usr) =
105 C.map (getDb ()) mkIssueRow ($`SELECT id, usr, cat, title, priv, status, stamp
106 FROM SupIssue
107 WHERE cat = ^(C.intToSql cat)
108 AND status < 2
109 AND (NOT priv OR usr = ^(C.intToSql usr))
110 ORDER BY stamp DESC`)
111
112fun listOpenCategoryIssuesAdmin cat =
113 C.map (getDb ()) mkIssueRow ($`SELECT id, usr, cat, title, priv, status, stamp
114 FROM SupIssue
115 WHERE cat = ^(C.intToSql cat)
116 AND status < 2
117 ORDER BY stamp DESC`)
118
5a2812ca
AC
119fun addIssue (usr, cat, title, priv, status) =
120 let
121 val db = getDb ()
122 val id = nextSeq (db, "SupIssueSeq")
123 in
124 C.dml db ($`INSERT INTO SupIssue (id, usr, cat, title, priv, status, stamp)
125 VALUES (^(C.intToSql id), ^(C.intToSql usr), ^(C.intToSql cat),
126 ^(C.stringToSql title), ^(C.boolToSql priv),
127 ^(statusToSql status), CURRENT_TIMESTAMP)`);
128 id
129 end
130
131fun modIssue (iss : issue) =
132 let
133 val db = getDb ()
134 in
135 ignore (C.dml db ($`UPDATE SupIssue SET
136 usr = ^(C.intToSql (#usr iss)), cat = ^(C.intToSql (#cat iss)),
137 title = ^(C.stringToSql (#title iss)), priv = ^(C.boolToSql (#priv iss)),
138 status = ^(statusToSql (#status iss))
139 WHERE id = ^(C.intToSql (#id iss))`))
140 end
141
142fun deleteIssue id =
143 ignore (C.dml (getDb ()) ($`DELETE FROM SupIssue WHERE id = ^(C.intToSql id)`))
144
145
146(* Posts *)
147
148fun mkPostRow [id, usr, iss, body, stamp] =
149 {id = C.intFromSql id, usr = C.intFromSql usr, iss = C.intFromSql iss,
150 body = C.stringFromSql body, stamp = C.timestampFromSql stamp}
151 | mkPostRow row = rowError ("post", row)
152
153fun lookupPost id =
154 mkPostRow (C.oneRow (getDb ()) ($`SELECT id, usr, iss, body, stamp
155 FROM SupPost
156 WHERE id = ^(C.intToSql id)`))
157
edeb626e
AC
158fun mkPostRow' (name :: rest) = (C.stringFromSql name, mkPostRow rest)
159 | mkPostRow' row = Init.rowError ("post'", row)
160
5a2812ca 161fun listPosts iss =
edeb626e
AC
162 C.map (getDb ()) mkPostRow' ($`SELECT name, SupPost.id, usr, iss, body, SupPost.stamp
163 FROM SupPost JOIN WebUser ON usr = WebUser.id
164 WHERE iss = ^(C.intToSql iss)
165 ORDER BY stamp`)
5a2812ca
AC
166
167fun addPost (usr, iss, body) =
168 let
169 val db = getDb ()
170 val id = nextSeq (db, "SupPostSeq")
171 in
172 C.dml db ($`INSERT INTO SupPost (id, usr, iss, body, stamp)
173 VALUES (^(C.intToSql id), ^(C.intToSql usr), ^(C.intToSql iss),
174 ^(C.stringToSql body), CURRENT_TIMESTAMP)`);
175 id
176 end
177
178fun modPost (post : post) =
179 let
180 val db = getDb ()
181 in
182 ignore (C.dml db ($`UPDATE SupPost SET
183 usr = ^(C.intToSql (#usr post)), iss = ^(C.intToSql (#iss post)),
184 body = ^(C.stringToSql (#body post))
185 WHERE id = ^(C.intToSql (#id post))`))
186 end
187
188fun deletePost id =
189 ignore (C.dml (getDb ()) ($`DELETE FROM SupPost WHERE id = ^(C.intToSql id)`))
190
191
192(* Subscriptions *)
193
194fun mkSubRow [usr, cat] =
195 {usr = C.intFromSql usr, cat = C.intFromSql cat}
196 | mkSubRow row = rowError ("subscription", row)
197
198fun subscribed {usr, cat} =
199 case C.oneRow (getDb ()) ($`SELECT COUNT( * ) FROM SupSubscription
200 WHERE usr = ^(C.intToSql usr) AND cat = ^(C.intToSql cat)`) of
201 [n] => not (C.isNull n) andalso C.intFromSql n > 0
202 | r => Init.rowError ("subscribed", r)
203
204fun subscribe (sub as {usr, cat}) =
205 if subscribed sub then
206 ()
207 else
208 ignore (C.dml (getDb ()) ($`INSERT INTO SupSubscription (usr, cat)
209 VALUES (^(C.intToSql usr), ^(C.intToSql cat))`))
210
211fun unsubscribe {usr, cat} =
212 ignore (C.dml (getDb ()) ($`DELETE FROM SupSubscription
213 WHERE usr = ^(C.intToSql usr) AND cat = ^(C.intToSql cat)`))
214
edeb626e
AC
215val okChars = [#" ", #"-", #".", #"!", #"?", #":", #";", #"'", #"\""]
216
1cb3df3f 217fun validTitle s = CharVector.exists (fn ch => not (Char.isSpace ch)) s
edeb626e
AC
218 andalso CharVector.all (fn ch => Char.isAlphaNum ch orelse List.exists (fn ch' => ch = ch') okChars) s
219
220fun allowedToSee iss =
221 let
222 val iss = lookupIssue iss
223 val cat = lookupCategory (#cat iss)
224 in
225 not (#priv iss) orelse Group.inGroupNum (#grp cat) orelse (Init.getUserId () = #usr iss)
226 end
227
228fun allowedToEdit iss =
229 let
230 val iss = lookupIssue iss
231 val cat = lookupCategory (#cat iss)
232 in
233 Group.inGroupNum (#grp cat) orelse (Init.getUserId () = #usr iss)
234 end
235
236fun writeRecipients (mail, iss : issue, cat : category) =
237 let
238 val query =
239 if #priv iss then
240 $`SELECT name
241 FROM WebUser JOIN Membership ON (usr = id AND grp = ^(C.intToSql (#grp cat)))`
242 else
243 $`SELECT name
244 FROM WebUser JOIN SupSubscription ON (usr = id AND cat = ^(C.intToSql (#id cat)))`
245
246 fun doOne [name] = (Mail.mwrite (mail, C.stringFromSql name);
247 Mail.mwrite (mail, ","))
248 in
249 Mail.mwrite (mail, "Bcc: ");
250 C.app (getDb ()) doOne query;
251 Mail.mwrite (mail, "\n")
252 end
5a2812ca 253
edeb626e
AC
254fun notify (prefix, f) iss =
255 let
256 val iss = lookupIssue iss
257 val cat = lookupCategory (#cat iss)
258 val user = Init.lookupUser (#usr iss)
259
260 val mail = Mail.mopen ()
261 in
262 Mail.mwrite (mail, "From: Hcoop Support System <support@hcoop.net>\nTo: ");
263 Mail.mwrite (mail, #name user);
264 Mail.mwrite (mail, "@hcoop.net\n");
265 writeRecipients (mail, iss, cat);
266 Mail.mwrite (mail, "Subject: ");
267 Mail.mwrite (mail, prefix);
268 Mail.mwrite (mail, #title iss);
269 Mail.mwrite (mail, "\n\nURL: ");
270 Mail.mwrite (mail, Init.urlPrefix);
271 Mail.mwrite (mail, "issue?cat=");
272 Mail.mwrite (mail, C.intToSql (#id cat));
273 Mail.mwrite (mail, "&id=");
274 Mail.mwrite (mail, C.intToSql (#id iss));
275 Mail.mwrite (mail, "\n\nSubmitted by: ");
276 Mail.mwrite (mail, #name user);
277 Mail.mwrite (mail, "\n Category: ");
278 Mail.mwrite (mail, #name cat);
279 Mail.mwrite (mail, "\n Issue: ");
280 Mail.mwrite (mail, #title iss);
281 Mail.mwrite (mail, "\n Private: ");
282 Mail.mwrite (mail, if #priv iss then "yes" else "no");
283 Mail.mwrite (mail, "\n\n");
284
285 f (iss, cat, user, mail);
286
287 OS.Process.isSuccess (Mail.mclose mail)
288 end
289
290val notifyCreation = notify ("[New] ",
291 fn (iss, cat, user, mail) =>
292 (case listPosts (#id iss) of
293 [] => ()
294 | [(_, post)] => Mail.mwrite (mail, #body post)
295 | _ => raise Fail "Too many posts for supposedly new support issue"))
296
297fun notifyPost pid =
298 let
299 val post = lookupPost pid
300 val poster = Init.lookupUser (#usr post)
301 in
302 notify ("[Post] ",
303 fn (iss, cat, user, mail) =>
304 (Mail.mwrite (mail, "New post by ");
305 Mail.mwrite (mail, #name poster);
306 Mail.mwrite (mail, ":\n\n");
307 Mail.mwrite (mail, #body post))) (#iss post)
308 end
309
310val statusToString =
311 fn NEW => "New"
312 | PENDING => "Pending"
313 | CLOSED => "Closed"
314
315fun notifyStatus (usr, oldStatus, newStatus, iss) =
316 let
317 val user = Init.lookupUser usr
318 in
319 notify ("[" ^ statusToString newStatus ^ "] ",
320 fn (iss, cat, user, mail) =>
321 (Mail.mwrite (mail, #name user);
322 Mail.mwrite (mail, " changed status from ");
323 Mail.mwrite (mail, statusToString oldStatus);
324 Mail.mwrite (mail, " to ");
325 Mail.mwrite (mail, statusToString newStatus);
326 Mail.mwrite (mail, ".\n"))) iss
327 end
328
5a2812ca 329end