Fossil SCM

Begin reimplementing the forum webpages. This is a non-functional incremental check-in.

drh 2018-07-22 18:14 forum-v2
Commit 2b8b189418148caa42e32f760e2d1b578a506c5066ad7c98920220fabf788db1
3 files changed +80 -316 +8 -2 +14 -3
+80 -316
--- src/forum.c
+++ src/forum.c
@@ -20,336 +20,100 @@
2020
#include "config.h"
2121
#include <assert.h>
2222
#include "forum.h"
2323
2424
/*
25
-** The schema for the tables that manage the forum, if forum is
26
-** enabled.
27
-*/
28
-static const char zForumInit[] =
29
-@ CREATE TABLE repository.forumpost(
30
-@ mpostid INTEGER PRIMARY KEY, -- unique id for each post (local)
31
-@ mposthash TEXT, -- uuid for this post
32
-@ mthreadid INTEGER, -- thread to which this post belongs
33
-@ uname TEXT, -- name of user
34
-@ mtime REAL, -- julian day number
35
-@ mstatus TEXT, -- status. NULL=ok. 'mod'=pending moderation
36
-@ mimetype TEXT, -- Mimetype for mbody
37
-@ ipaddr TEXT, -- IP address of post origin
38
-@ inreplyto INT, -- Parent posting
39
-@ mbody TEXT -- Content of the post
40
-@ );
41
-@ CREATE INDEX repository.forumpost_x1 ON
42
-@ forumpost(inreplyto,mtime);
43
-@ CREATE TABLE repository.forumthread(
44
-@ mthreadid INTEGER PRIMARY KEY,
45
-@ mthreadhash TEXT, -- uuid for this thread
46
-@ mtitle TEXT, -- Title or subject line
47
-@ mtime REAL, -- Most recent update
48
-@ npost INT -- Number of posts on this thread
49
-@ );
50
-;
51
-
52
-/*
53
-** Create the forum tables in the schema if they do not already
54
-** exist.
55
-*/
56
-static void forum_verify_schema(void){
57
- if( !db_table_exists("repository","forumpost") ){
58
- db_multi_exec(zForumInit /*works-like:""*/);
59
- }
60
-}
61
-
62
-/*
63
-** WEBPAGE: forum
64
-** URL: /forum
65
-** Query parameters:
66
-**
67
-** item=N Show post N and its replies
68
-**
69
-*/
70
-void forum_page(void){
71
- int itemId;
72
- Stmt q;
73
- int i;
74
-
75
- login_check_credentials();
76
- if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
77
- forum_verify_schema();
78
- style_header("Forum");
79
- itemId = atoi(PD("item","0"));
80
- if( itemId>0 ){
81
- int iUp;
82
- double rNow;
83
- style_submenu_element("Topics", "%R/forum");
84
- iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId);
85
- if( iUp ){
86
- style_submenu_element("Parent", "%R/forum?item=%d", iUp);
87
- }
88
- rNow = db_double(0.0, "SELECT julianday('now')");
89
- /* Show the post given by itemId and all its descendents */
90
- db_prepare(&q,
91
- "WITH RECURSIVE"
92
- " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS ("
93
- " SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody,"
94
- " 0, mtime FROM forumpost WHERE mpostid=%d"
95
- " UNION"
96
- " SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr,"
97
- " f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime"
98
- " FROM forumpost AS f, post AS p"
99
- " WHERE f.inreplyto=p.id"
100
- " ORDER BY xdepth DESC, xtime ASC"
101
- ") SELECT * FROM post;",
102
- itemId
103
- );
104
- while( db_step(&q)==SQLITE_ROW ){
105
- int id = db_column_int(&q, 0);
106
- const char *zUser = db_column_text(&q, 1);
107
- const char *zMime = db_column_text(&q, 3);
108
- int iDepth = db_column_int(&q, 7);
109
- double rMTime = db_column_double(&q, 8);
110
- char *zAge = db_timespan_name(rNow - rMTime);
111
- Blob body;
112
- @ <!-- Forum post %d(id) -->
113
- @ <table class="forum_post">
114
- @ <tr>
115
- @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2">
116
- @ <td><span class="forum_author">%h(zUser)</span>
117
- @ <span class="forum_age">%s(zAge) ago</span>
118
- sqlite3_free(zAge);
119
- if( g.perm.WrForum ){
120
- @ <span class="forum_buttons">
121
- if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){
122
- @ <a href='%R/forumedit?item=%d(id)'>Edit</a>
123
- }
124
- @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a>
125
- @ </span>
126
- }
127
- @ </tr>
128
- @ <tr><td><div class="forum_body">
129
- blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6));
130
- wiki_render_by_mimetype(&body, zMime);
131
- blob_reset(&body);
132
- @ </div></td></tr>
133
- @ </table>
134
- }
135
- }else{
136
- /* If we reach this point, that means the users wants a list of
137
- ** recent threads.
138
- */
139
- i = 0;
140
- db_prepare(&q,
141
- "SELECT a.mtitle, a.npost, b.mpostid"
142
- " FROM forumthread AS a, forumpost AS b "
143
- " WHERE a.mthreadid=b.mthreadid"
144
- " AND b.inreplyto IS NULL"
145
- " ORDER BY a.mtime DESC LIMIT 40"
146
- );
147
- if( g.perm.WrForum ){
148
- style_submenu_element("New", "%R/forumedit");
149
- }
150
- @ <h1>Recent Forum Threads</h1>
151
- while( db_step(&q)==SQLITE_ROW ){
152
- int n = db_column_int(&q,1);
153
- int itemid = db_column_int(&q,2);
154
- const char *zTitle = db_column_text(&q,0);
155
- if( (i++)==0 ){
156
- @ <ol>
157
- }
158
- @ <li><span class="forum_title">
159
- @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span>
160
- @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li>
161
- }
162
- if( i ){
163
- @ </ol>
164
- }
165
- }
25
+** Display all posts in a forum thread in chronological order
26
+*/
27
+static void forum_thread_chronological(int froot){
28
+ Stmt q;
29
+ db_prepare(&q, "SELECT fpid FROM forumpost WHERE froot=%d"
30
+ " ORDER BY fmtime", froot);
31
+ while( db_step(&q)==SQLITE_ROW ){
32
+ int fpid = db_column_int(&q, 0);
33
+ Manifest *pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
34
+ if( pPost==0 ) continue;
35
+ manifest_destroy(pPost);
36
+ }
37
+ db_finalize(&q);
38
+}
39
+
40
+/*
41
+** WEBPAGE: forumthread
42
+**
43
+** Show all forum messages associated with a particular message thread.
44
+**
45
+** Query parameters:
46
+**
47
+** name=X The hash of the first post of the thread. REQUIRED
48
+*/
49
+void forumthread_page(void){
50
+ int fpid;
51
+ int froot;
52
+ const char *zName = P("name");
53
+ login_check_credentials();
54
+ if( !g.perm.RdForum ){
55
+ login_needed(g.anon.RdForum);
56
+ return;
57
+ }
58
+ style_header("Forum");
59
+ if( zName==0 ){
60
+ @ <p class='generalError'>Missing name= query parameter</p>
61
+ style_footer();
62
+ return;
63
+ }
64
+ fpid = symbolic_name_to_rid(zName, "f");
65
+ if( fpid<=0 ){
66
+ @ <p class='generalError'>Unknown or ambiguous forum id in the "name="
67
+ @ query parameter</p>
68
+ style_footer();
69
+ return;
70
+ }
71
+ froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
72
+ if( froot==0 ){
73
+ @ <p class='generalError'>Invalid forum id in the "name="
74
+ @ query parameter</p>
75
+ style_footer();
76
+ return;
77
+ }
78
+ forum_thread_chronological(froot);
79
+ style_footer();
80
+}
81
+
82
+/*
83
+** WEBPAGE: forumnew
84
+**
85
+** Start a new forum thread.
86
+*/
87
+void forumnew_page(void){
88
+ style_header("Pending");
89
+ @ TBD...
16690
style_footer();
16791
}
16892
16993
/*
170
-** Use content in CGI parameters "s" (subject), "b" (body), and
171
-** "mimetype" (mimetype) to create a new forum entry.
172
-** Return the id of the new forum entry.
173
-**
174
-** If any problems occur, return 0 and set *pzErr to a description of
175
-** the problem.
176
-**
177
-** Cases:
178
-**
179
-** itemId==0 && parentId==0 Starting a new thread.
180
-** itemId==0 && parentId>0 New reply to parentId
181
-** itemId>0 && parentId==0 Edit existing post itemId
94
+** WEBPAGE: forumreply
95
+**
96
+** Reply to a forum message.
97
+** Query parameters:
98
+**
99
+** name=X Hash of the post to reply to. REQUIRED
182100
*/
183
-static int forum_post(int itemId, int parentId, char **pzErr){
184
- const char *zSubject = 0;
185
- int threadId;
186
- double rNow = db_double(0.0, "SELECT julianday('now')");
187
- const char *zMime = wiki_filter_mimetypes(P("mimetype"));
188
- if( itemId==0 && parentId==0 ){
189
- /* Start a new thread. Subject required. */
190
- sqlite3_uint64 r1, r2;
191
- zSubject = PT("s");
192
- if( zSubject==0 || zSubject[0]==0 ){
193
- *pzErr = "\"Subject\" required to start a new thread";
194
- return 0;
195
- }
196
- sqlite3_randomness(sizeof(r1), &r1);
197
- sqlite3_randomness(sizeof(r2), &r2);
198
- db_multi_exec(
199
- "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)"
200
- "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)",
201
- zSubject, rNow
202
- );
203
- threadId = db_last_insert_rowid();
204
- }else{
205
- threadId = db_int(0, "SELECT mthreadid FROM forumpost"
206
- " WHERE mpostid=%d", itemId ? itemId : parentId);
207
- }
208
- if( itemId ){
209
- if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
210
- " WHERE mpostid=%d", itemId) ){
211
- db_multi_exec(
212
- "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d",
213
- PT("s"), threadId
214
- );
215
- }
216
- db_multi_exec(
217
- "UPDATE forumpost SET"
218
- " mtime=%!.17g,"
219
- " mimetype=%Q,"
220
- " ipaddr=%Q,"
221
- " mbody=%Q"
222
- " WHERE mpostid=%d",
223
- rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId
224
- );
225
- }else{
226
- db_multi_exec(
227
- "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime,"
228
- " mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES"
229
- " (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)",
230
- threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b"));
231
- itemId = db_last_insert_rowid();
232
- }
233
- if( zSubject==0 ){
234
- db_multi_exec(
235
- "UPDATE forumthread SET mtime=%!.17g, npost=npost+1"
236
- " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)",
237
- rNow, itemId
238
- );
239
- }
240
- return itemId;
101
+void forumreply_page(void){
102
+ style_header("Pending");
103
+ @ TBD...
104
+ style_footer();
241105
}
242106
243107
/*
244108
** WEBPAGE: forumedit
245109
**
110
+** Edit an existing forum message.
246111
** Query parameters:
247112
**
248
-** replyto=N Enter a reply to forum item N
249
-** item=N Edit item N
250
-** s=SUBJECT Subject. New thread only. Omitted for replies
251
-** b=BODY Body of the post
252
-** m=MIMETYPE Mimetype for the body of the post
253
-** x Submit changes
254
-** p Preview changes
113
+** name=X Hash of the post to be editted. REQUIRED
255114
*/
256
-void forum_edit_page(void){
257
- int itemId;
258
- int parentId;
259
- char *zErr = 0;
260
- const char *zMime;
261
- const char *zSub;
262
-
263
- login_check_credentials();
264
- if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
265
- forum_verify_schema();
266
- itemId = atoi(PD("item","0"));
267
- parentId = atoi(PD("replyto","0"));
268
- if( P("cancel")!=0 ){
269
- cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId);
270
- return;
271
- }
272
- if( P("x")!=0 && cgi_csrf_safe(1) ){
273
- itemId = forum_post(itemId,parentId,&zErr);
274
- if( itemId ){
275
- cgi_redirectf("%R/forum?item=%d",itemId);
276
- return;
277
- }
278
- }
279
- if( itemId && (P("mimetype")==0 || P("b")==0) ){
280
- Stmt q;
281
- db_prepare(&q, "SELECT mimetype, mbody FROM forumpost"
282
- " WHERE mpostid=%d", itemId);
283
- if( db_step(&q)==SQLITE_ROW ){
284
- if( P("mimetype")==0 ){
285
- cgi_set_query_parameter("mimetype", db_column_text(&q, 0));
286
- }
287
- if( P("b")==0 ){
288
- cgi_set_query_parameter("b", db_column_text(&q, 1));
289
- }
290
- }
291
- db_finalize(&q);
292
- }
293
- zMime = wiki_filter_mimetypes(P("mimetype"));
294
- if( itemId>0 ){
295
- style_header("Edit Forum Post");
296
- }else if( parentId>0 ){
297
- style_header("Comment On Forum Post");
298
- }else{
299
- style_header("New Forum Thread");
300
- }
301
- @ <form action="%R/forumedit" method="POST">
302
- if( itemId ){
303
- @ <input type="hidden" name="item" value="%d(itemId)">
304
- }
305
- if( parentId ){
306
- @ <input type="hidden" name="replyto" value="%d(parentId)">
307
- }
308
- if( P("p") ){
309
- Blob x;
310
- @ <div class="forumpreview">
311
- if( P("s") ){
312
- @ <h1>%h(PT("s"))</h1>
313
- }
314
- @ <div class="forumpreviewbody">
315
- blob_init(&x, PT("b"), -1);
316
- wiki_render_by_mimetype(&x, PT("mimetype"));
317
- blob_reset(&x);
318
- @ </div>
319
- @ </div>
320
- @ <hr>
321
- }
322
- @ <table border="0" class="forumeditform">
323
- if( zErr ){
324
- @ <tr><td colspan="2">
325
- @ <span class='forumFormErr'>%h(zErr)</span>
326
- }
327
- if( (itemId==0 && parentId==0)
328
- || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
329
- " WHERE mpostid=%d", itemId))
330
- ){
331
- zSub = PT("s");
332
- if( zSub==0 && itemId ){
333
- zSub = db_text("",
334
- "SELECT mtitle FROM forumthread"
335
- " WHERE mthreadid=(SELECT mthreadid FROM forumpost"
336
- " WHERE mpostid=%d)", itemId);
337
- }
338
- @ <tr><td>Subject:</td>
339
- @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'>
340
- }
341
- @ <tr><td>Markup:</td><td>
342
- mimetype_option_menu(zMime);
343
- @ <tr><td>Comment:</td><td>
344
- @ <textarea name="b" class="wikiedit" cols="80"\
345
- @ rows="20" wrap="virtual">%h(PD("b",""))</textarea></td>
346
- @ <tr><td></td><td>
347
- @ <input type="submit" name="p" value="Preview">
348
- if( P("p")!=0 ){
349
- @ <input type="submit" name="x" value="Submit">
350
- }
351
- @ <input type="submit" name="cancel" value="Cancel">
352
- @ </table>
353
- @ </form>
115
+void forumedit_page(void){
116
+ style_header("Pending");
117
+ @ TBD...
354118
style_footer();
355119
}
356120
--- src/forum.c
+++ src/forum.c
@@ -20,336 +20,100 @@
20 #include "config.h"
21 #include <assert.h>
22 #include "forum.h"
23
24 /*
25 ** The schema for the tables that manage the forum, if forum is
26 ** enabled.
27 */
28 static const char zForumInit[] =
29 @ CREATE TABLE repository.forumpost(
30 @ mpostid INTEGER PRIMARY KEY, -- unique id for each post (local)
31 @ mposthash TEXT, -- uuid for this post
32 @ mthreadid INTEGER, -- thread to which this post belongs
33 @ uname TEXT, -- name of user
34 @ mtime REAL, -- julian day number
35 @ mstatus TEXT, -- status. NULL=ok. 'mod'=pending moderation
36 @ mimetype TEXT, -- Mimetype for mbody
37 @ ipaddr TEXT, -- IP address of post origin
38 @ inreplyto INT, -- Parent posting
39 @ mbody TEXT -- Content of the post
40 @ );
41 @ CREATE INDEX repository.forumpost_x1 ON
42 @ forumpost(inreplyto,mtime);
43 @ CREATE TABLE repository.forumthread(
44 @ mthreadid INTEGER PRIMARY KEY,
45 @ mthreadhash TEXT, -- uuid for this thread
46 @ mtitle TEXT, -- Title or subject line
47 @ mtime REAL, -- Most recent update
48 @ npost INT -- Number of posts on this thread
49 @ );
50 ;
51
52 /*
53 ** Create the forum tables in the schema if they do not already
54 ** exist.
55 */
56 static void forum_verify_schema(void){
57 if( !db_table_exists("repository","forumpost") ){
58 db_multi_exec(zForumInit /*works-like:""*/);
59 }
60 }
61
62 /*
63 ** WEBPAGE: forum
64 ** URL: /forum
65 ** Query parameters:
66 **
67 ** item=N Show post N and its replies
68 **
69 */
70 void forum_page(void){
71 int itemId;
72 Stmt q;
73 int i;
74
75 login_check_credentials();
76 if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
77 forum_verify_schema();
78 style_header("Forum");
79 itemId = atoi(PD("item","0"));
80 if( itemId>0 ){
81 int iUp;
82 double rNow;
83 style_submenu_element("Topics", "%R/forum");
84 iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId);
85 if( iUp ){
86 style_submenu_element("Parent", "%R/forum?item=%d", iUp);
87 }
88 rNow = db_double(0.0, "SELECT julianday('now')");
89 /* Show the post given by itemId and all its descendents */
90 db_prepare(&q,
91 "WITH RECURSIVE"
92 " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS ("
93 " SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody,"
94 " 0, mtime FROM forumpost WHERE mpostid=%d"
95 " UNION"
96 " SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr,"
97 " f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime"
98 " FROM forumpost AS f, post AS p"
99 " WHERE f.inreplyto=p.id"
100 " ORDER BY xdepth DESC, xtime ASC"
101 ") SELECT * FROM post;",
102 itemId
103 );
104 while( db_step(&q)==SQLITE_ROW ){
105 int id = db_column_int(&q, 0);
106 const char *zUser = db_column_text(&q, 1);
107 const char *zMime = db_column_text(&q, 3);
108 int iDepth = db_column_int(&q, 7);
109 double rMTime = db_column_double(&q, 8);
110 char *zAge = db_timespan_name(rNow - rMTime);
111 Blob body;
112 @ <!-- Forum post %d(id) -->
113 @ <table class="forum_post">
114 @ <tr>
115 @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2">
116 @ <td><span class="forum_author">%h(zUser)</span>
117 @ <span class="forum_age">%s(zAge) ago</span>
118 sqlite3_free(zAge);
119 if( g.perm.WrForum ){
120 @ <span class="forum_buttons">
121 if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){
122 @ <a href='%R/forumedit?item=%d(id)'>Edit</a>
123 }
124 @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a>
125 @ </span>
126 }
127 @ </tr>
128 @ <tr><td><div class="forum_body">
129 blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6));
130 wiki_render_by_mimetype(&body, zMime);
131 blob_reset(&body);
132 @ </div></td></tr>
133 @ </table>
134 }
135 }else{
136 /* If we reach this point, that means the users wants a list of
137 ** recent threads.
138 */
139 i = 0;
140 db_prepare(&q,
141 "SELECT a.mtitle, a.npost, b.mpostid"
142 " FROM forumthread AS a, forumpost AS b "
143 " WHERE a.mthreadid=b.mthreadid"
144 " AND b.inreplyto IS NULL"
145 " ORDER BY a.mtime DESC LIMIT 40"
146 );
147 if( g.perm.WrForum ){
148 style_submenu_element("New", "%R/forumedit");
149 }
150 @ <h1>Recent Forum Threads</h1>
151 while( db_step(&q)==SQLITE_ROW ){
152 int n = db_column_int(&q,1);
153 int itemid = db_column_int(&q,2);
154 const char *zTitle = db_column_text(&q,0);
155 if( (i++)==0 ){
156 @ <ol>
157 }
158 @ <li><span class="forum_title">
159 @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span>
160 @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li>
161 }
162 if( i ){
163 @ </ol>
164 }
165 }
166 style_footer();
167 }
168
169 /*
170 ** Use content in CGI parameters "s" (subject), "b" (body), and
171 ** "mimetype" (mimetype) to create a new forum entry.
172 ** Return the id of the new forum entry.
173 **
174 ** If any problems occur, return 0 and set *pzErr to a description of
175 ** the problem.
176 **
177 ** Cases:
178 **
179 ** itemId==0 && parentId==0 Starting a new thread.
180 ** itemId==0 && parentId>0 New reply to parentId
181 ** itemId>0 && parentId==0 Edit existing post itemId
182 */
183 static int forum_post(int itemId, int parentId, char **pzErr){
184 const char *zSubject = 0;
185 int threadId;
186 double rNow = db_double(0.0, "SELECT julianday('now')");
187 const char *zMime = wiki_filter_mimetypes(P("mimetype"));
188 if( itemId==0 && parentId==0 ){
189 /* Start a new thread. Subject required. */
190 sqlite3_uint64 r1, r2;
191 zSubject = PT("s");
192 if( zSubject==0 || zSubject[0]==0 ){
193 *pzErr = "\"Subject\" required to start a new thread";
194 return 0;
195 }
196 sqlite3_randomness(sizeof(r1), &r1);
197 sqlite3_randomness(sizeof(r2), &r2);
198 db_multi_exec(
199 "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)"
200 "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)",
201 zSubject, rNow
202 );
203 threadId = db_last_insert_rowid();
204 }else{
205 threadId = db_int(0, "SELECT mthreadid FROM forumpost"
206 " WHERE mpostid=%d", itemId ? itemId : parentId);
207 }
208 if( itemId ){
209 if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
210 " WHERE mpostid=%d", itemId) ){
211 db_multi_exec(
212 "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d",
213 PT("s"), threadId
214 );
215 }
216 db_multi_exec(
217 "UPDATE forumpost SET"
218 " mtime=%!.17g,"
219 " mimetype=%Q,"
220 " ipaddr=%Q,"
221 " mbody=%Q"
222 " WHERE mpostid=%d",
223 rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId
224 );
225 }else{
226 db_multi_exec(
227 "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime,"
228 " mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES"
229 " (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)",
230 threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b"));
231 itemId = db_last_insert_rowid();
232 }
233 if( zSubject==0 ){
234 db_multi_exec(
235 "UPDATE forumthread SET mtime=%!.17g, npost=npost+1"
236 " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)",
237 rNow, itemId
238 );
239 }
240 return itemId;
241 }
242
243 /*
244 ** WEBPAGE: forumedit
245 **
 
246 ** Query parameters:
247 **
248 ** replyto=N Enter a reply to forum item N
249 ** item=N Edit item N
250 ** s=SUBJECT Subject. New thread only. Omitted for replies
251 ** b=BODY Body of the post
252 ** m=MIMETYPE Mimetype for the body of the post
253 ** x Submit changes
254 ** p Preview changes
255 */
256 void forum_edit_page(void){
257 int itemId;
258 int parentId;
259 char *zErr = 0;
260 const char *zMime;
261 const char *zSub;
262
263 login_check_credentials();
264 if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
265 forum_verify_schema();
266 itemId = atoi(PD("item","0"));
267 parentId = atoi(PD("replyto","0"));
268 if( P("cancel")!=0 ){
269 cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId);
270 return;
271 }
272 if( P("x")!=0 && cgi_csrf_safe(1) ){
273 itemId = forum_post(itemId,parentId,&zErr);
274 if( itemId ){
275 cgi_redirectf("%R/forum?item=%d",itemId);
276 return;
277 }
278 }
279 if( itemId && (P("mimetype")==0 || P("b")==0) ){
280 Stmt q;
281 db_prepare(&q, "SELECT mimetype, mbody FROM forumpost"
282 " WHERE mpostid=%d", itemId);
283 if( db_step(&q)==SQLITE_ROW ){
284 if( P("mimetype")==0 ){
285 cgi_set_query_parameter("mimetype", db_column_text(&q, 0));
286 }
287 if( P("b")==0 ){
288 cgi_set_query_parameter("b", db_column_text(&q, 1));
289 }
290 }
291 db_finalize(&q);
292 }
293 zMime = wiki_filter_mimetypes(P("mimetype"));
294 if( itemId>0 ){
295 style_header("Edit Forum Post");
296 }else if( parentId>0 ){
297 style_header("Comment On Forum Post");
298 }else{
299 style_header("New Forum Thread");
300 }
301 @ <form action="%R/forumedit" method="POST">
302 if( itemId ){
303 @ <input type="hidden" name="item" value="%d(itemId)">
304 }
305 if( parentId ){
306 @ <input type="hidden" name="replyto" value="%d(parentId)">
307 }
308 if( P("p") ){
309 Blob x;
310 @ <div class="forumpreview">
311 if( P("s") ){
312 @ <h1>%h(PT("s"))</h1>
313 }
314 @ <div class="forumpreviewbody">
315 blob_init(&x, PT("b"), -1);
316 wiki_render_by_mimetype(&x, PT("mimetype"));
317 blob_reset(&x);
318 @ </div>
319 @ </div>
320 @ <hr>
321 }
322 @ <table border="0" class="forumeditform">
323 if( zErr ){
324 @ <tr><td colspan="2">
325 @ <span class='forumFormErr'>%h(zErr)</span>
326 }
327 if( (itemId==0 && parentId==0)
328 || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
329 " WHERE mpostid=%d", itemId))
330 ){
331 zSub = PT("s");
332 if( zSub==0 && itemId ){
333 zSub = db_text("",
334 "SELECT mtitle FROM forumthread"
335 " WHERE mthreadid=(SELECT mthreadid FROM forumpost"
336 " WHERE mpostid=%d)", itemId);
337 }
338 @ <tr><td>Subject:</td>
339 @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'>
340 }
341 @ <tr><td>Markup:</td><td>
342 mimetype_option_menu(zMime);
343 @ <tr><td>Comment:</td><td>
344 @ <textarea name="b" class="wikiedit" cols="80"\
345 @ rows="20" wrap="virtual">%h(PD("b",""))</textarea></td>
346 @ <tr><td></td><td>
347 @ <input type="submit" name="p" value="Preview">
348 if( P("p")!=0 ){
349 @ <input type="submit" name="x" value="Submit">
350 }
351 @ <input type="submit" name="cancel" value="Cancel">
352 @ </table>
353 @ </form>
354 style_footer();
355 }
356
--- src/forum.c
+++ src/forum.c
@@ -20,336 +20,100 @@
20 #include "config.h"
21 #include <assert.h>
22 #include "forum.h"
23
24 /*
25 ** Display all posts in a forum thread in chronological order
26 */
27 static void forum_thread_chronological(int froot){
28 Stmt q;
29 db_prepare(&q, "SELECT fpid FROM forumpost WHERE froot=%d"
30 " ORDER BY fmtime", froot);
31 while( db_step(&q)==SQLITE_ROW ){
32 int fpid = db_column_int(&q, 0);
33 Manifest *pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
34 if( pPost==0 ) continue;
35 manifest_destroy(pPost);
36 }
37 db_finalize(&q);
38 }
39
40 /*
41 ** WEBPAGE: forumthread
42 **
43 ** Show all forum messages associated with a particular message thread.
44 **
45 ** Query parameters:
46 **
47 ** name=X The hash of the first post of the thread. REQUIRED
48 */
49 void forumthread_page(void){
50 int fpid;
51 int froot;
52 const char *zName = P("name");
53 login_check_credentials();
54 if( !g.perm.RdForum ){
55 login_needed(g.anon.RdForum);
56 return;
57 }
58 style_header("Forum");
59 if( zName==0 ){
60 @ <p class='generalError'>Missing name= query parameter</p>
61 style_footer();
62 return;
63 }
64 fpid = symbolic_name_to_rid(zName, "f");
65 if( fpid<=0 ){
66 @ <p class='generalError'>Unknown or ambiguous forum id in the "name="
67 @ query parameter</p>
68 style_footer();
69 return;
70 }
71 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
72 if( froot==0 ){
73 @ <p class='generalError'>Invalid forum id in the "name="
74 @ query parameter</p>
75 style_footer();
76 return;
77 }
78 forum_thread_chronological(froot);
79 style_footer();
80 }
81
82 /*
83 ** WEBPAGE: forumnew
84 **
85 ** Start a new forum thread.
86 */
87 void forumnew_page(void){
88 style_header("Pending");
89 @ TBD...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90 style_footer();
91 }
92
93 /*
94 ** WEBPAGE: forumreply
95 **
96 ** Reply to a forum message.
97 ** Query parameters:
98 **
99 ** name=X Hash of the post to reply to. REQUIRED
 
 
 
 
 
 
100 */
101 void forumreply_page(void){
102 style_header("Pending");
103 @ TBD...
104 style_footer();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105 }
106
107 /*
108 ** WEBPAGE: forumedit
109 **
110 ** Edit an existing forum message.
111 ** Query parameters:
112 **
113 ** name=X Hash of the post to be editted. REQUIRED
 
 
 
 
 
 
114 */
115 void forumedit_page(void){
116 style_header("Pending");
117 @ TBD...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118 style_footer();
119 }
120
+8 -2
--- src/schema.c
+++ src/schema.c
@@ -290,14 +290,20 @@
290290
@ -- and so it makes sense to precompute the set of leaves. There is
291291
@ -- one entry in the following table for each leaf.
292292
@ --
293293
@ CREATE TABLE leaf(rid INTEGER PRIMARY KEY);
294294
@
295
-@ -- Events used to generate a timeline
295
+@ -- Events used to generate a timeline. Type meanings:
296
+@ -- ci Check-ins
297
+@ -- e Technotes
298
+@ -- f Forum posts
299
+@ -- g Tags
300
+@ -- t Ticket changes
301
+@ -- w Wiki page edit
296302
@ --
297303
@ CREATE TABLE event(
298
-@ type TEXT, -- Type of event: 'ci', 'w', 'e', 't', 'g'
304
+@ type TEXT, -- Type of event: ci, e, f, g, t, w
299305
@ mtime DATETIME, -- Time of occurrence. Julian day.
300306
@ objid INTEGER PRIMARY KEY, -- Associated record ID
301307
@ tagid INTEGER, -- Associated ticket or wiki name tag
302308
@ uid INTEGER REFERENCES user, -- User who caused the event
303309
@ bgcolor TEXT, -- Color set by 'bgcolor' property
304310
--- src/schema.c
+++ src/schema.c
@@ -290,14 +290,20 @@
290 @ -- and so it makes sense to precompute the set of leaves. There is
291 @ -- one entry in the following table for each leaf.
292 @ --
293 @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY);
294 @
295 @ -- Events used to generate a timeline
 
 
 
 
 
 
296 @ --
297 @ CREATE TABLE event(
298 @ type TEXT, -- Type of event: 'ci', 'w', 'e', 't', 'g'
299 @ mtime DATETIME, -- Time of occurrence. Julian day.
300 @ objid INTEGER PRIMARY KEY, -- Associated record ID
301 @ tagid INTEGER, -- Associated ticket or wiki name tag
302 @ uid INTEGER REFERENCES user, -- User who caused the event
303 @ bgcolor TEXT, -- Color set by 'bgcolor' property
304
--- src/schema.c
+++ src/schema.c
@@ -290,14 +290,20 @@
290 @ -- and so it makes sense to precompute the set of leaves. There is
291 @ -- one entry in the following table for each leaf.
292 @ --
293 @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY);
294 @
295 @ -- Events used to generate a timeline. Type meanings:
296 @ -- ci Check-ins
297 @ -- e Technotes
298 @ -- f Forum posts
299 @ -- g Tags
300 @ -- t Ticket changes
301 @ -- w Wiki page edit
302 @ --
303 @ CREATE TABLE event(
304 @ type TEXT, -- Type of event: ci, e, f, g, t, w
305 @ mtime DATETIME, -- Time of occurrence. Julian day.
306 @ objid INTEGER PRIMARY KEY, -- Associated record ID
307 @ tagid INTEGER, -- Associated ticket or wiki name tag
308 @ uid INTEGER REFERENCES user, -- User who caused the event
309 @ bgcolor TEXT, -- Color set by 'bgcolor' property
310
+14 -3
--- src/timeline.c
+++ src/timeline.c
@@ -574,11 +574,11 @@
574574
cgi_printf("technote:&nbsp;");
575575
hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
576576
}else{
577577
cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
578578
}
579
- }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' ){
579
+ }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' || zType[0]=='f'){
580580
cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
581581
}
582582
583583
if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
584584
char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate);
@@ -1031,11 +1031,11 @@
10311031
** set the y= parameter that determines which elements to display
10321032
** on the timeline.
10331033
*/
10341034
static void timeline_y_submenu(int isDisabled){
10351035
static int i = 0;
1036
- static const char *az[12];
1036
+ static const char *az[14];
10371037
if( i==0 ){
10381038
az[0] = "all";
10391039
az[1] = "Any Type";
10401040
i = 2;
10411041
if( g.perm.Read ){
@@ -1053,10 +1053,14 @@
10531053
az[i++] = "Tickets";
10541054
}
10551055
if( g.perm.RdWiki ){
10561056
az[i++] = "w";
10571057
az[i++] = "Wiki";
1058
+ }
1059
+ if( g.perm.RdForum ){
1060
+ az[i++] = "f";
1061
+ az[i++] = "Forum";
10581062
}
10591063
assert( i<=count(az) );
10601064
}
10611065
if( i>2 ){
10621066
style_submenu_multichoice("y", i/2, az, isDisabled);
@@ -1357,11 +1361,11 @@
13571361
** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
13581362
** rel Show related check-ins as well as those matching t=TAG
13591363
** mionly Limit rel to show ancestors but not descendants
13601364
** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
13611365
** u=USER Only show items associated with USER
1362
-** y=TYPE 'ci', 'w', 't', 'e', or 'all'.
1366
+** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
13631367
** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
13641368
** advm Use the "Advanced" or "Busy" menu design.
13651369
** ng No Graph.
13661370
** nd Do not highlight the focus check-in
13671371
** v Show details of files changed
@@ -1854,10 +1858,11 @@
18541858
if( (zType[0]=='w' && !g.perm.RdWiki)
18551859
|| (zType[0]=='t' && !g.perm.RdTkt)
18561860
|| (zType[0]=='e' && !g.perm.RdWiki)
18571861
|| (zType[0]=='c' && !g.perm.Read)
18581862
|| (zType[0]=='g' && !g.perm.Read)
1863
+ || (zType[0]=='f' && !g.perm.RdForum)
18591864
){
18601865
zType = "all";
18611866
}
18621867
if( zType[0]=='a' ){
18631868
if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){
@@ -1872,10 +1877,14 @@
18721877
cSep = ',';
18731878
}
18741879
if( g.perm.RdTkt ){
18751880
blob_append_sql(&cond, "%c't'", cSep);
18761881
cSep = ',';
1882
+ }
1883
+ if( g.perm.RdForum ){
1884
+ blob_append_sql(&cond, "%c'f'", cSep);
1885
+ cSep = ',';
18771886
}
18781887
blob_append_sql(&cond, ")");
18791888
}
18801889
}else{ /* zType!="all" */
18811890
blob_append_sql(&cond, " AND event.type=%Q", zType);
@@ -1887,10 +1896,12 @@
18871896
zEType = "ticket change";
18881897
}else if( zType[0]=='e' ){
18891898
zEType = "technical note";
18901899
}else if( zType[0]=='g' ){
18911900
zEType = "tag";
1901
+ }else if( zType[0]=='f' ){
1902
+ zEType = "forum post";
18921903
}
18931904
}
18941905
if( zUser ){
18951906
int n = db_int(0,"SELECT count(*) FROM event"
18961907
" WHERE user=%Q OR euser=%Q", zUser, zUser);
18971908
--- src/timeline.c
+++ src/timeline.c
@@ -574,11 +574,11 @@
574 cgi_printf("technote:&nbsp;");
575 hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
576 }else{
577 cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
578 }
579 }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' ){
580 cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
581 }
582
583 if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
584 char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate);
@@ -1031,11 +1031,11 @@
1031 ** set the y= parameter that determines which elements to display
1032 ** on the timeline.
1033 */
1034 static void timeline_y_submenu(int isDisabled){
1035 static int i = 0;
1036 static const char *az[12];
1037 if( i==0 ){
1038 az[0] = "all";
1039 az[1] = "Any Type";
1040 i = 2;
1041 if( g.perm.Read ){
@@ -1053,10 +1053,14 @@
1053 az[i++] = "Tickets";
1054 }
1055 if( g.perm.RdWiki ){
1056 az[i++] = "w";
1057 az[i++] = "Wiki";
 
 
 
 
1058 }
1059 assert( i<=count(az) );
1060 }
1061 if( i>2 ){
1062 style_submenu_multichoice("y", i/2, az, isDisabled);
@@ -1357,11 +1361,11 @@
1357 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1358 ** rel Show related check-ins as well as those matching t=TAG
1359 ** mionly Limit rel to show ancestors but not descendants
1360 ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1361 ** u=USER Only show items associated with USER
1362 ** y=TYPE 'ci', 'w', 't', 'e', or 'all'.
1363 ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
1364 ** advm Use the "Advanced" or "Busy" menu design.
1365 ** ng No Graph.
1366 ** nd Do not highlight the focus check-in
1367 ** v Show details of files changed
@@ -1854,10 +1858,11 @@
1854 if( (zType[0]=='w' && !g.perm.RdWiki)
1855 || (zType[0]=='t' && !g.perm.RdTkt)
1856 || (zType[0]=='e' && !g.perm.RdWiki)
1857 || (zType[0]=='c' && !g.perm.Read)
1858 || (zType[0]=='g' && !g.perm.Read)
 
1859 ){
1860 zType = "all";
1861 }
1862 if( zType[0]=='a' ){
1863 if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){
@@ -1872,10 +1877,14 @@
1872 cSep = ',';
1873 }
1874 if( g.perm.RdTkt ){
1875 blob_append_sql(&cond, "%c't'", cSep);
1876 cSep = ',';
 
 
 
 
1877 }
1878 blob_append_sql(&cond, ")");
1879 }
1880 }else{ /* zType!="all" */
1881 blob_append_sql(&cond, " AND event.type=%Q", zType);
@@ -1887,10 +1896,12 @@
1887 zEType = "ticket change";
1888 }else if( zType[0]=='e' ){
1889 zEType = "technical note";
1890 }else if( zType[0]=='g' ){
1891 zEType = "tag";
 
 
1892 }
1893 }
1894 if( zUser ){
1895 int n = db_int(0,"SELECT count(*) FROM event"
1896 " WHERE user=%Q OR euser=%Q", zUser, zUser);
1897
--- src/timeline.c
+++ src/timeline.c
@@ -574,11 +574,11 @@
574 cgi_printf("technote:&nbsp;");
575 hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
576 }else{
577 cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
578 }
579 }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' || zType[0]=='f'){
580 cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
581 }
582
583 if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
584 char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate);
@@ -1031,11 +1031,11 @@
1031 ** set the y= parameter that determines which elements to display
1032 ** on the timeline.
1033 */
1034 static void timeline_y_submenu(int isDisabled){
1035 static int i = 0;
1036 static const char *az[14];
1037 if( i==0 ){
1038 az[0] = "all";
1039 az[1] = "Any Type";
1040 i = 2;
1041 if( g.perm.Read ){
@@ -1053,10 +1053,14 @@
1053 az[i++] = "Tickets";
1054 }
1055 if( g.perm.RdWiki ){
1056 az[i++] = "w";
1057 az[i++] = "Wiki";
1058 }
1059 if( g.perm.RdForum ){
1060 az[i++] = "f";
1061 az[i++] = "Forum";
1062 }
1063 assert( i<=count(az) );
1064 }
1065 if( i>2 ){
1066 style_submenu_multichoice("y", i/2, az, isDisabled);
@@ -1357,11 +1361,11 @@
1361 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1362 ** rel Show related check-ins as well as those matching t=TAG
1363 ** mionly Limit rel to show ancestors but not descendants
1364 ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1365 ** u=USER Only show items associated with USER
1366 ** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
1367 ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
1368 ** advm Use the "Advanced" or "Busy" menu design.
1369 ** ng No Graph.
1370 ** nd Do not highlight the focus check-in
1371 ** v Show details of files changed
@@ -1854,10 +1858,11 @@
1858 if( (zType[0]=='w' && !g.perm.RdWiki)
1859 || (zType[0]=='t' && !g.perm.RdTkt)
1860 || (zType[0]=='e' && !g.perm.RdWiki)
1861 || (zType[0]=='c' && !g.perm.Read)
1862 || (zType[0]=='g' && !g.perm.Read)
1863 || (zType[0]=='f' && !g.perm.RdForum)
1864 ){
1865 zType = "all";
1866 }
1867 if( zType[0]=='a' ){
1868 if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){
@@ -1872,10 +1877,14 @@
1877 cSep = ',';
1878 }
1879 if( g.perm.RdTkt ){
1880 blob_append_sql(&cond, "%c't'", cSep);
1881 cSep = ',';
1882 }
1883 if( g.perm.RdForum ){
1884 blob_append_sql(&cond, "%c'f'", cSep);
1885 cSep = ',';
1886 }
1887 blob_append_sql(&cond, ")");
1888 }
1889 }else{ /* zType!="all" */
1890 blob_append_sql(&cond, " AND event.type=%Q", zType);
@@ -1887,10 +1896,12 @@
1896 zEType = "ticket change";
1897 }else if( zType[0]=='e' ){
1898 zEType = "technical note";
1899 }else if( zType[0]=='g' ){
1900 zEType = "tag";
1901 }else if( zType[0]=='f' ){
1902 zEType = "forum post";
1903 }
1904 }
1905 if( zUser ){
1906 int n = db_int(0,"SELECT count(*) FROM event"
1907 " WHERE user=%Q OR euser=%Q", zUser, zUser);
1908

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button