Fossil SCM

Render a column for each post's status in the /forum list if the repo has a valid forum-statuses setting. These are exploratory, not definitive, changes. It's not clear how to get both a status's label and its value into this list without duplicating one of the inner queries.

stephan 2026-05-26 09:45 UTC forum-statuses
Commit 696c85418d037a84f41d62ea8ce1c25b1a642eaab9e1c715ffa6e8c21bc0737a
1 file changed +49 -24
+49 -24
--- src/forum.c
+++ src/forum.c
@@ -89,11 +89,10 @@
8989
static const ForumStatusList * forum_statuses(void){
9090
static ForumStatusList fses = {0,0};
9191
static int once = 0;
9292
while( !once ){
9393
Stmt q;
94
- char *zSetting;
9594
++once;
9695
/* Read `forum-statuses` setting and transform it into the
9796
** fses object.
9897
**
9998
** Maybe: if it's empty, synthesize a length-1 list from
@@ -100,32 +99,40 @@
10099
** {value:"default",label:"Default",...}. It's expected that
101100
** usage may be slightly simplified if we always have a non-empty
102101
** list. A length-1 list is, for purposes of the UI, identical to
103102
** an empty one - status selection/filtering makes no sense if
104103
** there's only one choice. */
105
- zSetting = db_get("forum-statuses", 0);
104
+ db_get("forum-statuses", 0);
106105
db_multi_exec(
107106
"CREATE TEMP TABLE forumstatus("
108107
" ord, label, value, descr"
109108
");"
110109
);
111
- if( !zSetting ) break;
112110
db_prepare(&q,
113
- " WITH setting(v) AS (SELECT %Q),"
114
- " room(r) AS ("
115
- " SELECT e.value FROM setting s, jsonb_each(s.v) e"
116
- " WHERE json_valid(s.v, 0x02)"
117
- " )"
111
+ "WITH setting(v) AS ("
112
+ " SELECT value v FROM config WHERE name='forum-statuses'"
113
+ "),"
114
+ "room(r) AS ("
115
+ " SELECT e.value FROM setting s, jsonb_each(s.v) e"
116
+ " WHERE json_valid(s.v, 0x02)"
117
+ ")"
118118
" SELECT r->>'label', r->>'value', r->>'description'"
119
- " FROM room",
120
- zSetting
119
+ " FROM room"
121120
);
121
+ /*
122
+ ** Quirk: we run the query twice: once to count and once to insert
123
+ ** into the forumstatus table. We could do the query and populate
124
+ ** in the same step but we need an ordinal value to go with each
125
+ ** so that we can retain their order. If the above CTE can be
126
+ ** altered to provide a sequential ordinal value then we can
127
+ ** eliminate this double-step.
128
+ */
122129
while( SQLITE_ROW==db_step(&q) ){
123130
++fses.n;
124131
}
125132
if( fses.n ){
126
- unsigned int i = 0;
133
+ int i = 0;
127134
Stmt qIns;
128135
fses.aStatus = fossil_malloc(sizeof(fses.aStatus[0]) * fses.n);
129136
db_reset(&q);
130137
db_prepare(&qIns,
131138
"INSERT INTO forumstatus(ord,label,value,descr)"
@@ -134,20 +141,19 @@
134141
ForumStatus * fs = &fses.aStatus[i++];
135142
fs->zLabel = fossil_strdup(db_column_text(&q, 0));
136143
fs->zValue = fossil_strdup(db_column_text(&q, 1));
137144
fs->zDescr = fossil_strdup(db_column_text(&q, 2));
138145
db_reset(&qIns);
139
- db_bind_int(&qIns, ":ord", (int)i);
146
+ db_bind_int(&qIns, ":ord", i);
140147
db_bind_text(&qIns, ":label", fs->zLabel);
141148
db_bind_text(&qIns, ":value", fs->zValue);
142149
db_bind_text(&qIns, ":descr", fs->zDescr);
143150
db_step(&qIns);
144151
}
145152
db_finalize(&qIns);
146153
}
147154
db_finalize(&q);
148
- fossil_free(zSetting);
149155
}
150156
return &fses;
151157
}
152158
153159
/*
@@ -2358,29 +2364,42 @@
23582364
}
23592365
style_submenu_entry("n","Max:",4,0);
23602366
iOfst = atoi(PD("x","0"));
23612367
iCnt = 0;
23622368
if( db_table_exists("repository","forumpost") ){
2369
+ const int bHasStatus = forum_statuses()->n>1;
23632370
db_prepare(&q,
2364
- "WITH thread(age,duration,cnt,root,last,pinned) AS ("
2371
+ "WITH thread(age,duration,cnt,root,last,pinned,status) AS ("
23652372
" SELECT"
23662373
" julianday('now') - max(fmtime),"
23672374
" max(fmtime) - min(fmtime),"
23682375
" sum(fprev IS NULL),"
23692376
" froot,"
23702377
" (SELECT fpid FROM forumpost AS y"
23712378
" WHERE y.froot=x.froot %s"
23722379
" ORDER BY y.fmtime DESC LIMIT 1),"
2373
- " CASE WHEN"
2374
- " firt IS NULL AND"
2375
- " (SELECT 1 FROM tagxref ref, tag t"
2376
- " WHERE ref.rid=x.fpid AND ref.tagtype>0"
2380
+ " (firt IS NULL AND"
2381
+ " (SELECT 1 FROM tagxref ref, tag t"
2382
+ " WHERE ref.rid=x.fpid AND ref.tagtype>0"
2383
+ " AND ref.tagid=t.tagid"
2384
+ " AND t.tagname='pinned')),"
2385
+#if 0
2386
+ " (SELECT ref.value FROM tagxref ref, tag t"
2387
+ " WHERE ref.rid=x.froot AND ref.tagtype>0"
2388
+ " AND ref.tagid=t.tagid"
2389
+ " AND t.tagname='status'"
2390
+ " UNION ALL"
2391
+ " SELECT value FROM forumstatus WHERE ord=1)"
2392
+#else
2393
+ " (SELECT fs.label FROM tagxref ref, tag t, forumstatus fs"
2394
+ " WHERE ref.rid=x.froot AND ref.tagtype>0"
23772395
" AND ref.tagid=t.tagid"
2378
- " AND t.tagname='pinned')"
2379
- " THEN 1"
2380
- " ELSE 0"
2381
- " END"
2396
+ " AND t.tagname='status'"
2397
+ " AND fs.value=ref.value"
2398
+ " UNION ALL"
2399
+ " SELECT label FROM forumstatus WHERE ord=1)"
2400
+#endif
23822401
" FROM forumpost AS x"
23832402
" WHERE %s"
23842403
" GROUP BY froot"
23852404
" ORDER BY 6 DESC, 1 LIMIT %d OFFSET %d"
23862405
")"
@@ -2389,11 +2408,12 @@
23892408
" thread.duration," /* 1 */
23902409
" thread.cnt," /* 2 */
23912410
" blob.uuid," /* 3 */
23922411
" substr(event.comment,instr(event.comment,':')+1)," /* 4 */
23932412
" thread.last," /* 5 */
2394
- " thread.pinned" /* 6 */
2413
+ " thread.pinned," /* 6 */
2414
+ " thread.status" /* 7 */
23952415
" FROM thread, blob, event"
23962416
" WHERE blob.rid=thread.last"
23972417
" AND event.objid=thread.last"
23982418
" ORDER BY 7 DESC, 1;",
23992419
g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/,
@@ -2404,10 +2424,11 @@
24042424
char *zAge = human_readable_age(db_column_double(&q,0));
24052425
int nMsg = db_column_int(&q, 2);
24062426
int bPinned = db_column_int(&q, 6);
24072427
const char *zUuid = db_column_text(&q, 3);
24082428
const char *zTitle = db_column_text(&q, 4);
2429
+ const char *zStatus = bHasStatus ? db_column_text(&q, 7) : NULL;
24092430
if( iCnt==0 ){
24102431
if( iOfst>0 ){
24112432
@ <h1>Threads at least %s(zAge) old</h1>
24122433
}else{
24132434
@ <h1>Most recent threads</h1>
@@ -2444,11 +2465,15 @@
24442465
}else{
24452466
char *zDuration = human_readable_age(db_column_double(&q,1));
24462467
@ %d(nMsg) posts spanning %h(zDuration)\
24472468
fossil_free(zDuration);
24482469
}
2449
- @ </td></tr>
2470
+ @ </td>\
2471
+ if( bHasStatus ){
2472
+ @ <td>%h(zStatus)</td>\
2473
+ }
2474
+ @</tr>
24502475
fossil_free(zAge);
24512476
}
24522477
db_finalize(&q);
24532478
}
24542479
if( iCnt>0 ){
24552480
--- src/forum.c
+++ src/forum.c
@@ -89,11 +89,10 @@
89 static const ForumStatusList * forum_statuses(void){
90 static ForumStatusList fses = {0,0};
91 static int once = 0;
92 while( !once ){
93 Stmt q;
94 char *zSetting;
95 ++once;
96 /* Read `forum-statuses` setting and transform it into the
97 ** fses object.
98 **
99 ** Maybe: if it's empty, synthesize a length-1 list from
@@ -100,32 +99,40 @@
100 ** {value:"default",label:"Default",...}. It's expected that
101 ** usage may be slightly simplified if we always have a non-empty
102 ** list. A length-1 list is, for purposes of the UI, identical to
103 ** an empty one - status selection/filtering makes no sense if
104 ** there's only one choice. */
105 zSetting = db_get("forum-statuses", 0);
106 db_multi_exec(
107 "CREATE TEMP TABLE forumstatus("
108 " ord, label, value, descr"
109 ");"
110 );
111 if( !zSetting ) break;
112 db_prepare(&q,
113 " WITH setting(v) AS (SELECT %Q),"
114 " room(r) AS ("
115 " SELECT e.value FROM setting s, jsonb_each(s.v) e"
116 " WHERE json_valid(s.v, 0x02)"
117 " )"
 
 
118 " SELECT r->>'label', r->>'value', r->>'description'"
119 " FROM room",
120 zSetting
121 );
 
 
 
 
 
 
 
 
122 while( SQLITE_ROW==db_step(&q) ){
123 ++fses.n;
124 }
125 if( fses.n ){
126 unsigned int i = 0;
127 Stmt qIns;
128 fses.aStatus = fossil_malloc(sizeof(fses.aStatus[0]) * fses.n);
129 db_reset(&q);
130 db_prepare(&qIns,
131 "INSERT INTO forumstatus(ord,label,value,descr)"
@@ -134,20 +141,19 @@
134 ForumStatus * fs = &fses.aStatus[i++];
135 fs->zLabel = fossil_strdup(db_column_text(&q, 0));
136 fs->zValue = fossil_strdup(db_column_text(&q, 1));
137 fs->zDescr = fossil_strdup(db_column_text(&q, 2));
138 db_reset(&qIns);
139 db_bind_int(&qIns, ":ord", (int)i);
140 db_bind_text(&qIns, ":label", fs->zLabel);
141 db_bind_text(&qIns, ":value", fs->zValue);
142 db_bind_text(&qIns, ":descr", fs->zDescr);
143 db_step(&qIns);
144 }
145 db_finalize(&qIns);
146 }
147 db_finalize(&q);
148 fossil_free(zSetting);
149 }
150 return &fses;
151 }
152
153 /*
@@ -2358,29 +2364,42 @@
2358 }
2359 style_submenu_entry("n","Max:",4,0);
2360 iOfst = atoi(PD("x","0"));
2361 iCnt = 0;
2362 if( db_table_exists("repository","forumpost") ){
 
2363 db_prepare(&q,
2364 "WITH thread(age,duration,cnt,root,last,pinned) AS ("
2365 " SELECT"
2366 " julianday('now') - max(fmtime),"
2367 " max(fmtime) - min(fmtime),"
2368 " sum(fprev IS NULL),"
2369 " froot,"
2370 " (SELECT fpid FROM forumpost AS y"
2371 " WHERE y.froot=x.froot %s"
2372 " ORDER BY y.fmtime DESC LIMIT 1),"
2373 " CASE WHEN"
2374 " firt IS NULL AND"
2375 " (SELECT 1 FROM tagxref ref, tag t"
2376 " WHERE ref.rid=x.fpid AND ref.tagtype>0"
 
 
 
 
 
 
 
 
 
 
 
2377 " AND ref.tagid=t.tagid"
2378 " AND t.tagname='pinned')"
2379 " THEN 1"
2380 " ELSE 0"
2381 " END"
 
2382 " FROM forumpost AS x"
2383 " WHERE %s"
2384 " GROUP BY froot"
2385 " ORDER BY 6 DESC, 1 LIMIT %d OFFSET %d"
2386 ")"
@@ -2389,11 +2408,12 @@
2389 " thread.duration," /* 1 */
2390 " thread.cnt," /* 2 */
2391 " blob.uuid," /* 3 */
2392 " substr(event.comment,instr(event.comment,':')+1)," /* 4 */
2393 " thread.last," /* 5 */
2394 " thread.pinned" /* 6 */
 
2395 " FROM thread, blob, event"
2396 " WHERE blob.rid=thread.last"
2397 " AND event.objid=thread.last"
2398 " ORDER BY 7 DESC, 1;",
2399 g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/,
@@ -2404,10 +2424,11 @@
2404 char *zAge = human_readable_age(db_column_double(&q,0));
2405 int nMsg = db_column_int(&q, 2);
2406 int bPinned = db_column_int(&q, 6);
2407 const char *zUuid = db_column_text(&q, 3);
2408 const char *zTitle = db_column_text(&q, 4);
 
2409 if( iCnt==0 ){
2410 if( iOfst>0 ){
2411 @ <h1>Threads at least %s(zAge) old</h1>
2412 }else{
2413 @ <h1>Most recent threads</h1>
@@ -2444,11 +2465,15 @@
2444 }else{
2445 char *zDuration = human_readable_age(db_column_double(&q,1));
2446 @ %d(nMsg) posts spanning %h(zDuration)\
2447 fossil_free(zDuration);
2448 }
2449 @ </td></tr>
 
 
 
 
2450 fossil_free(zAge);
2451 }
2452 db_finalize(&q);
2453 }
2454 if( iCnt>0 ){
2455
--- src/forum.c
+++ src/forum.c
@@ -89,11 +89,10 @@
89 static const ForumStatusList * forum_statuses(void){
90 static ForumStatusList fses = {0,0};
91 static int once = 0;
92 while( !once ){
93 Stmt q;
 
94 ++once;
95 /* Read `forum-statuses` setting and transform it into the
96 ** fses object.
97 **
98 ** Maybe: if it's empty, synthesize a length-1 list from
@@ -100,32 +99,40 @@
99 ** {value:"default",label:"Default",...}. It's expected that
100 ** usage may be slightly simplified if we always have a non-empty
101 ** list. A length-1 list is, for purposes of the UI, identical to
102 ** an empty one - status selection/filtering makes no sense if
103 ** there's only one choice. */
104 db_get("forum-statuses", 0);
105 db_multi_exec(
106 "CREATE TEMP TABLE forumstatus("
107 " ord, label, value, descr"
108 ");"
109 );
 
110 db_prepare(&q,
111 "WITH setting(v) AS ("
112 " SELECT value v FROM config WHERE name='forum-statuses'"
113 "),"
114 "room(r) AS ("
115 " SELECT e.value FROM setting s, jsonb_each(s.v) e"
116 " WHERE json_valid(s.v, 0x02)"
117 ")"
118 " SELECT r->>'label', r->>'value', r->>'description'"
119 " FROM room"
 
120 );
121 /*
122 ** Quirk: we run the query twice: once to count and once to insert
123 ** into the forumstatus table. We could do the query and populate
124 ** in the same step but we need an ordinal value to go with each
125 ** so that we can retain their order. If the above CTE can be
126 ** altered to provide a sequential ordinal value then we can
127 ** eliminate this double-step.
128 */
129 while( SQLITE_ROW==db_step(&q) ){
130 ++fses.n;
131 }
132 if( fses.n ){
133 int i = 0;
134 Stmt qIns;
135 fses.aStatus = fossil_malloc(sizeof(fses.aStatus[0]) * fses.n);
136 db_reset(&q);
137 db_prepare(&qIns,
138 "INSERT INTO forumstatus(ord,label,value,descr)"
@@ -134,20 +141,19 @@
141 ForumStatus * fs = &fses.aStatus[i++];
142 fs->zLabel = fossil_strdup(db_column_text(&q, 0));
143 fs->zValue = fossil_strdup(db_column_text(&q, 1));
144 fs->zDescr = fossil_strdup(db_column_text(&q, 2));
145 db_reset(&qIns);
146 db_bind_int(&qIns, ":ord", i);
147 db_bind_text(&qIns, ":label", fs->zLabel);
148 db_bind_text(&qIns, ":value", fs->zValue);
149 db_bind_text(&qIns, ":descr", fs->zDescr);
150 db_step(&qIns);
151 }
152 db_finalize(&qIns);
153 }
154 db_finalize(&q);
 
155 }
156 return &fses;
157 }
158
159 /*
@@ -2358,29 +2364,42 @@
2364 }
2365 style_submenu_entry("n","Max:",4,0);
2366 iOfst = atoi(PD("x","0"));
2367 iCnt = 0;
2368 if( db_table_exists("repository","forumpost") ){
2369 const int bHasStatus = forum_statuses()->n>1;
2370 db_prepare(&q,
2371 "WITH thread(age,duration,cnt,root,last,pinned,status) AS ("
2372 " SELECT"
2373 " julianday('now') - max(fmtime),"
2374 " max(fmtime) - min(fmtime),"
2375 " sum(fprev IS NULL),"
2376 " froot,"
2377 " (SELECT fpid FROM forumpost AS y"
2378 " WHERE y.froot=x.froot %s"
2379 " ORDER BY y.fmtime DESC LIMIT 1),"
2380 " (firt IS NULL AND"
2381 " (SELECT 1 FROM tagxref ref, tag t"
2382 " WHERE ref.rid=x.fpid AND ref.tagtype>0"
2383 " AND ref.tagid=t.tagid"
2384 " AND t.tagname='pinned')),"
2385 #if 0
2386 " (SELECT ref.value FROM tagxref ref, tag t"
2387 " WHERE ref.rid=x.froot AND ref.tagtype>0"
2388 " AND ref.tagid=t.tagid"
2389 " AND t.tagname='status'"
2390 " UNION ALL"
2391 " SELECT value FROM forumstatus WHERE ord=1)"
2392 #else
2393 " (SELECT fs.label FROM tagxref ref, tag t, forumstatus fs"
2394 " WHERE ref.rid=x.froot AND ref.tagtype>0"
2395 " AND ref.tagid=t.tagid"
2396 " AND t.tagname='status'"
2397 " AND fs.value=ref.value"
2398 " UNION ALL"
2399 " SELECT label FROM forumstatus WHERE ord=1)"
2400 #endif
2401 " FROM forumpost AS x"
2402 " WHERE %s"
2403 " GROUP BY froot"
2404 " ORDER BY 6 DESC, 1 LIMIT %d OFFSET %d"
2405 ")"
@@ -2389,11 +2408,12 @@
2408 " thread.duration," /* 1 */
2409 " thread.cnt," /* 2 */
2410 " blob.uuid," /* 3 */
2411 " substr(event.comment,instr(event.comment,':')+1)," /* 4 */
2412 " thread.last," /* 5 */
2413 " thread.pinned," /* 6 */
2414 " thread.status" /* 7 */
2415 " FROM thread, blob, event"
2416 " WHERE blob.rid=thread.last"
2417 " AND event.objid=thread.last"
2418 " ORDER BY 7 DESC, 1;",
2419 g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/,
@@ -2404,10 +2424,11 @@
2424 char *zAge = human_readable_age(db_column_double(&q,0));
2425 int nMsg = db_column_int(&q, 2);
2426 int bPinned = db_column_int(&q, 6);
2427 const char *zUuid = db_column_text(&q, 3);
2428 const char *zTitle = db_column_text(&q, 4);
2429 const char *zStatus = bHasStatus ? db_column_text(&q, 7) : NULL;
2430 if( iCnt==0 ){
2431 if( iOfst>0 ){
2432 @ <h1>Threads at least %s(zAge) old</h1>
2433 }else{
2434 @ <h1>Most recent threads</h1>
@@ -2444,11 +2465,15 @@
2465 }else{
2466 char *zDuration = human_readable_age(db_column_double(&q,1));
2467 @ %d(nMsg) posts spanning %h(zDuration)\
2468 fossil_free(zDuration);
2469 }
2470 @ </td>\
2471 if( bHasStatus ){
2472 @ <td>%h(zStatus)</td>\
2473 }
2474 @</tr>
2475 fossil_free(zAge);
2476 }
2477 db_finalize(&q);
2478 }
2479 if( iCnt>0 ){
2480

Keyboard Shortcuts

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