Fossil SCM
Improvements to the "status=" query parameter processing on the /forum page.
Commit
57e21b8cfa61984bc9928b9a859e0eb628deb3d4a0624b7a5eb2a46c9d360cde
Parent
ee392e1366cc05b…
1 file changed
+133
-94
+133
-94
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -75,14 +75,27 @@ | ||
| 75 | 75 | |
| 76 | 76 | /* |
| 77 | 77 | ** A list of ForumStatus objects. |
| 78 | 78 | */ |
| 79 | 79 | struct ForumStatusList { |
| 80 | - struct ForumStatus * aStatus; /* List of statuses */ | |
| 81 | - unsigned int n; /* Number of entries */ | |
| 80 | + struct ForumStatus *aStatus; /* List of statuses */ | |
| 81 | + unsigned int n; /* Number of entries */ | |
| 82 | +}; | |
| 83 | + | |
| 84 | +/* | |
| 85 | +** Information passed into the status_match() SQL function | |
| 86 | +** via the sqlite3_user_data() mechanism, and used by status_match() | |
| 87 | +** to determine whether or not a particular forum thread should | |
| 88 | +** be displayed. | |
| 89 | +*/ | |
| 90 | +struct ForumStatusMatch { | |
| 91 | + const ForumStatusList *pFses; /* Parsed forum-statuses setting */ | |
| 92 | + int eStatusTag; /* tagid for the "status" property */ | |
| 93 | + unsigned int iMatch; /* Match this status value */ | |
| 82 | 94 | }; |
| 83 | 95 | #endif /* INTERFACE */ |
| 96 | + | |
| 84 | 97 | |
| 85 | 98 | /* |
| 86 | 99 | ** Returns a high-level representation of the forum-statuses setting. |
| 87 | 100 | ** This is a singleton, cached across calls. |
| 88 | 101 | */ |
| @@ -114,11 +127,10 @@ | ||
| 114 | 127 | " SELECT e.value FROM setting s, jsonb_each(s.v) e" |
| 115 | 128 | " WHERE json_valid(s.v, 0x02)" |
| 116 | 129 | " )" |
| 117 | 130 | " SELECT r->>'label', r->>'value', r->>'description'" |
| 118 | 131 | " FROM room;" |
| 119 | -/* "INSERT OR IGNORE INTO forumstatus(1,'Open','open','Open');" */ | |
| 120 | 132 | ); |
| 121 | 133 | fses.n = (unsigned)db_int(0, "SELECT count(*) FROM forumstatus"); |
| 122 | 134 | if( fses.n ){ |
| 123 | 135 | int i = 0; |
| 124 | 136 | Stmt q; |
| @@ -2393,10 +2405,75 @@ | ||
| 2393 | 2405 | } |
| 2394 | 2406 | if( i ){ |
| 2395 | 2407 | style_submenu_multichoice("status", i/2, az, 0); |
| 2396 | 2408 | } |
| 2397 | 2409 | } |
| 2410 | + | |
| 2411 | +/* | |
| 2412 | +** Transient SQL Function: status_match(FROOT) | |
| 2413 | +** | |
| 2414 | +** Return true if the forum thread identified by FROOT should be included | |
| 2415 | +** in a list of threads. Used to implement the status=NAME query parameter | |
| 2416 | +** on /forum. | |
| 2417 | +** | |
| 2418 | +** The result of this routine depends on the content of the | |
| 2419 | +** ForumStatusMatch *pMData object that is available via sqlite3_user_data(). | |
| 2420 | +** | |
| 2421 | +** * If pMData==NULL, always return true. This means that no | |
| 2422 | +** filtering of threads is being done. This is the common case. | |
| 2423 | +** | |
| 2424 | +** * If FROOT contains a status property value that matches | |
| 2425 | +** pMData->iMatch, return true. | |
| 2426 | +** | |
| 2427 | +** * if pMData->iMatch==0 (meaning we want to match the default | |
| 2428 | +** status value) and if the FROOT thread contains a status that | |
| 2429 | +** is not on the list of statuses or if FROOT has no statue | |
| 2430 | +** property at all, then return true. In other words, a forum | |
| 2431 | +** thread with no status property or an unknown status property | |
| 2432 | +** is treated as if it had the default status. | |
| 2433 | +** | |
| 2434 | +** * Otherwise, return false. | |
| 2435 | +*/ | |
| 2436 | +static void forum_status_match( | |
| 2437 | + sqlite3_context *context, | |
| 2438 | + int argc, | |
| 2439 | + sqlite3_value **argv | |
| 2440 | +){ | |
| 2441 | + static Stmt q; | |
| 2442 | + ForumStatusMatch *pMData = sqlite3_user_data(context); | |
| 2443 | + int i; | |
| 2444 | + | |
| 2445 | + if( pMData==0 ){ | |
| 2446 | + sqlite3_result_int(context, 1); | |
| 2447 | + return; | |
| 2448 | + } | |
| 2449 | + db_static_prepare(&q, | |
| 2450 | + "SELECT value FROM tagxref\n" | |
| 2451 | + " WHERE tagid=%d\n" | |
| 2452 | + " AND tagtype>=1\n" | |
| 2453 | + " AND rid=:rid\n" | |
| 2454 | + " ORDER BY mtime DESC LIMIT 1", | |
| 2455 | + pMData->eStatusTag | |
| 2456 | + ); | |
| 2457 | + db_bind_int(&q, ":rid", sqlite3_value_int(argv[0])); | |
| 2458 | + if( db_step(&q)==SQLITE_ROW ){ | |
| 2459 | + const char *zValue = (const char*)db_column_text(&q,0); | |
| 2460 | + const ForumStatusList *pFses = pMData->pFses; | |
| 2461 | + if( zValue==0 ){ | |
| 2462 | + i = 0; | |
| 2463 | + }else{ | |
| 2464 | + for(i=0; i<pFses->n; i++){ | |
| 2465 | + if( fossil_strcmp(pFses->aStatus[i].zValue,zValue)==0 ) break; | |
| 2466 | + } | |
| 2467 | + } | |
| 2468 | + if( i>=pMData->pFses->n ) i = 0; | |
| 2469 | + }else{ | |
| 2470 | + i = 0; | |
| 2471 | + } | |
| 2472 | + db_reset(&q); | |
| 2473 | + sqlite3_result_int(context, i==pMData->iMatch); | |
| 2474 | +} | |
| 2398 | 2475 | |
| 2399 | 2476 | /* |
| 2400 | 2477 | ** WEBPAGE: forummain |
| 2401 | 2478 | ** WEBPAGE: forum |
| 2402 | 2479 | ** |
| @@ -2414,13 +2491,15 @@ | ||
| 2414 | 2491 | Stmt q; |
| 2415 | 2492 | int iLimit = 0, iOfst, iCnt; |
| 2416 | 2493 | int srchFlags; |
| 2417 | 2494 | const int isSearch = P("s")!=0; |
| 2418 | 2495 | const char *zStatusFilter = P("status"); |
| 2419 | - char const *zLimit = 0; | |
| 2420 | - int eStatusTag = 0; | |
| 2421 | - int bHasStatus = 0; | |
| 2496 | + char const *zLimit = 0; /* Value of the n= query parameter */ | |
| 2497 | + int eStatusTag = 0; /* tagid for the "status" property */ | |
| 2498 | + int bHasStatus = 0; /* True if forum-statuses setting exists */ | |
| 2499 | + int bFilter = 0; /* True if status=NAME query parameter */ | |
| 2500 | + ForumStatusMatch sFSM; /* Aux data to status_match() SQL function */ | |
| 2422 | 2501 | |
| 2423 | 2502 | login_check_credentials(); |
| 2424 | 2503 | srchFlags = search_restrict(SRCH_FORUM); |
| 2425 | 2504 | if( !g.perm.RdForum ){ |
| 2426 | 2505 | login_needed(g.anon.RdForum); |
| @@ -2465,103 +2544,66 @@ | ||
| 2465 | 2544 | } |
| 2466 | 2545 | style_submenu_entry("n","Max:",4,0); |
| 2467 | 2546 | forum_status_submenu(); |
| 2468 | 2547 | iOfst = atoi(PD("x","0")); |
| 2469 | 2548 | iCnt = 0; |
| 2470 | - if( zStatusFilter && | |
| 2471 | - (!*zStatusFilter || 0==fossil_strcmp("*",zStatusFilter)) ){ | |
| 2472 | - zStatusFilter = 0; | |
| 2549 | + if( zStatusFilter ){ | |
| 2550 | + if( zStatusFilter[0]==0 || 0==fossil_strcmp("*",zStatusFilter) ){ | |
| 2551 | + zStatusFilter = 0; | |
| 2552 | + }else{ | |
| 2553 | + bFilter = bHasStatus; | |
| 2554 | + } | |
| 2473 | 2555 | } |
| 2474 | 2556 | if( db_table_exists("repository","forumpost") ){ |
| 2475 | - const ForumStatusList * pFstat = forum_statuses(); | |
| 2476 | - const int iStatusTagId = bHasStatus | |
| 2477 | - ? db_int(0, "SELECT tagid FROM TAG WHERE tagname='status'") | |
| 2478 | - : 0; | |
| 2479 | - Stmt qStat = empty_Stmt; | |
| 2480 | - db_multi_exec( | |
| 2481 | - /* List of forumpost.fpid values which match current | |
| 2482 | - filters. */ | |
| 2483 | - "CREATE TEMP TABLE trootid(id INTEGER PRIMARY KEY);" | |
| 2484 | - ); | |
| 2557 | + const ForumStatusList *pFstat = forum_statuses(); | |
| 2558 | + Stmt qStat = empty_Stmt; /* Query to get status information */ | |
| 2485 | 2559 | if( bHasStatus ){ |
| 2486 | - db_prepare( | |
| 2487 | - /* FIXME/TODO: it would be nice to be able to get this info | |
| 2488 | - ** directly into the thread query below, rather than | |
| 2489 | - ** requiring a separate statement and lookup inside the | |
| 2490 | - ** q loop. */ | |
| 2491 | - &qStat, | |
| 2560 | + /* The qStat query runs once for each output row generate by the | |
| 2561 | + ** q query. It determines the value and label of the status for | |
| 2562 | + ** the row with froot=:rowid | |
| 2563 | + */ | |
| 2564 | + db_prepare(&qStat, | |
| 2492 | 2565 | "SELECT tagxref.value, forumstatus.label\n" |
| 2493 | 2566 | " FROM forumstatus, tagxref\n" |
| 2494 | 2567 | " WHERE tagid=%d AND tagtype>=1\n" |
| 2495 | 2568 | " AND forumstatus.value=tagxref.value\n" |
| 2496 | 2569 | " AND rid=:rid\n" |
| 2497 | - " AND if(%d=0,1,tagxref.value=%Q)\n" | |
| 2498 | - " ORDER BY mtime DESC", | |
| 2499 | - iStatusTagId, !!zStatusFilter, zStatusFilter | |
| 2500 | - ); | |
| 2501 | - if( zStatusFilter ){ | |
| 2502 | - const int bIsDflt = | |
| 2503 | - 0==fossil_strcmp(pFstat->aStatus[0].zValue, zStatusFilter); | |
| 2504 | - db_multi_exec( | |
| 2505 | - "WITH stati(val) AS (SELECT value FROM forumstatus)\n" | |
| 2506 | - "INSERT INTO trootid\n" | |
| 2507 | - /* Rules: | |
| 2508 | - | |
| 2509 | - (1) Filter on status=$zStatusFilter | |
| 2510 | - (2) If $zStatusFilter==default status then also include | |
| 2511 | - any posts with no status tag. | |
| 2512 | - (3) If no status tag is found for a given post, assume a tag | |
| 2513 | - with the value of the first (default) status. | |
| 2514 | - (4) If a status value is found which is not in | |
| 2515 | - [forumstatus] then treat it as if it were the default | |
| 2516 | - value. | |
| 2517 | - | |
| 2518 | - We need to ensure that we filter only the most recent | |
| 2519 | - value of each tag and count tagtype=0 (cancel) tags | |
| 2520 | - properly. | |
| 2521 | - */ | |
| 2522 | - " SELECT fpid FROM forumpost\n" | |
| 2523 | - " LEFT JOIN tagxref x ON fpid=rid\n" | |
| 2524 | - " WHERE froot=fpid AND firt IS NULL\n" | |
| 2525 | - " AND (x.tagtype IS NULL OR x.tagtype>0)\n" | |
| 2526 | - " AND (x.tagid IS NULL OR x.tagid=%d)\n" | |
| 2527 | - " AND CASE\n" | |
| 2528 | - " WHEN %d" /*bIsDflt*/ | |
| 2529 | - " THEN (" | |
| 2530 | - " x.value IS NULL" | |
| 2531 | - " OR x.value=%Q" | |
| 2532 | - " OR x.value NOT IN stati" /* (4) */ | |
| 2533 | - " )\n" /* (2,3,4) */ | |
| 2534 | - " ELSE x.value=%Q\n" /* (1) */ | |
| 2535 | - " END", | |
| 2536 | - iStatusTagId, bIsDflt, zStatusFilter, zStatusFilter | |
| 2537 | - ); | |
| 2538 | - (void)bIsDflt; | |
| 2539 | - }else{ | |
| 2540 | - db_multi_exec( | |
| 2541 | - "INSERT INTO trootid\n" | |
| 2542 | - " SELECT fpid FROM forumpost\n" | |
| 2543 | - " WHERE froot=fpid AND firt IS NULL" | |
| 2544 | - ); | |
| 2545 | - } | |
| 2546 | - }else{ | |
| 2547 | - db_multi_exec( | |
| 2548 | - "INSERT INTO trootid\n" | |
| 2549 | - " SELECT fpid FROM forumpost\n" | |
| 2550 | - " WHERE froot=fpid AND firt IS NULL" | |
| 2551 | - ); | |
| 2552 | - } | |
| 2570 | + " ORDER BY mtime DESC", | |
| 2571 | + eStatusTag | |
| 2572 | + ); | |
| 2573 | + } | |
| 2574 | + | |
| 2575 | + /* Create the status_match() SQL function that will determine | |
| 2576 | + ** whether or not each thread in the "q" query below is eligible | |
| 2577 | + ** for display | |
| 2578 | + */ | |
| 2579 | + if( bFilter ){ | |
| 2580 | + sFSM.pFses = pFstat; | |
| 2581 | + sFSM.eStatusTag = eStatusTag; | |
| 2582 | + for(sFSM.iMatch=0; sFSM.iMatch<pFstat->n; sFSM.iMatch++){ | |
| 2583 | + if( 0==fossil_strcmp(zStatusFilter, | |
| 2584 | + pFstat->aStatus[sFSM.iMatch].zValue) ){ | |
| 2585 | + break; | |
| 2586 | + } | |
| 2587 | + } | |
| 2588 | + sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,(void*)&sFSM, | |
| 2589 | + forum_status_match, 0, 0); | |
| 2590 | + }else{ | |
| 2591 | + sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0, | |
| 2592 | + forum_status_match, 0, 0); | |
| 2593 | + } | |
| 2594 | + | |
| 2553 | 2595 | db_prepare(&q, |
| 2554 | 2596 | "WITH thread(root,endtime,lastrid) AS (\n" |
| 2555 | 2597 | " SELECT\n" |
| 2556 | 2598 | " froot,\n" |
| 2557 | 2599 | " max(fmtime),\n" |
| 2558 | 2600 | " fpid\n" |
| 2559 | - " FROM forumpost, trootid\n" | |
| 2601 | + " FROM forumpost\n" | |
| 2560 | 2602 | " WHERE %s/*ModForum*/\n" |
| 2561 | - " AND froot=trootid.id\n" | |
| 2562 | 2603 | " GROUP BY froot\n" |
| 2604 | + " HAVING status_match(froot)\n" | |
| 2563 | 2605 | " ORDER BY 2 DESC\n" |
| 2564 | 2606 | " LIMIT %d OFFSET %d\n" |
| 2565 | 2607 | ")\n" |
| 2566 | 2608 | "SELECT\n" |
| 2567 | 2609 | " julianday('now') - thread.endtime,\n" /* 0 */ |
| @@ -2586,36 +2628,32 @@ | ||
| 2586 | 2628 | const char *zUuid; |
| 2587 | 2629 | const char *zTitle; |
| 2588 | 2630 | const char *zStatus; |
| 2589 | 2631 | const char *zStatusLbl; |
| 2590 | 2632 | const int bShowStatus = bHasStatus && !zStatusFilter; |
| 2591 | - const int nCols = bShowStatus ? 4 : 3 | |
| 2592 | - /* When filtering on status, elide the status column */; | |
| 2633 | + const int nCols = bShowStatus ? 4 : 3; | |
| 2634 | + | |
| 2593 | 2635 | if( qStat.pStmt ){ |
| 2636 | + /* Determine the status value for this row */ | |
| 2594 | 2637 | db_reset(&qStat); |
| 2595 | 2638 | db_bind_int(&qStat, ":rid", db_column_int(&q,6)); |
| 2596 | 2639 | if( db_step(&qStat)==SQLITE_ROW ){ |
| 2597 | 2640 | zStatus = db_column_text(&qStat, 0); |
| 2598 | 2641 | zStatusLbl = db_column_text(&qStat, 1); |
| 2599 | 2642 | }else{ |
| 2600 | 2643 | zStatus = pFstat->aStatus[0].zValue; |
| 2601 | 2644 | zStatusLbl = pFstat->aStatus[0].zLabel; |
| 2602 | 2645 | } |
| 2603 | - if( zStatusFilter && 0!=fossil_strcmp(zStatusFilter,zStatus) ){ | |
| 2604 | - continue; | |
| 2605 | - } | |
| 2606 | 2646 | }else{ |
| 2607 | 2647 | zStatus = zStatusLbl = NULL; |
| 2608 | 2648 | } |
| 2609 | 2649 | zAge = human_readable_age(db_column_double(&q,0)); |
| 2610 | 2650 | nMsg = db_column_int(&q, 2); |
| 2611 | 2651 | zUuid = db_column_text(&q, 3); |
| 2612 | 2652 | zTitle = db_column_text(&q, 4); |
| 2613 | 2653 | if( iCnt==0 ){ |
| 2614 | - char * zTail = zStatusFilter | |
| 2615 | - ? mprintf(" with status=%Q", zStatusFilter) | |
| 2616 | - : 0; | |
| 2654 | + char *zTail = bFilter ? mprintf(" with status=%Q", zStatusFilter): 0; | |
| 2617 | 2655 | if( iOfst>0 ){ |
| 2618 | 2656 | @ <h1>Threads at least %s(zAge) old%h(zTail ? zTail : "")</h1> |
| 2619 | 2657 | }else{ |
| 2620 | 2658 | @ <h1>Most recent threads%h(zTail ? zTail : "")</h1> |
| 2621 | 2659 | } |
| @@ -2623,18 +2661,18 @@ | ||
| 2623 | 2661 | @ <div class='forumPosts fileage'><table width="100%%"> |
| 2624 | 2662 | if( iOfst>0 ){ |
| 2625 | 2663 | if( iOfst>iLimit ){ |
| 2626 | 2664 | @ <tr><td colspan="%d(nCols)">\ |
| 2627 | 2665 | @ <a href='%R/forum?x=%d(iOfst-iLimit)&n=%d(iLimit) \ |
| 2628 | - if( zStatusFilter ){ | |
| 2666 | + if( bFilter ){ | |
| 2629 | 2667 | @ &status=%T(zStatusFilter)\ |
| 2630 | 2668 | } |
| 2631 | 2669 | @ '>↑ Newer...</a></td></tr> |
| 2632 | 2670 | }else{ |
| 2633 | 2671 | @ <tr><td colspan="%d(nCols)">\ |
| 2634 | 2672 | @ <a href='%R/forum?n=%d(iLimit)\ |
| 2635 | - if( zStatusFilter ){ | |
| 2673 | + if( bFilter ){ | |
| 2636 | 2674 | @ &status=%T(zStatusFilter) \ |
| 2637 | 2675 | } |
| 2638 | 2676 | @ '>↑ Newer...</a></td></tr> |
| 2639 | 2677 | } |
| 2640 | 2678 | } |
| @@ -2641,11 +2679,11 @@ | ||
| 2641 | 2679 | } |
| 2642 | 2680 | iCnt++; |
| 2643 | 2681 | if( iCnt>iLimit ){ |
| 2644 | 2682 | @ <tr><td colspan="%d(nCols)">\ |
| 2645 | 2683 | @ <a href='%R/forum?x=%d(iOfst+iLimit)&n=%d(iLimit) \ |
| 2646 | - if( zStatusFilter ){ | |
| 2684 | + if( bFilter ){ | |
| 2647 | 2685 | @ &status=%T(zStatusFilter)\ |
| 2648 | 2686 | } |
| 2649 | 2687 | @ '>↓ Older...</a></td></tr> |
| 2650 | 2688 | fossil_free(zAge); |
| 2651 | 2689 | break; |
| @@ -2678,10 +2716,11 @@ | ||
| 2678 | 2716 | @</tr> |
| 2679 | 2717 | fossil_free(zAge); |
| 2680 | 2718 | } |
| 2681 | 2719 | db_finalize(&q); |
| 2682 | 2720 | if( qStat.pStmt ) db_finalize(&qStat); |
| 2721 | + sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0,0,0,0); | |
| 2683 | 2722 | } |
| 2684 | 2723 | if( iCnt>0 ){ |
| 2685 | 2724 | @ </table></div> |
| 2686 | 2725 | }else{ |
| 2687 | 2726 | @ <h1>No forum posts found</h1> |
| 2688 | 2727 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -75,14 +75,27 @@ | |
| 75 | |
| 76 | /* |
| 77 | ** A list of ForumStatus objects. |
| 78 | */ |
| 79 | struct ForumStatusList { |
| 80 | struct ForumStatus * aStatus; /* List of statuses */ |
| 81 | unsigned int n; /* Number of entries */ |
| 82 | }; |
| 83 | #endif /* INTERFACE */ |
| 84 | |
| 85 | /* |
| 86 | ** Returns a high-level representation of the forum-statuses setting. |
| 87 | ** This is a singleton, cached across calls. |
| 88 | */ |
| @@ -114,11 +127,10 @@ | |
| 114 | " SELECT e.value FROM setting s, jsonb_each(s.v) e" |
| 115 | " WHERE json_valid(s.v, 0x02)" |
| 116 | " )" |
| 117 | " SELECT r->>'label', r->>'value', r->>'description'" |
| 118 | " FROM room;" |
| 119 | /* "INSERT OR IGNORE INTO forumstatus(1,'Open','open','Open');" */ |
| 120 | ); |
| 121 | fses.n = (unsigned)db_int(0, "SELECT count(*) FROM forumstatus"); |
| 122 | if( fses.n ){ |
| 123 | int i = 0; |
| 124 | Stmt q; |
| @@ -2393,10 +2405,75 @@ | |
| 2393 | } |
| 2394 | if( i ){ |
| 2395 | style_submenu_multichoice("status", i/2, az, 0); |
| 2396 | } |
| 2397 | } |
| 2398 | |
| 2399 | /* |
| 2400 | ** WEBPAGE: forummain |
| 2401 | ** WEBPAGE: forum |
| 2402 | ** |
| @@ -2414,13 +2491,15 @@ | |
| 2414 | Stmt q; |
| 2415 | int iLimit = 0, iOfst, iCnt; |
| 2416 | int srchFlags; |
| 2417 | const int isSearch = P("s")!=0; |
| 2418 | const char *zStatusFilter = P("status"); |
| 2419 | char const *zLimit = 0; |
| 2420 | int eStatusTag = 0; |
| 2421 | int bHasStatus = 0; |
| 2422 | |
| 2423 | login_check_credentials(); |
| 2424 | srchFlags = search_restrict(SRCH_FORUM); |
| 2425 | if( !g.perm.RdForum ){ |
| 2426 | login_needed(g.anon.RdForum); |
| @@ -2465,103 +2544,66 @@ | |
| 2465 | } |
| 2466 | style_submenu_entry("n","Max:",4,0); |
| 2467 | forum_status_submenu(); |
| 2468 | iOfst = atoi(PD("x","0")); |
| 2469 | iCnt = 0; |
| 2470 | if( zStatusFilter && |
| 2471 | (!*zStatusFilter || 0==fossil_strcmp("*",zStatusFilter)) ){ |
| 2472 | zStatusFilter = 0; |
| 2473 | } |
| 2474 | if( db_table_exists("repository","forumpost") ){ |
| 2475 | const ForumStatusList * pFstat = forum_statuses(); |
| 2476 | const int iStatusTagId = bHasStatus |
| 2477 | ? db_int(0, "SELECT tagid FROM TAG WHERE tagname='status'") |
| 2478 | : 0; |
| 2479 | Stmt qStat = empty_Stmt; |
| 2480 | db_multi_exec( |
| 2481 | /* List of forumpost.fpid values which match current |
| 2482 | filters. */ |
| 2483 | "CREATE TEMP TABLE trootid(id INTEGER PRIMARY KEY);" |
| 2484 | ); |
| 2485 | if( bHasStatus ){ |
| 2486 | db_prepare( |
| 2487 | /* FIXME/TODO: it would be nice to be able to get this info |
| 2488 | ** directly into the thread query below, rather than |
| 2489 | ** requiring a separate statement and lookup inside the |
| 2490 | ** q loop. */ |
| 2491 | &qStat, |
| 2492 | "SELECT tagxref.value, forumstatus.label\n" |
| 2493 | " FROM forumstatus, tagxref\n" |
| 2494 | " WHERE tagid=%d AND tagtype>=1\n" |
| 2495 | " AND forumstatus.value=tagxref.value\n" |
| 2496 | " AND rid=:rid\n" |
| 2497 | " AND if(%d=0,1,tagxref.value=%Q)\n" |
| 2498 | " ORDER BY mtime DESC", |
| 2499 | iStatusTagId, !!zStatusFilter, zStatusFilter |
| 2500 | ); |
| 2501 | if( zStatusFilter ){ |
| 2502 | const int bIsDflt = |
| 2503 | 0==fossil_strcmp(pFstat->aStatus[0].zValue, zStatusFilter); |
| 2504 | db_multi_exec( |
| 2505 | "WITH stati(val) AS (SELECT value FROM forumstatus)\n" |
| 2506 | "INSERT INTO trootid\n" |
| 2507 | /* Rules: |
| 2508 | |
| 2509 | (1) Filter on status=$zStatusFilter |
| 2510 | (2) If $zStatusFilter==default status then also include |
| 2511 | any posts with no status tag. |
| 2512 | (3) If no status tag is found for a given post, assume a tag |
| 2513 | with the value of the first (default) status. |
| 2514 | (4) If a status value is found which is not in |
| 2515 | [forumstatus] then treat it as if it were the default |
| 2516 | value. |
| 2517 | |
| 2518 | We need to ensure that we filter only the most recent |
| 2519 | value of each tag and count tagtype=0 (cancel) tags |
| 2520 | properly. |
| 2521 | */ |
| 2522 | " SELECT fpid FROM forumpost\n" |
| 2523 | " LEFT JOIN tagxref x ON fpid=rid\n" |
| 2524 | " WHERE froot=fpid AND firt IS NULL\n" |
| 2525 | " AND (x.tagtype IS NULL OR x.tagtype>0)\n" |
| 2526 | " AND (x.tagid IS NULL OR x.tagid=%d)\n" |
| 2527 | " AND CASE\n" |
| 2528 | " WHEN %d" /*bIsDflt*/ |
| 2529 | " THEN (" |
| 2530 | " x.value IS NULL" |
| 2531 | " OR x.value=%Q" |
| 2532 | " OR x.value NOT IN stati" /* (4) */ |
| 2533 | " )\n" /* (2,3,4) */ |
| 2534 | " ELSE x.value=%Q\n" /* (1) */ |
| 2535 | " END", |
| 2536 | iStatusTagId, bIsDflt, zStatusFilter, zStatusFilter |
| 2537 | ); |
| 2538 | (void)bIsDflt; |
| 2539 | }else{ |
| 2540 | db_multi_exec( |
| 2541 | "INSERT INTO trootid\n" |
| 2542 | " SELECT fpid FROM forumpost\n" |
| 2543 | " WHERE froot=fpid AND firt IS NULL" |
| 2544 | ); |
| 2545 | } |
| 2546 | }else{ |
| 2547 | db_multi_exec( |
| 2548 | "INSERT INTO trootid\n" |
| 2549 | " SELECT fpid FROM forumpost\n" |
| 2550 | " WHERE froot=fpid AND firt IS NULL" |
| 2551 | ); |
| 2552 | } |
| 2553 | db_prepare(&q, |
| 2554 | "WITH thread(root,endtime,lastrid) AS (\n" |
| 2555 | " SELECT\n" |
| 2556 | " froot,\n" |
| 2557 | " max(fmtime),\n" |
| 2558 | " fpid\n" |
| 2559 | " FROM forumpost, trootid\n" |
| 2560 | " WHERE %s/*ModForum*/\n" |
| 2561 | " AND froot=trootid.id\n" |
| 2562 | " GROUP BY froot\n" |
| 2563 | " ORDER BY 2 DESC\n" |
| 2564 | " LIMIT %d OFFSET %d\n" |
| 2565 | ")\n" |
| 2566 | "SELECT\n" |
| 2567 | " julianday('now') - thread.endtime,\n" /* 0 */ |
| @@ -2586,36 +2628,32 @@ | |
| 2586 | const char *zUuid; |
| 2587 | const char *zTitle; |
| 2588 | const char *zStatus; |
| 2589 | const char *zStatusLbl; |
| 2590 | const int bShowStatus = bHasStatus && !zStatusFilter; |
| 2591 | const int nCols = bShowStatus ? 4 : 3 |
| 2592 | /* When filtering on status, elide the status column */; |
| 2593 | if( qStat.pStmt ){ |
| 2594 | db_reset(&qStat); |
| 2595 | db_bind_int(&qStat, ":rid", db_column_int(&q,6)); |
| 2596 | if( db_step(&qStat)==SQLITE_ROW ){ |
| 2597 | zStatus = db_column_text(&qStat, 0); |
| 2598 | zStatusLbl = db_column_text(&qStat, 1); |
| 2599 | }else{ |
| 2600 | zStatus = pFstat->aStatus[0].zValue; |
| 2601 | zStatusLbl = pFstat->aStatus[0].zLabel; |
| 2602 | } |
| 2603 | if( zStatusFilter && 0!=fossil_strcmp(zStatusFilter,zStatus) ){ |
| 2604 | continue; |
| 2605 | } |
| 2606 | }else{ |
| 2607 | zStatus = zStatusLbl = NULL; |
| 2608 | } |
| 2609 | zAge = human_readable_age(db_column_double(&q,0)); |
| 2610 | nMsg = db_column_int(&q, 2); |
| 2611 | zUuid = db_column_text(&q, 3); |
| 2612 | zTitle = db_column_text(&q, 4); |
| 2613 | if( iCnt==0 ){ |
| 2614 | char * zTail = zStatusFilter |
| 2615 | ? mprintf(" with status=%Q", zStatusFilter) |
| 2616 | : 0; |
| 2617 | if( iOfst>0 ){ |
| 2618 | @ <h1>Threads at least %s(zAge) old%h(zTail ? zTail : "")</h1> |
| 2619 | }else{ |
| 2620 | @ <h1>Most recent threads%h(zTail ? zTail : "")</h1> |
| 2621 | } |
| @@ -2623,18 +2661,18 @@ | |
| 2623 | @ <div class='forumPosts fileage'><table width="100%%"> |
| 2624 | if( iOfst>0 ){ |
| 2625 | if( iOfst>iLimit ){ |
| 2626 | @ <tr><td colspan="%d(nCols)">\ |
| 2627 | @ <a href='%R/forum?x=%d(iOfst-iLimit)&n=%d(iLimit) \ |
| 2628 | if( zStatusFilter ){ |
| 2629 | @ &status=%T(zStatusFilter)\ |
| 2630 | } |
| 2631 | @ '>↑ Newer...</a></td></tr> |
| 2632 | }else{ |
| 2633 | @ <tr><td colspan="%d(nCols)">\ |
| 2634 | @ <a href='%R/forum?n=%d(iLimit)\ |
| 2635 | if( zStatusFilter ){ |
| 2636 | @ &status=%T(zStatusFilter) \ |
| 2637 | } |
| 2638 | @ '>↑ Newer...</a></td></tr> |
| 2639 | } |
| 2640 | } |
| @@ -2641,11 +2679,11 @@ | |
| 2641 | } |
| 2642 | iCnt++; |
| 2643 | if( iCnt>iLimit ){ |
| 2644 | @ <tr><td colspan="%d(nCols)">\ |
| 2645 | @ <a href='%R/forum?x=%d(iOfst+iLimit)&n=%d(iLimit) \ |
| 2646 | if( zStatusFilter ){ |
| 2647 | @ &status=%T(zStatusFilter)\ |
| 2648 | } |
| 2649 | @ '>↓ Older...</a></td></tr> |
| 2650 | fossil_free(zAge); |
| 2651 | break; |
| @@ -2678,10 +2716,11 @@ | |
| 2678 | @</tr> |
| 2679 | fossil_free(zAge); |
| 2680 | } |
| 2681 | db_finalize(&q); |
| 2682 | if( qStat.pStmt ) db_finalize(&qStat); |
| 2683 | } |
| 2684 | if( iCnt>0 ){ |
| 2685 | @ </table></div> |
| 2686 | }else{ |
| 2687 | @ <h1>No forum posts found</h1> |
| 2688 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -75,14 +75,27 @@ | |
| 75 | |
| 76 | /* |
| 77 | ** A list of ForumStatus objects. |
| 78 | */ |
| 79 | struct ForumStatusList { |
| 80 | struct ForumStatus *aStatus; /* List of statuses */ |
| 81 | unsigned int n; /* Number of entries */ |
| 82 | }; |
| 83 | |
| 84 | /* |
| 85 | ** Information passed into the status_match() SQL function |
| 86 | ** via the sqlite3_user_data() mechanism, and used by status_match() |
| 87 | ** to determine whether or not a particular forum thread should |
| 88 | ** be displayed. |
| 89 | */ |
| 90 | struct ForumStatusMatch { |
| 91 | const ForumStatusList *pFses; /* Parsed forum-statuses setting */ |
| 92 | int eStatusTag; /* tagid for the "status" property */ |
| 93 | unsigned int iMatch; /* Match this status value */ |
| 94 | }; |
| 95 | #endif /* INTERFACE */ |
| 96 | |
| 97 | |
| 98 | /* |
| 99 | ** Returns a high-level representation of the forum-statuses setting. |
| 100 | ** This is a singleton, cached across calls. |
| 101 | */ |
| @@ -114,11 +127,10 @@ | |
| 127 | " SELECT e.value FROM setting s, jsonb_each(s.v) e" |
| 128 | " WHERE json_valid(s.v, 0x02)" |
| 129 | " )" |
| 130 | " SELECT r->>'label', r->>'value', r->>'description'" |
| 131 | " FROM room;" |
| 132 | ); |
| 133 | fses.n = (unsigned)db_int(0, "SELECT count(*) FROM forumstatus"); |
| 134 | if( fses.n ){ |
| 135 | int i = 0; |
| 136 | Stmt q; |
| @@ -2393,10 +2405,75 @@ | |
| 2405 | } |
| 2406 | if( i ){ |
| 2407 | style_submenu_multichoice("status", i/2, az, 0); |
| 2408 | } |
| 2409 | } |
| 2410 | |
| 2411 | /* |
| 2412 | ** Transient SQL Function: status_match(FROOT) |
| 2413 | ** |
| 2414 | ** Return true if the forum thread identified by FROOT should be included |
| 2415 | ** in a list of threads. Used to implement the status=NAME query parameter |
| 2416 | ** on /forum. |
| 2417 | ** |
| 2418 | ** The result of this routine depends on the content of the |
| 2419 | ** ForumStatusMatch *pMData object that is available via sqlite3_user_data(). |
| 2420 | ** |
| 2421 | ** * If pMData==NULL, always return true. This means that no |
| 2422 | ** filtering of threads is being done. This is the common case. |
| 2423 | ** |
| 2424 | ** * If FROOT contains a status property value that matches |
| 2425 | ** pMData->iMatch, return true. |
| 2426 | ** |
| 2427 | ** * if pMData->iMatch==0 (meaning we want to match the default |
| 2428 | ** status value) and if the FROOT thread contains a status that |
| 2429 | ** is not on the list of statuses or if FROOT has no statue |
| 2430 | ** property at all, then return true. In other words, a forum |
| 2431 | ** thread with no status property or an unknown status property |
| 2432 | ** is treated as if it had the default status. |
| 2433 | ** |
| 2434 | ** * Otherwise, return false. |
| 2435 | */ |
| 2436 | static void forum_status_match( |
| 2437 | sqlite3_context *context, |
| 2438 | int argc, |
| 2439 | sqlite3_value **argv |
| 2440 | ){ |
| 2441 | static Stmt q; |
| 2442 | ForumStatusMatch *pMData = sqlite3_user_data(context); |
| 2443 | int i; |
| 2444 | |
| 2445 | if( pMData==0 ){ |
| 2446 | sqlite3_result_int(context, 1); |
| 2447 | return; |
| 2448 | } |
| 2449 | db_static_prepare(&q, |
| 2450 | "SELECT value FROM tagxref\n" |
| 2451 | " WHERE tagid=%d\n" |
| 2452 | " AND tagtype>=1\n" |
| 2453 | " AND rid=:rid\n" |
| 2454 | " ORDER BY mtime DESC LIMIT 1", |
| 2455 | pMData->eStatusTag |
| 2456 | ); |
| 2457 | db_bind_int(&q, ":rid", sqlite3_value_int(argv[0])); |
| 2458 | if( db_step(&q)==SQLITE_ROW ){ |
| 2459 | const char *zValue = (const char*)db_column_text(&q,0); |
| 2460 | const ForumStatusList *pFses = pMData->pFses; |
| 2461 | if( zValue==0 ){ |
| 2462 | i = 0; |
| 2463 | }else{ |
| 2464 | for(i=0; i<pFses->n; i++){ |
| 2465 | if( fossil_strcmp(pFses->aStatus[i].zValue,zValue)==0 ) break; |
| 2466 | } |
| 2467 | } |
| 2468 | if( i>=pMData->pFses->n ) i = 0; |
| 2469 | }else{ |
| 2470 | i = 0; |
| 2471 | } |
| 2472 | db_reset(&q); |
| 2473 | sqlite3_result_int(context, i==pMData->iMatch); |
| 2474 | } |
| 2475 | |
| 2476 | /* |
| 2477 | ** WEBPAGE: forummain |
| 2478 | ** WEBPAGE: forum |
| 2479 | ** |
| @@ -2414,13 +2491,15 @@ | |
| 2491 | Stmt q; |
| 2492 | int iLimit = 0, iOfst, iCnt; |
| 2493 | int srchFlags; |
| 2494 | const int isSearch = P("s")!=0; |
| 2495 | const char *zStatusFilter = P("status"); |
| 2496 | char const *zLimit = 0; /* Value of the n= query parameter */ |
| 2497 | int eStatusTag = 0; /* tagid for the "status" property */ |
| 2498 | int bHasStatus = 0; /* True if forum-statuses setting exists */ |
| 2499 | int bFilter = 0; /* True if status=NAME query parameter */ |
| 2500 | ForumStatusMatch sFSM; /* Aux data to status_match() SQL function */ |
| 2501 | |
| 2502 | login_check_credentials(); |
| 2503 | srchFlags = search_restrict(SRCH_FORUM); |
| 2504 | if( !g.perm.RdForum ){ |
| 2505 | login_needed(g.anon.RdForum); |
| @@ -2465,103 +2544,66 @@ | |
| 2544 | } |
| 2545 | style_submenu_entry("n","Max:",4,0); |
| 2546 | forum_status_submenu(); |
| 2547 | iOfst = atoi(PD("x","0")); |
| 2548 | iCnt = 0; |
| 2549 | if( zStatusFilter ){ |
| 2550 | if( zStatusFilter[0]==0 || 0==fossil_strcmp("*",zStatusFilter) ){ |
| 2551 | zStatusFilter = 0; |
| 2552 | }else{ |
| 2553 | bFilter = bHasStatus; |
| 2554 | } |
| 2555 | } |
| 2556 | if( db_table_exists("repository","forumpost") ){ |
| 2557 | const ForumStatusList *pFstat = forum_statuses(); |
| 2558 | Stmt qStat = empty_Stmt; /* Query to get status information */ |
| 2559 | if( bHasStatus ){ |
| 2560 | /* The qStat query runs once for each output row generate by the |
| 2561 | ** q query. It determines the value and label of the status for |
| 2562 | ** the row with froot=:rowid |
| 2563 | */ |
| 2564 | db_prepare(&qStat, |
| 2565 | "SELECT tagxref.value, forumstatus.label\n" |
| 2566 | " FROM forumstatus, tagxref\n" |
| 2567 | " WHERE tagid=%d AND tagtype>=1\n" |
| 2568 | " AND forumstatus.value=tagxref.value\n" |
| 2569 | " AND rid=:rid\n" |
| 2570 | " ORDER BY mtime DESC", |
| 2571 | eStatusTag |
| 2572 | ); |
| 2573 | } |
| 2574 | |
| 2575 | /* Create the status_match() SQL function that will determine |
| 2576 | ** whether or not each thread in the "q" query below is eligible |
| 2577 | ** for display |
| 2578 | */ |
| 2579 | if( bFilter ){ |
| 2580 | sFSM.pFses = pFstat; |
| 2581 | sFSM.eStatusTag = eStatusTag; |
| 2582 | for(sFSM.iMatch=0; sFSM.iMatch<pFstat->n; sFSM.iMatch++){ |
| 2583 | if( 0==fossil_strcmp(zStatusFilter, |
| 2584 | pFstat->aStatus[sFSM.iMatch].zValue) ){ |
| 2585 | break; |
| 2586 | } |
| 2587 | } |
| 2588 | sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,(void*)&sFSM, |
| 2589 | forum_status_match, 0, 0); |
| 2590 | }else{ |
| 2591 | sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0, |
| 2592 | forum_status_match, 0, 0); |
| 2593 | } |
| 2594 | |
| 2595 | db_prepare(&q, |
| 2596 | "WITH thread(root,endtime,lastrid) AS (\n" |
| 2597 | " SELECT\n" |
| 2598 | " froot,\n" |
| 2599 | " max(fmtime),\n" |
| 2600 | " fpid\n" |
| 2601 | " FROM forumpost\n" |
| 2602 | " WHERE %s/*ModForum*/\n" |
| 2603 | " GROUP BY froot\n" |
| 2604 | " HAVING status_match(froot)\n" |
| 2605 | " ORDER BY 2 DESC\n" |
| 2606 | " LIMIT %d OFFSET %d\n" |
| 2607 | ")\n" |
| 2608 | "SELECT\n" |
| 2609 | " julianday('now') - thread.endtime,\n" /* 0 */ |
| @@ -2586,36 +2628,32 @@ | |
| 2628 | const char *zUuid; |
| 2629 | const char *zTitle; |
| 2630 | const char *zStatus; |
| 2631 | const char *zStatusLbl; |
| 2632 | const int bShowStatus = bHasStatus && !zStatusFilter; |
| 2633 | const int nCols = bShowStatus ? 4 : 3; |
| 2634 | |
| 2635 | if( qStat.pStmt ){ |
| 2636 | /* Determine the status value for this row */ |
| 2637 | db_reset(&qStat); |
| 2638 | db_bind_int(&qStat, ":rid", db_column_int(&q,6)); |
| 2639 | if( db_step(&qStat)==SQLITE_ROW ){ |
| 2640 | zStatus = db_column_text(&qStat, 0); |
| 2641 | zStatusLbl = db_column_text(&qStat, 1); |
| 2642 | }else{ |
| 2643 | zStatus = pFstat->aStatus[0].zValue; |
| 2644 | zStatusLbl = pFstat->aStatus[0].zLabel; |
| 2645 | } |
| 2646 | }else{ |
| 2647 | zStatus = zStatusLbl = NULL; |
| 2648 | } |
| 2649 | zAge = human_readable_age(db_column_double(&q,0)); |
| 2650 | nMsg = db_column_int(&q, 2); |
| 2651 | zUuid = db_column_text(&q, 3); |
| 2652 | zTitle = db_column_text(&q, 4); |
| 2653 | if( iCnt==0 ){ |
| 2654 | char *zTail = bFilter ? mprintf(" with status=%Q", zStatusFilter): 0; |
| 2655 | if( iOfst>0 ){ |
| 2656 | @ <h1>Threads at least %s(zAge) old%h(zTail ? zTail : "")</h1> |
| 2657 | }else{ |
| 2658 | @ <h1>Most recent threads%h(zTail ? zTail : "")</h1> |
| 2659 | } |
| @@ -2623,18 +2661,18 @@ | |
| 2661 | @ <div class='forumPosts fileage'><table width="100%%"> |
| 2662 | if( iOfst>0 ){ |
| 2663 | if( iOfst>iLimit ){ |
| 2664 | @ <tr><td colspan="%d(nCols)">\ |
| 2665 | @ <a href='%R/forum?x=%d(iOfst-iLimit)&n=%d(iLimit) \ |
| 2666 | if( bFilter ){ |
| 2667 | @ &status=%T(zStatusFilter)\ |
| 2668 | } |
| 2669 | @ '>↑ Newer...</a></td></tr> |
| 2670 | }else{ |
| 2671 | @ <tr><td colspan="%d(nCols)">\ |
| 2672 | @ <a href='%R/forum?n=%d(iLimit)\ |
| 2673 | if( bFilter ){ |
| 2674 | @ &status=%T(zStatusFilter) \ |
| 2675 | } |
| 2676 | @ '>↑ Newer...</a></td></tr> |
| 2677 | } |
| 2678 | } |
| @@ -2641,11 +2679,11 @@ | |
| 2679 | } |
| 2680 | iCnt++; |
| 2681 | if( iCnt>iLimit ){ |
| 2682 | @ <tr><td colspan="%d(nCols)">\ |
| 2683 | @ <a href='%R/forum?x=%d(iOfst+iLimit)&n=%d(iLimit) \ |
| 2684 | if( bFilter ){ |
| 2685 | @ &status=%T(zStatusFilter)\ |
| 2686 | } |
| 2687 | @ '>↓ Older...</a></td></tr> |
| 2688 | fossil_free(zAge); |
| 2689 | break; |
| @@ -2678,10 +2716,11 @@ | |
| 2716 | @</tr> |
| 2717 | fossil_free(zAge); |
| 2718 | } |
| 2719 | db_finalize(&q); |
| 2720 | if( qStat.pStmt ) db_finalize(&qStat); |
| 2721 | sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0,0,0,0); |
| 2722 | } |
| 2723 | if( iCnt>0 ){ |
| 2724 | @ </table></div> |
| 2725 | }else{ |
| 2726 | @ <h1>No forum posts found</h1> |
| 2727 |