| | @@ -89,17 +89,12 @@ |
| 89 | 89 | ** 7. True if is a leaf |
| 90 | 90 | ** 8. background color |
| 91 | 91 | ** 9. type ("ci", "w") |
| 92 | 92 | */ |
| 93 | 93 | void www_print_timeline( |
| 94 | | - Stmt *pQuery, |
| 95 | | - int *pFirstEvent, |
| 96 | | - int *pLastEvent, |
| 97 | | - int (*xCallback)(int, Blob*), |
| 98 | | - Blob *pArg |
| 94 | + Stmt *pQuery |
| 99 | 95 | ){ |
| 100 | | - int cnt = 0; |
| 101 | 96 | int wikiFlags; |
| 102 | 97 | int mxWikiLen; |
| 103 | 98 | Blob comment; |
| 104 | 99 | char zPrevDate[20]; |
| 105 | 100 | zPrevDate[0] = 0; |
| | @@ -125,21 +120,11 @@ |
| 125 | 120 | int isLeaf = db_column_int(pQuery, 7); |
| 126 | 121 | const char *zBgClr = db_column_text(pQuery, 8); |
| 127 | 122 | const char *zDate = db_column_text(pQuery, 2); |
| 128 | 123 | const char *zType = db_column_text(pQuery, 9); |
| 129 | 124 | const char *zUser = db_column_text(pQuery, 4); |
| 130 | | - if( cnt==0 && pFirstEvent ){ |
| 131 | | - *pFirstEvent = rid; |
| 132 | | - } |
| 133 | | - cnt++; |
| 134 | | - if( pLastEvent ){ |
| 135 | | - *pLastEvent = rid; |
| 136 | | - } |
| 137 | 125 | db_multi_exec("INSERT OR IGNORE INTO seen VALUES(%d)", rid); |
| 138 | | - if( xCallback ){ |
| 139 | | - xCallback(rid, pArg); |
| 140 | | - } |
| 141 | 126 | if( memcmp(zDate, zPrevDate, 10) ){ |
| 142 | 127 | sprintf(zPrevDate, "%.10s", zDate); |
| 143 | 128 | @ <tr><td colspan=3> |
| 144 | 129 | @ <div class="divider">%s(zPrevDate)</div> |
| 145 | 130 | @ </td></tr> |
| | @@ -183,38 +168,28 @@ |
| 183 | 168 | } |
| 184 | 169 | @ </table> |
| 185 | 170 | } |
| 186 | 171 | |
| 187 | 172 | /* |
| 188 | | -** Generate javascript code that records the parents and children |
| 189 | | -** of the version rid. |
| 173 | +** Create a temporary table suitable for storing timeline data. |
| 190 | 174 | */ |
| 191 | | -static int save_parentage_javascript(int rid, Blob *pOut){ |
| 192 | | - const char *zSep; |
| 193 | | - Stmt q; |
| 194 | | - |
| 195 | | - db_prepare(&q, "SELECT pid FROM plink WHERE cid=%d", rid); |
| 196 | | - zSep = ""; |
| 197 | | - blob_appendf(pOut, "parentof[\"m%d\"] = [", rid); |
| 198 | | - while( db_step(&q)==SQLITE_ROW ){ |
| 199 | | - int pid = db_column_int(&q, 0); |
| 200 | | - blob_appendf(pOut, "%s\"m%d\"", zSep, pid); |
| 201 | | - zSep = ","; |
| 202 | | - } |
| 203 | | - db_finalize(&q); |
| 204 | | - blob_appendf(pOut, "];\n"); |
| 205 | | - db_prepare(&q, "SELECT cid FROM plink WHERE pid=%d", rid); |
| 206 | | - zSep = ""; |
| 207 | | - blob_appendf(pOut, "childof[\"m%d\"] = [", rid); |
| 208 | | - while( db_step(&q)==SQLITE_ROW ){ |
| 209 | | - int pid = db_column_int(&q, 0); |
| 210 | | - blob_appendf(pOut, "%s\"m%d\"", zSep, pid); |
| 211 | | - zSep = ","; |
| 212 | | - } |
| 213 | | - db_finalize(&q); |
| 214 | | - blob_appendf(pOut, "];\n"); |
| 215 | | - return 0; |
| 175 | +static void timeline_temp_table(void){ |
| 176 | + static const char zSql[] = |
| 177 | + @ CREATE TEMP TABLE IF NOT EXISTS timeline( |
| 178 | + @ rid INTEGER PRIMARY KEY, |
| 179 | + @ uuid TEXT, |
| 180 | + @ timestamp TEXT, |
| 181 | + @ comment TEXT, |
| 182 | + @ user TEXT, |
| 183 | + @ nchild INTEGER, |
| 184 | + @ nparent INTEGER, |
| 185 | + @ isleaf BOOLEAN, |
| 186 | + @ bgcolor TEXT, |
| 187 | + @ etype TEXT |
| 188 | + @ ) |
| 189 | + ; |
| 190 | + db_multi_exec(zSql); |
| 216 | 191 | } |
| 217 | 192 | |
| 218 | 193 | /* |
| 219 | 194 | ** Return a pointer to a constant string that forms the basis |
| 220 | 195 | ** for a timeline query for the WWW interface. |
| | @@ -241,36 +216,37 @@ |
| 241 | 216 | /* |
| 242 | 217 | ** WEBPAGE: timeline |
| 243 | 218 | ** |
| 244 | 219 | ** Query parameters: |
| 245 | 220 | ** |
| 246 | | -** d=STARTDATE date in iso8601 notation. dflt: newest event |
| 247 | | -** n=INTEGER number of events to show. dflt: 25 |
| 248 | | -** e=INTEGER starting event id. dflt: nil |
| 249 | | -** u=NAME show only events from user. dflt: nil |
| 250 | | -** a show events after and including. dflt: false |
| 251 | | -** r show only related events. dflt: false |
| 252 | | -** y=TYPE show only TYPE ('ci' or 'w') dflt: nil |
| 253 | | -** s show the SQL dflt: nil |
| 221 | +** a=TIMESTAMP after this date |
| 222 | +** b=TIMESTAMP before this date. |
| 223 | +** n=COUNT number of events in output |
| 224 | +** p=RID artifact RID and up to COUNT parents and ancestors |
| 225 | +** d=RID artifact RID and up to COUNT descendents |
| 226 | +** u=USER only if belonging to this user |
| 227 | +** y=TYPE 'ci', 'w', 'tkt' |
| 228 | +** |
| 229 | +** p= and d= can appear individually or together. If either p= or d= |
| 230 | +** appear, then u=, y=, a=, and b= are ignored. |
| 231 | +** |
| 232 | +** If a= and b= appear, only a= is used. If neither appear, the most |
| 233 | +** recent events are choosen. |
| 234 | +** |
| 235 | +** If n= is missing, the default count is 20. |
| 254 | 236 | */ |
| 255 | 237 | void page_timeline(void){ |
| 256 | | - Stmt q; |
| 257 | | - Blob sql; /* text of SQL used to generate timeline */ |
| 258 | | - char *zSQL; /* Rendered copy of sql */ |
| 259 | | - Blob scriptInit; |
| 260 | | - char zDate[100]; |
| 261 | | - const char *zStart = P("d"); /* Starting date */ |
| 238 | + Stmt q; /* Query used to generate the timeline */ |
| 239 | + Blob sql; /* text of SQL used to generate timeline */ |
| 240 | + Blob desc; /* Description of the timeline */ |
| 262 | 241 | int nEntry = atoi(PD("n","20")); /* Max number of entries on timeline */ |
| 242 | + int p_rid = atoi(PD("p","0")); /* artifact p and its parents */ |
| 243 | + int d_rid = atoi(PD("d","0")); /* artifact d and its descendents */ |
| 263 | 244 | const char *zUser = P("u"); /* All entries by this user if not NULL */ |
| 264 | | - int objid = atoi(PD("e","0")); /* Entries related to this event */ |
| 265 | | - int relatedEvents = P("r")!=0; /* Must be directly related to of objid */ |
| 266 | | - int afterFlag = P("a")!=0; /* After objid if true */ |
| 267 | 245 | const char *zType = P("y"); /* Type of events. All if NULL */ |
| 268 | | - int firstEvent; /* First event displayed */ |
| 269 | | - int lastEvent; /* Last event displayed */ |
| 270 | | - Blob desc; /* Human readable description of the timeline */ |
| 271 | | - const char *zEType; /* Human readable event type */ |
| 246 | + const char *zAfter = P("a"); /* Events after this time */ |
| 247 | + const char *zBefore = P("b"); /* Events before this time */ |
| 272 | 248 | |
| 273 | 249 | /* To view the timeline, must have permission to read project data. |
| 274 | 250 | */ |
| 275 | 251 | login_check_credentials(); |
| 276 | 252 | if( !g.okRead ){ login_needed(); return; } |
| | @@ -281,100 +257,158 @@ |
| 281 | 257 | " WHERE login='anonymous'" |
| 282 | 258 | " AND cap LIKE '%%h%%'") ){ |
| 283 | 259 | @ <p><b>Note:</b> You will be able to access <u>much</u> more |
| 284 | 260 | @ historical information if you <a href="%s(g.zTop)/login">login</a>.</p> |
| 285 | 261 | } |
| 262 | + timeline_temp_table(); |
| 286 | 263 | blob_zero(&sql); |
| 287 | 264 | blob_zero(&desc); |
| 265 | + blob_append(&sql, "INSERT OR IGNORE INTO timeline ", -1); |
| 288 | 266 | blob_append(&sql, timeline_query_for_www(), -1); |
| 289 | | - zEType = "events"; |
| 290 | | - if( zType ){ |
| 291 | | - blob_appendf(&sql, " AND event.type=%Q", zType); |
| 292 | | - if( zType[0]=='c' ){ |
| 293 | | - zEType = "checkins"; |
| 294 | | - }else if( zType[0]=='w' ){ |
| 295 | | - zEType = "wiki edits"; |
| 296 | | - } |
| 297 | | - } |
| 298 | | - blob_appendf(&desc, "Timeline of up to %d %s", nEntry, zEType); |
| 299 | | - if( zUser ){ |
| 300 | | - blob_appendf(&sql, " AND event.user=%Q", zUser); |
| 301 | | - blob_appendf(&desc, " by user %h", zUser); |
| 302 | | - } |
| 303 | | - if( objid ){ |
| 304 | | - char *z = db_text(0, "SELECT datetime(event.mtime, 'localtime') FROM event" |
| 305 | | - " WHERE objid=%d", objid); |
| 306 | | - if( z ){ |
| 307 | | - zStart = z; |
| 308 | | - } |
| 309 | | - } |
| 310 | | - if( zStart ){ |
| 311 | | - while( isspace(zStart[0]) ){ zStart++; } |
| 312 | | - if( zStart[0] ){ |
| 313 | | - blob_appendf(&sql, |
| 314 | | - " AND event.mtime %s (SELECT julianday(%Q, 'utc'))", |
| 315 | | - afterFlag ? ">=" : "<=", zStart); |
| 316 | | - blob_appendf(&desc, " occurring on or %s %h", |
| 317 | | - afterFlag ? "after": "before", |
| 318 | | - zStart); |
| 319 | | - } |
| 320 | | - } |
| 321 | | - if( relatedEvents && objid ){ |
| 267 | + if( p_rid || d_rid ){ |
| 268 | + /* If p= or d= is present, ignore all other parameters other than n= */ |
| 322 | 269 | char *zUuid; |
| 270 | + int np, nd; |
| 271 | + |
| 272 | + if( p_rid && d_rid && p_rid!=d_rid ) p_rid = d_rid; |
| 323 | 273 | db_multi_exec( |
| 324 | 274 | "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)" |
| 325 | 275 | ); |
| 326 | | - zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", objid); |
| 327 | | - if( afterFlag ){ |
| 328 | | - compute_descendents(objid, nEntry); |
| 329 | | - blob_appendf(&desc, |
| 330 | | - " and decended from <a href='%s/vinfo/%d'>[%.10s]</a>", |
| 331 | | - g.zBaseURL, objid, zUuid); |
| 332 | | - }else{ |
| 333 | | - compute_ancestors(objid, nEntry); |
| 334 | | - blob_appendf(&desc, |
| 335 | | - " and a ancestor of <a href='%s/vinfo/%d'>[%.10s]</a>", |
| 336 | | - g.zBaseURL, objid, zUuid); |
| 337 | | - } |
| 338 | | - blob_append(&sql, " AND event.objid IN ok", -1); |
| 339 | | - } |
| 340 | | - if( afterFlag ){ |
| 341 | | - blob_appendf(&sql, " ORDER BY event.mtime ASC LIMIT %d", |
| 342 | | - nEntry); |
| 343 | | - }else{ |
| 344 | | - blob_appendf(&sql, " ORDER BY event.mtime DESC LIMIT %d", |
| 345 | | - nEntry); |
| 346 | | - } |
| 347 | | - zSQL = blob_str(&sql); |
| 348 | | - if( afterFlag ){ |
| 349 | | - zSQL = mprintf("SELECT * FROM (%s) ORDER BY timestamp DESC", zSQL); |
| 350 | | - } |
| 351 | | - db_prepare(&q, zSQL); |
| 352 | | - if( P("s")!=0 ){ |
| 353 | | - @ <hr><p>%h(zSQL)</p><hr> |
| 354 | | - } |
| 276 | + zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", |
| 277 | + p_rid ? p_rid : d_rid); |
| 278 | + blob_appendf(&sql, " AND event.objid IN ok"); |
| 279 | + nd = 0; |
| 280 | + if( d_rid ){ |
| 281 | + compute_descendents(d_rid, nEntry); |
| 282 | + nd = db_int(0, "SELECT count(*)-1 FROM ok"); |
| 283 | + if( nd>0 ){ |
| 284 | + db_multi_exec("%s", blob_str(&sql)); |
| 285 | + blob_appendf(&desc, "%d descendents", nd); |
| 286 | + } |
| 287 | + db_multi_exec("DELETE FROM ok"); |
| 288 | + } |
| 289 | + if( p_rid ){ |
| 290 | + compute_ancestors(p_rid, nEntry); |
| 291 | + np = db_int(0, "SELECT count(*)-1 FROM ok"); |
| 292 | + if( np>0 ){ |
| 293 | + if( nd>0 ) blob_appendf(&desc, " and "); |
| 294 | + blob_appendf(&desc, "%d ancestors", np); |
| 295 | + db_multi_exec("%s", blob_str(&sql)); |
| 296 | + } |
| 297 | + } |
| 298 | + blob_appendf(&desc, " of <a href='%s/info/%s'>[%.10s]</a>", |
| 299 | + g.zBaseURL, zUuid, zUuid); |
| 300 | + db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC"); |
| 301 | + }else{ |
| 302 | + int n; |
| 303 | + Blob url; |
| 304 | + const char *zEType = "event"; |
| 305 | + const char *zDate; |
| 306 | + blob_zero(&url); |
| 307 | + blob_appendf(&url, "%s/timeline?n=%d", g.zBaseURL, nEntry); |
| 308 | + if( zType ){ |
| 309 | + blob_appendf(&sql, " AND event.type=%Q", zType); |
| 310 | + blob_appendf(&url, "&y=%T", zType); |
| 311 | + if( zType[0]=='c' ){ |
| 312 | + zEType = "checkin"; |
| 313 | + }else if( zType[0]=='w' ){ |
| 314 | + zEType = "wiki edit"; |
| 315 | + }else if( zType[0]=='t' ){ |
| 316 | + zEType = "ticket change"; |
| 317 | + } |
| 318 | + } |
| 319 | + if( zUser ){ |
| 320 | + blob_appendf(&sql, " AND event.user=%Q", zUser); |
| 321 | + blob_appendf(&url, "&u=%T", zUser); |
| 322 | + } |
| 323 | + if( zAfter ){ |
| 324 | + while( isspace(zAfter[0]) ){ zAfter++; } |
| 325 | + if( zAfter[0] ){ |
| 326 | + blob_appendf(&sql, |
| 327 | + " AND event.mtime>=(SELECT julianday(%Q, 'utc'))" |
| 328 | + " ORDER BY event.mtime ASC", zAfter); |
| 329 | + zBefore = 0; |
| 330 | + }else{ |
| 331 | + zAfter = 0; |
| 332 | + } |
| 333 | + }else if( zBefore ){ |
| 334 | + while( isspace(zBefore[0]) ){ zBefore++; } |
| 335 | + if( zBefore[0] ){ |
| 336 | + blob_appendf(&sql, |
| 337 | + " AND event.mtime<=(SELECT julianday(%Q, 'utc'))" |
| 338 | + " ORDER BY event.mtime DESC", zBefore); |
| 339 | + }else{ |
| 340 | + zBefore = 0; |
| 341 | + } |
| 342 | + }else{ |
| 343 | + blob_appendf(&sql, " ORDER BY event.mtime DESC"); |
| 344 | + } |
| 345 | + blob_appendf(&sql, " LIMIT %d", nEntry); |
| 346 | + db_multi_exec("%s", blob_str(&sql)); |
| 347 | + |
| 348 | + n = db_int(0, "SELECT count(*) FROM timeline"); |
| 349 | + if( zAfter==0 && zBefore==0 ){ |
| 350 | + blob_appendf(&desc, "%d most recent %ss", n, zEType); |
| 351 | + }else{ |
| 352 | + blob_appendf(&desc, "%d %ss", n, zEType); |
| 353 | + } |
| 354 | + if( zUser ){ |
| 355 | + blob_appendf(&desc, " by user %h", zUser); |
| 356 | + } |
| 357 | + if( zAfter ){ |
| 358 | + blob_appendf(&desc, " occurring on or after %h.<br>", zAfter); |
| 359 | + }else if( zBefore ){ |
| 360 | + blob_appendf(&desc, " occurring on or before %h.<br>", zBefore); |
| 361 | + } |
| 362 | + if( zAfter || n==nEntry ){ |
| 363 | + zDate = db_text(0, "SELECT min(timestamp) FROM timeline"); |
| 364 | + blob_appendf(&desc, " <a href='%b&b=%s'>[older]</a>", &url, zDate); |
| 365 | + } |
| 366 | + if( zBefore || (zAfter && n==nEntry) ){ |
| 367 | + zDate = db_text(0, "SELECT max(timestamp) FROM timeline"); |
| 368 | + blob_appendf(&desc, " <a href='%b&a=%s'>[more recent]</a>", &url, zDate); |
| 369 | + } |
| 370 | + } |
| 371 | + blob_zero(&sql); |
| 372 | + db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC"); |
| 355 | 373 | @ <h2>%b(&desc)</h2> |
| 356 | 374 | blob_reset(&desc); |
| 357 | | - blob_zero(&sql); |
| 358 | | - if( afterFlag ){ |
| 359 | | - free(zSQL); |
| 360 | | - } |
| 361 | | - zDate[0] = 0; |
| 362 | | - blob_zero(&scriptInit); |
| 363 | | - zDate[0] = 0; |
| 364 | | - www_print_timeline(&q, &firstEvent, &lastEvent, |
| 365 | | - save_parentage_javascript, &scriptInit); |
| 375 | + www_print_timeline(&q); |
| 366 | 376 | db_finalize(&q); |
| 367 | | - /* @ <p>firstEvent=%d(firstEvent) lastEvent=%d(lastEvent)</p> */ |
| 368 | | - if( zStart==0 ){ |
| 369 | | - zStart = zDate; |
| 370 | | - } |
| 377 | + |
| 371 | 378 | @ <script> |
| 372 | 379 | @ var parentof = new Object(); |
| 373 | 380 | @ var childof = new Object(); |
| 374 | | - cgi_append_content(blob_buffer(&scriptInit), blob_size(&scriptInit)); |
| 375 | | - blob_reset(&scriptInit); |
| 381 | + db_prepare(&q, "SELECT rid FROM timeline"); |
| 382 | + while( db_step(&q)==SQLITE_ROW ){ |
| 383 | + int rid = db_column_int(&q, 0); |
| 384 | + Stmt q2; |
| 385 | + const char *zSep; |
| 386 | + Blob *pOut = cgi_output_blob(); |
| 387 | + |
| 388 | + db_prepare(&q2, "SELECT pid FROM plink WHERE cid=%d", rid); |
| 389 | + zSep = ""; |
| 390 | + blob_appendf(pOut, "parentof[\"m%d\"] = [", rid); |
| 391 | + while( db_step(&q2)==SQLITE_ROW ){ |
| 392 | + int pid = db_column_int(&q2, 0); |
| 393 | + blob_appendf(pOut, "%s\"m%d\"", zSep, pid); |
| 394 | + zSep = ","; |
| 395 | + } |
| 396 | + db_finalize(&q2); |
| 397 | + blob_appendf(pOut, "];\n"); |
| 398 | + db_prepare(&q2, "SELECT cid FROM plink WHERE pid=%d", rid); |
| 399 | + zSep = ""; |
| 400 | + blob_appendf(pOut, "childof[\"m%d\"] = [", rid); |
| 401 | + while( db_step(&q2)==SQLITE_ROW ){ |
| 402 | + int pid = db_column_int(&q2, 0); |
| 403 | + blob_appendf(pOut, "%s\"m%d\"", zSep, pid); |
| 404 | + zSep = ","; |
| 405 | + } |
| 406 | + db_finalize(&q2); |
| 407 | + blob_appendf(pOut, "];\n"); |
| 408 | + } |
| 409 | + db_finalize(&q); |
| 376 | 410 | @ function setall(value){ |
| 377 | 411 | @ for(var x in parentof){ |
| 378 | 412 | @ setone(x,value); |
| 379 | 413 | @ } |
| 380 | 414 | @ } |
| | @@ -431,30 +465,10 @@ |
| 431 | 465 | @ set_children(cid,clr); |
| 432 | 466 | @ } |
| 433 | 467 | @ } |
| 434 | 468 | @ } |
| 435 | 469 | @ </script> |
| 436 | | - @ <hr> |
| 437 | | - @ <form method="GET" action="%s(g.zBaseURL)/timeline"> |
| 438 | | - @ Start Date: |
| 439 | | - @ <input type="text" size="30" value="%h(zStart)" name="d"> |
| 440 | | - @ Number Of Entries: |
| 441 | | - @ <input type="text" size="4" value="%d(nEntry)" name="n"> |
| 442 | | - @ <br><input type="submit" value="Submit"> |
| 443 | | - @ </form> |
| 444 | | - @ <table><tr><td> |
| 445 | | - @ <form method="GET" action="%s(g.zBaseURL)/timeline"> |
| 446 | | - @ <input type="hidden" value="%d(lastEvent)" name="e"> |
| 447 | | - @ <input type="hidden" value="%d(nEntry)" name="n"> |
| 448 | | - @ <input type="submit" value="Next %d(nEntry) Rows"> |
| 449 | | - @ </form></td><td> |
| 450 | | - @ <form method="GET" action="%s(g.zBaseURL)/timeline"> |
| 451 | | - @ <input type="hidden" value="%d(firstEvent)" name="e"> |
| 452 | | - @ <input type="hidden" value="%d(nEntry)" name="n"> |
| 453 | | - @ <input type="hidden" value="1" name="a"> |
| 454 | | - @ <input type="submit" value="Previous %d(nEntry) Rows"> |
| 455 | | - @ </form></td></tr></table> |
| 456 | 470 | style_footer(); |
| 457 | 471 | } |
| 458 | 472 | |
| 459 | 473 | /* |
| 460 | 474 | ** The input query q selects various records. Print a human-readable |
| 461 | 475 | |