Fossil SCM

Enhancements to the /reports interface, including improved filtering by user.

drh 2015-05-19 10:24 trunk merge
Commit 16ab6ee852cdebbf9b8a2cb33d8d5be4892f88f3
+251 -268
--- src/statrep.c
+++ src/statrep.c
@@ -141,63 +141,10 @@
141141
default:
142142
return "all types";
143143
}
144144
}
145145
146
-/*
147
-** A helper for the /reports family of pages which prints out a menu
148
-** of links for the various type=XXX flags. zCurrentViewName must be
149
-** the name/value of the 'view' parameter which is in effect at the
150
-** time this is called. e.g. if called from the 'byuser' view then
151
-** zCurrentViewName must be "byuser". Any URL parameters which need to
152
-** be added to the generated URLs should be passed in zParam. The
153
-** caller is expected to have already encoded any zParam in the %T or
154
-** %t encoding. */
155
-static void stats_report_event_types_menu(const char *zCurrentViewName,
156
- const char *zParam){
157
- char *zTop;
158
- if(zParam && !*zParam){
159
- zParam = NULL;
160
- }
161
- zTop = mprintf("%s/reports?view=%s%s%s", g.zTop, zCurrentViewName,
162
- zParam ? "&" : "", zParam);
163
- cgi_printf("<div>");
164
- cgi_printf("<span>Types:</span> ");
165
- if('*' == statsReportType){
166
- cgi_printf(" <strong>all</strong>", zTop);
167
- }else{
168
- cgi_printf(" <a href='%s'>all</a>", zTop);
169
- }
170
- if('c' == statsReportType){
171
- cgi_printf(" <strong>check-ins</strong>", zTop);
172
- }else{
173
- cgi_printf(" <a href='%s&type=ci'>check-ins</a>", zTop);
174
- }
175
- if('e' == statsReportType){
176
- cgi_printf(" <strong>technotes</strong>", zTop);
177
- }else{
178
- cgi_printf(" <a href='%s&type=e'>technotes</a>", zTop);
179
- }
180
- if( 't' == statsReportType ){
181
- cgi_printf(" <strong>tickets</strong>", zTop);
182
- }else{
183
- cgi_printf(" <a href='%s&type=t'>tickets</a>", zTop);
184
- }
185
- if( 'g' == statsReportType ){
186
- cgi_printf(" <strong>tags</strong>", zTop);
187
- }else{
188
- cgi_printf(" <a href='%s&type=g'>tags</a>", zTop);
189
- }
190
- if( 'w' == statsReportType ){
191
- cgi_printf(" <strong>wiki</strong>", zTop);
192
- }else{
193
- cgi_printf(" <a href='%s&type=w'>wiki</a>", zTop);
194
- }
195
- fossil_free(zTop);
196
- cgi_printf("</div>");
197
-}
198
-
199146
200147
/*
201148
** Helper for stats_report_by_month_year(), which generates a list of
202149
** week numbers. zTimeframe should be either a timeframe in the form YYYY
203150
** or YYYY-MM.
@@ -227,23 +174,21 @@
227174
}
228175
229176
/*
230177
** Implements the "byyear" and "bymonth" reports for /reports.
231178
** If includeMonth is true then it generates the "bymonth" report,
232
-** else the "byyear" report. If zUserName is not NULL and not empty
233
-** then the report is restricted to events created by the named user
234
-** account.
179
+** else the "byyear" report. If zUserName is not NULL then the report is
180
+** restricted to events created by the named user account.
235181
*/
236182
static void stats_report_by_month_year(char includeMonth,
237183
char includeWeeks,
238184
const char *zUserName){
239185
Stmt query = empty_Stmt;
240186
int nRowNumber = 0; /* current TR number */
241187
int nEventTotal = 0; /* Total event count */
242188
int rowClass = 0; /* counter for alternating
243189
row colors */
244
- Blob sql = empty_blob; /* SQL */
245190
const char *zTimeLabel = includeMonth ? "Year/Month" : "Year";
246191
char zPrevYear[5] = {0}; /* For keeping track of when
247192
we change years while looping */
248193
int nEventsPerYear = 0; /* Total event count for the
249194
current year */
@@ -252,39 +197,37 @@
252197
Blob header = empty_blob; /* Page header text */
253198
int nMaxEvents = 1; /* for calculating length of graph
254199
bars. */
255200
int iterations = 0; /* number of weeks/months we iterate
256201
over */
202
+ Blob userFilter = empty_blob; /* Optional user=johndoe query string */
257203
stats_report_init_view();
258
- stats_report_event_types_menu( includeMonth ? "bymonth" : "byyear", NULL );
259
- blob_appendf(&header, "Timeline Events (%s) by year%s",
260
- stats_report_label_for_type(),
261
- (includeMonth ? "/month" : ""));
262
- blob_append_sql(&sql,
263
- "SELECT substr(date(mtime),1,%d) AS timeframe, "
264
- "count(*) AS eventCount "
265
- "FROM v_reports ",
266
- includeMonth ? 7 : 4);
267
- if(zUserName&&*zUserName){
268
- blob_append_sql(&sql, " WHERE user=%Q ", zUserName);
269
- blob_appendf(&header," for user %q", zUserName);
270
- }
271
- blob_append(&sql,
272
- " GROUP BY timeframe"
273
- " ORDER BY timeframe DESC",
274
- -1);
275
- db_prepare(&query, "%s", blob_sql_text(&sql));
276
- blob_reset(&sql);
277
- @ <h1>%b(&header)</h1>
204
+ if( zUserName ){
205
+ blob_appendf(&userFilter, "user=%s", zUserName);
206
+ }
207
+ blob_reset(&userFilter);
208
+ db_prepare(&query,
209
+ "SELECT substr(date(mtime),1,%d) AS timeframe,"
210
+ " count(*) AS eventCount"
211
+ " FROM v_reports"
212
+ " WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
213
+ " GROUP BY timeframe"
214
+ " ORDER BY timeframe DESC",
215
+ includeMonth ? 7 : 4, zUserName);
216
+ @ <h1>Timeline Events (%s(stats_report_label_for_type()))
217
+ @ by year%s(includeMonth ? "/month" : "")
218
+ if( zUserName ){
219
+ @ for user %h(zUserName)
220
+ }
221
+ @ </h1>
278222
@ <table class='statistics-report-table-events' border='0' cellpadding='2'
279223
@ cellspacing='0' id='statsTable'>
280224
@ <thead>
281225
@ <th>%s(zTimeLabel)</th>
282226
@ <th>Events</th>
283227
@ <th width='90%%'><!-- relative commits graph --></th>
284228
@ </thead><tbody>
285
- blob_reset(&header);
286229
/*
287230
Run the query twice. The first time we calculate the maximum
288231
number of events for a given row. Maybe someone with better SQL
289232
Fu can re-implement this with a single query.
290233
*/
@@ -335,18 +278,18 @@
335278
zTimeframe, nCount,
336279
statsReportTimelineYFlag );
337280
/* Reminder: n=nCount is not actually correct for bymonth unless
338281
that was the only user who caused events.
339282
*/
340
- if( zUserName && *zUserName ){
283
+ if( zUserName ){
341284
cgi_printf("&u=%t", zUserName);
342285
}
343286
cgi_printf("' target='_new'>%s</a>",zTimeframe);
344287
}else {
345288
cgi_printf("<a href='?view=byweek&y=%s&type=%c",
346289
zTimeframe, (char)statsReportType);
347
- if(zUserName && *zUserName){
290
+ if( zUserName ){
348291
cgi_printf("&u=%t", zUserName);
349292
}
350293
cgi_printf("'>%s</a>", zTimeframe);
351294
}
352295
@ </td><td>%d(nCount)</td>
@@ -403,17 +346,16 @@
403346
int rowClass = 0; /* counter for alternating
404347
row colors */
405348
int nMaxEvents = 1; /* max number of events for
406349
all rows. */
407350
stats_report_init_view();
408
- stats_report_event_types_menu("byuser", NULL);
409351
@ <h1>Timeline Events
410352
@ (%s(stats_report_label_for_type())) by User</h1>
411353
db_multi_exec(
412354
"CREATE TEMP TABLE piechart(amt,label);"
413
- "INSERT INTO piechart SELECT count(*), user FROM v_reports"
414
- " GROUP BY user ORDER BY count(*) DESC;"
355
+ "INSERT INTO piechart SELECT count(*), ifnull(euser,user) FROM v_reports"
356
+ " GROUP BY ifnull(euser,user) ORDER BY count(*) DESC;"
415357
);
416358
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
417359
@ <center><svg width=700 height=400>
418360
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
419361
@ </svg></centre><hr/>
@@ -424,14 +366,14 @@
424366
@ <th>User</th>
425367
@ <th>Events</th>
426368
@ <th width='90%%'><!-- relative commits graph --></th>
427369
@ </tr></thead><tbody>
428370
db_prepare(&query,
429
- "SELECT user, "
371
+ "SELECT ifnull(euser,user), "
430372
"COUNT(*) AS eventCount "
431373
"FROM v_reports "
432
- "GROUP BY user ORDER BY eventCount DESC");
374
+ "GROUP BY ifnull(euser,user) ORDER BY eventCount DESC");
433375
while( SQLITE_ROW == db_step(&query) ){
434376
const int nCount = db_column_int(&query, 1);
435377
if(nCount>nMaxEvents){
436378
nMaxEvents = nCount;
437379
}
@@ -438,20 +380,21 @@
438380
}
439381
db_reset(&query);
440382
while( SQLITE_ROW == db_step(&query) ){
441383
const char *zUser = db_column_text(&query, 0);
442384
const int nCount = db_column_int(&query, 1);
385
+ char y = (char)statsReportType;
443386
int nSize = nCount
444387
? (int)(100 * nCount / nMaxEvents)
445388
: 0;
446389
if(!nCount) continue /* arguable! Possible? */;
447390
else if(!nSize) nSize = 1;
448391
rowClass = ++nRowNumber % 2;
449392
nEventTotal += nCount;
450
- @<tr class='row%d(rowClass)'>
393
+ @ <tr class='row%d(rowClass)'>
451394
@ <td>
452
- @ <a href="?view=bymonth&user=%h(zUser)&type=%c((char)statsReportType)">%h(zUser)</a>
395
+ @ <a href="?view=bymonth&user=%h(zUser)&type=%c(y)">%h(zUser)</a>
453396
@ </td><td data-sortkey='%08x(-nCount)'>%d(nCount)</td>
454397
@ <td>
455398
@ <div class='statistics-report-graph-line'
456399
@ style='width:%d(nSize)%%;'>&nbsp;</div>
457400
@ </td>
@@ -465,30 +408,37 @@
465408
db_finalize(&query);
466409
output_table_sorting_javascript("statsTable","tkx",2);
467410
}
468411
469412
/*
470
-** Implements the "byfile" view for /reports.
413
+** Implements the "byfile" view for /reports. If zUserName is not NULL then the
414
+** report is restricted to events created by the named user account.
471415
*/
472
-static void stats_report_by_file(){
416
+static void stats_report_by_file(const char *zUserName){
473417
Stmt query;
474418
int mxEvent = 1; /* max number of events across all rows */
475419
int nRowNumber = 0;
476420
477421
db_multi_exec(
478422
"CREATE TEMP TABLE statrep(filename, cnt);"
479423
"INSERT INTO statrep(filename, cnt)"
480424
" SELECT filename.name, count(distinct mlink.mid)"
481
- " FROM filename, mlink"
425
+ " FROM filename, mlink, event"
482426
" WHERE filename.fnid=mlink.fnid"
483
- " GROUP BY 1;"
427
+ " AND mlink.mid=event.objid"
428
+ " AND ifnull(coalesce(euser,user,'')=%Q,1)"
429
+ " GROUP BY 1", zUserName
484430
);
485431
db_prepare(&query,
486432
"SELECT filename, cnt FROM statrep ORDER BY cnt DESC, filename /*sort*/"
487433
);
488434
mxEvent = db_int(1, "SELECT max(cnt) FROM statrep");
489
- @ <h1>Check-ins Per File</h1>
435
+ @ <h1>Check-ins Per File
436
+ if( zUserName ){
437
+ @ for user %h(zUserName)
438
+ }
439
+ @ </h1>
490440
@ <table class='statistics-report-table-events' border='0'
491441
@ cellpadding='2' cellspacing='0' id='statsTable'>
492442
@ <thead><tr>
493443
@ <th>File</th>
494444
@ <th>Check-ins</th>
@@ -514,41 +464,56 @@
514464
db_finalize(&query);
515465
output_table_sorting_javascript("statsTable","tNx",2);
516466
}
517467
518468
/*
519
-** Implements the "byweekday" view for /reports.
469
+** Implements the "byweekday" view for /reports. If zUserName is not NULL then
470
+** the report is restricted to events created by the named user account.
520471
*/
521
-static void stats_report_day_of_week(){
472
+static void stats_report_day_of_week(const char *zUserName){
522473
Stmt query = empty_Stmt;
523474
int nRowNumber = 0; /* current TR number */
524475
int nEventTotal = 0; /* Total event count */
525476
int rowClass = 0; /* counter for alternating
526477
row colors */
527478
int nMaxEvents = 1; /* max number of events for
528479
all rows. */
480
+ Blob userFilter = empty_blob; /* Optional user=johndoe query string */
529481
static const char *const daysOfWeek[] = {
530482
"Monday", "Tuesday", "Wednesday", "Thursday",
531483
"Friday", "Saturday", "Sunday"
532484
};
533485
534486
stats_report_init_view();
535
- stats_report_event_types_menu("byweekday", NULL);
487
+ if( zUserName ){
488
+ blob_appendf(&userFilter, "user=%s", zUserName);
489
+ }
536490
db_prepare(&query,
537
- "SELECT cast(mtime %% 7 AS INTEGER) dow, "
538
- "COUNT(*) AS eventCount "
539
- "FROM v_reports "
540
- "GROUP BY dow ORDER BY dow");
541
- @ <h1>Timeline Events
542
- @ (%s(stats_report_label_for_type())) by Day of the Week</h1>
491
+ "SELECT cast(mtime %% 7 AS INTEGER) dow,"
492
+ " COUNT(*) AS eventCount"
493
+ " FROM v_reports"
494
+ " WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
495
+ " GROUP BY dow ORDER BY dow", zUserName);
496
+ @ <h1>Timeline Events (%h(stats_report_label_for_type())) by Day of the Week
497
+ if( zUserName ){
498
+ @ for user %h(zUserName)
499
+ }
500
+ @ </h1>
543501
db_multi_exec(
544502
"CREATE TEMP TABLE piechart(amt,label);"
545
- "INSERT INTO piechart SELECT count(*), cast(mtime %% 7 AS INT) FROM v_reports"
546
- " GROUP BY 2 ORDER BY 2;"
547
- "UPDATE piechart SET label = CASE label WHEN 0 THEN 'Monday' WHEN 1 THEN 'Tuesday'"
548
- " WHEN 2 THEN 'Wednesday' WHEN 3 THEN 'Thursday' WHEN 4 THEN 'Friday'"
549
- " WHEN 5 THEN 'Saturday' ELSE 'Sunday' END;"
503
+ "INSERT INTO piechart"
504
+ " SELECT count(*), cast(mtime %% 7 AS INT) FROM v_reports"
505
+ " WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
506
+ " GROUP BY 2 ORDER BY 2;"
507
+ "UPDATE piechart SET label = CASE label"
508
+ " WHEN 0 THEN 'Monday'"
509
+ " WHEN 1 THEN 'Tuesday'"
510
+ " WHEN 2 THEN 'Wednesday'"
511
+ " WHEN 3 THEN 'Thursday'"
512
+ " WHEN 4 THEN 'Friday'"
513
+ " WHEN 5 THEN 'Saturday'"
514
+ " ELSE 'Sunday' END;", zUserName
550515
);
551516
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
552517
@ <center><svg width=700 height=400>
553518
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
554519
@ </svg></centre><hr/>
@@ -595,139 +560,114 @@
595560
596561
597562
/*
598563
** Helper for stats_report_by_month_year(), which generates a list of
599564
** week numbers. zTimeframe should be either a timeframe in the form YYYY
600
-** or YYYY-MM.
565
+** or YYYY-MM. If zUserName is not NULL then the report is restricted to events
566
+** created by the named user account.
601567
*/
602568
static void stats_report_year_weeks(const char *zUserName){
603
- const char *zYear = P("y");
604
- int nYear = zYear ? strlen(zYear) : 0;
569
+ const char *zYear = P("y"); /* Year for which report shown */
570
+ int isValidYear = 0; /* True if a valid year */
605571
int i = 0;
606
- Stmt qYears = empty_Stmt;
607
- char *zDefaultYear = NULL;
608
- Blob sql = empty_blob;
572
+ Stmt q;
609573
int nMaxEvents = 1; /* max number of events for
610574
all rows. */
611575
int iterations = 0; /* # of active time periods. */
612
- stats_report_init_view();
613
- if(4==nYear){
614
- Blob urlParams = empty_blob;
615
- blob_appendf(&urlParams, "y=%T", zYear);
616
- stats_report_event_types_menu("byweek", blob_str(&urlParams));
617
- blob_reset(&urlParams);
618
- }else{
619
- stats_report_event_types_menu("byweek", NULL);
620
- }
621
- blob_append(&sql,
622
- "SELECT DISTINCT substr(date(mtime),1,4) AS y "
623
- "FROM v_reports WHERE 1 ", -1);
624
- if(zUserName&&*zUserName){
625
- blob_append_sql(&sql,"AND user=%Q ", zUserName);
626
- }
627
- blob_append(&sql,"GROUP BY y ORDER BY y", -1);
628
- db_prepare(&qYears, "%s", blob_sql_text(&sql));
629
- blob_reset(&sql);
630
- cgi_printf("Select year: ");
631
- while( SQLITE_ROW == db_step(&qYears) ){
632
- const char *zT = db_column_text(&qYears, 0);
633
- if( i++ ){
634
- cgi_printf(" ");
635
- }
636
- cgi_printf("<a href='?view=byweek&y=%s&type=%c", zT,
637
- (char)statsReportType);
638
- if(zUserName && *zUserName){
639
- cgi_printf("&user=%t",zUserName);
640
- }
641
- cgi_printf("'>%s</a>",zT);
642
- }
643
- db_finalize(&qYears);
644
- cgi_printf("<br/>");
645
- if(!zYear || !*zYear){
646
- zDefaultYear = db_text("????", "SELECT strftime('%%Y')");
647
- zYear = zDefaultYear;
648
- nYear = 4;
649
- }
650
- if(4 == nYear){
651
- Stmt stWeek = empty_Stmt;
652
- int rowCount = 0;
653
- int total = 0;
654
- Blob header = empty_blob;
655
- blob_appendf(&header, "Timeline events (%s) for the calendar weeks "
656
- "of %h", stats_report_label_for_type(),
657
- zYear);
658
- blob_append_sql(&sql,
659
- "SELECT DISTINCT strftime('%%W',mtime) AS wk, "
660
- "count(*) AS n "
661
- "FROM v_reports "
662
- "WHERE %Q=substr(date(mtime),1,4) "
663
- "AND mtime < current_timestamp ",
664
- zYear);
665
- if(zUserName&&*zUserName){
666
- blob_append_sql(&sql, " AND user=%Q ", zUserName);
667
- blob_appendf(&header," for user %h", zUserName);
668
- }
669
- blob_append_sql(&sql, "GROUP BY wk ORDER BY wk DESC");
670
- cgi_printf("<h1>%h</h1>", blob_str(&header));
671
- blob_reset(&header);
672
- cgi_printf("<table class='statistics-report-table-events' "
673
- "border='0' cellpadding='2' width='100%%' "
674
- "cellspacing='0' id='statsTable'>");
675
- cgi_printf("<thead><tr>"
676
- "<th>Week</th>"
677
- "<th>Events</th>"
678
- "<th width='90%%'><!-- relative commits graph --></th>"
679
- "</tr></thead>"
680
- "<tbody>");
681
- db_prepare(&stWeek, "%s", blob_sql_text(&sql));
682
- blob_reset(&sql);
683
- while( SQLITE_ROW == db_step(&stWeek) ){
684
- const int nCount = db_column_int(&stWeek, 1);
685
- if(nCount>nMaxEvents){
686
- nMaxEvents = nCount;
687
- }
688
- ++iterations;
689
- }
690
- db_reset(&stWeek);
691
- while( SQLITE_ROW == db_step(&stWeek) ){
692
- const char *zWeek = db_column_text(&stWeek,0);
693
- const int nCount = db_column_int(&stWeek,1);
694
- int nSize = nCount
695
- ? (int)(100 * nCount / nMaxEvents)
696
- : 0;
697
- if(!nSize) nSize = 1;
698
- total += nCount;
699
- cgi_printf("<tr class='row%d'>", ++rowCount % 2 );
700
- cgi_printf("<td><a href='%R/timeline?yw=%t-%s&n=%d&y=%s",
701
- zYear, zWeek, nCount,
702
- statsReportTimelineYFlag);
703
- if(zUserName && *zUserName){
704
- cgi_printf("&u=%t",zUserName);
705
- }
706
- cgi_printf("'>%s</a></td>",zWeek);
707
-
708
- cgi_printf("<td>%d</td>",nCount);
709
- cgi_printf("<td>");
710
- if(nCount){
711
- cgi_printf("<div class='statistics-report-graph-line'"
712
- "style='width:%d%%;'>&nbsp;</div>",
713
- nSize);
714
- }
715
- cgi_printf("</td></tr>");
716
- }
717
- db_finalize(&stWeek);
718
- free(zDefaultYear);
719
- cgi_printf("</tbody></table>");
720
- if(total){
721
- int nAvg = iterations ? (total/iterations) : 0;
722
- cgi_printf("<br><div>Total events: %d<br>"
723
- "Average per active week: %d</div>",
724
- total, nAvg);
725
- }
726
- output_table_sorting_javascript("statsTable","tnx",-1);
727
- }
728
-}
576
+ int n = 0; /* Number of entries in azYear */
577
+ char **azYear = 0; /* Year dropdown menu */
578
+ int rowCount = 0;
579
+ int total = 0;
580
+
581
+ stats_report_init_view();
582
+ style_submenu_sql("y", "Year:",
583
+ "WITH RECURSIVE a(b) AS ("
584
+ " SELECT substr(date('now'),1,4) UNION ALL"
585
+ " SELECT b-1 FROM a"
586
+ " WHERE b>0+(SELECT substr(date(min(mtime)),1,4) FROM event)"
587
+ ") SELECT b, b FROM a ORDER BY b DESC");
588
+ if( zYear==0 || strlen(zYear)!=4 ){
589
+ zYear = db_text("1970","SELECT substr(date('now'),1,4);");
590
+ }
591
+ cgi_printf("<br/>");
592
+ db_prepare(&q,
593
+ "SELECT DISTINCT strftime('%%W',mtime) AS wk, "
594
+ " count(*) AS n "
595
+ " FROM v_reports "
596
+ " WHERE %Q=substr(date(mtime),1,4) "
597
+ " AND mtime < current_timestamp "
598
+ " AND ifnull(coalesce(euser,user,'')=%Q,1)"
599
+ " GROUP BY wk ORDER BY wk DESC", zYear, zUserName);
600
+ @ <h1>Timeline events (%h(stats_report_label_for_type()))
601
+ @ for the calendar weeks of %h(zYear)
602
+ if( zUserName ){
603
+ @ for user %h(zUserName)
604
+ }
605
+ @ </h1>
606
+ cgi_printf("<table class='statistics-report-table-events' "
607
+ "border='0' cellpadding='2' width='100%%' "
608
+ "cellspacing='0' id='statsTable'>");
609
+ cgi_printf("<thead><tr>"
610
+ "<th>Week</th>"
611
+ "<th>Events</th>"
612
+ "<th width='90%%'><!-- relative commits graph --></th>"
613
+ "</tr></thead>"
614
+ "<tbody>");
615
+ while( SQLITE_ROW == db_step(&q) ){
616
+ const int nCount = db_column_int(&q, 1);
617
+ if(nCount>nMaxEvents){
618
+ nMaxEvents = nCount;
619
+ }
620
+ ++iterations;
621
+ }
622
+ db_reset(&q);
623
+ while( SQLITE_ROW == db_step(&q) ){
624
+ const char *zWeek = db_column_text(&q,0);
625
+ const int nCount = db_column_int(&q,1);
626
+ int nSize = nCount
627
+ ? (int)(100 * nCount / nMaxEvents)
628
+ : 0;
629
+ if(!nSize) nSize = 1;
630
+ total += nCount;
631
+ cgi_printf("<tr class='row%d'>", ++rowCount % 2 );
632
+ cgi_printf("<td><a href='%R/timeline?yw=%t-%s&n=%d&y=%s",
633
+ zYear, zWeek, nCount,
634
+ statsReportTimelineYFlag);
635
+ if( zUserName ){
636
+ cgi_printf("&u=%t",zUserName);
637
+ }
638
+ cgi_printf("'>%s</a></td>",zWeek);
639
+
640
+ cgi_printf("<td>%d</td>",nCount);
641
+ cgi_printf("<td>");
642
+ if(nCount){
643
+ cgi_printf("<div class='statistics-report-graph-line'"
644
+ "style='width:%d%%;'>&nbsp;</div>",
645
+ nSize);
646
+ }
647
+ cgi_printf("</td></tr>");
648
+ }
649
+ db_finalize(&q);
650
+ cgi_printf("</tbody></table>");
651
+ if(total){
652
+ int nAvg = iterations ? (total/iterations) : 0;
653
+ cgi_printf("<br><div>Total events: %d<br>"
654
+ "Average per active week: %d</div>",
655
+ total, nAvg);
656
+ }
657
+ output_table_sorting_javascript("statsTable","tnx",-1);
658
+}
659
+
660
+/* Report types
661
+*/
662
+#define RPT_BYFILE 1
663
+#define RPT_BYMONTH 2
664
+#define RPT_BYUSER 3
665
+#define RPT_BYWEEK 4
666
+#define RPT_BYWEEKDAY 5
667
+#define RPT_BYYEAR 6
668
+#define RPT_NONE 0 /* None of the above */
729669
730670
/*
731671
** WEBPAGE: reports
732672
**
733673
** Shows activity reports for the repository.
@@ -747,51 +687,94 @@
747687
** y=YYYY The year to report (default is the server's
748688
** current year).
749689
*/
750690
void stats_report_page(){
751691
HQuery url; /* URL for various branch links */
752
- const char *zView = P("view"); /* Which view/report to show. */
753
- const char *zUserName = P("user");
754
-
692
+ const char *zView = P("view"); /* Which view/report to show. */
693
+ int eType = RPT_NONE; /* Numeric code for view/report to show */
694
+ int i; /* Loop counter */
695
+ const char *zUserName; /* Name of user */
696
+ const struct {
697
+ const char *zName; /* Name of view= screen type */
698
+ const char *zVal; /* Value of view= query parameter */
699
+ int eType; /* Corresponding RPT_* define */
700
+ } aViewType[] = {
701
+ { "File Changes","byfile", RPT_BYFILE },
702
+ { "By Month", "bymonth", RPT_BYMONTH },
703
+ { "By User", "byuser", RPT_BYUSER },
704
+ { "By Week", "byweek", RPT_BYWEEK },
705
+ { "By Weekday", "byweekday", RPT_BYWEEKDAY },
706
+ { "By Year", "byyear", RPT_BYYEAR },
707
+ };
708
+ const char *azType[] = {
709
+ "a", "All Changes",
710
+ "ci", "Check-ins",
711
+ "g", "Tags",
712
+ "e", "Tech Notes",
713
+ "t", "Tickets",
714
+ "w", "Wiki"
715
+ };
716
+
755717
login_check_credentials();
756718
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
757
- if(!zUserName) zUserName = P("u");
758
- url_initialize(&url, "reports");
759
- if(zUserName && *zUserName){
760
- url_add_parameter(&url,"user", zUserName);
761
- statrep_submenu(&url, "(Remove User Flag)", "view", zView, "user");
762
- }
763
- statrep_submenu(&url, "By Year", "view", "byyear", 0);
764
- statrep_submenu(&url, "By Month", "view", "bymonth", 0);
765
- statrep_submenu(&url, "By Week", "view", "byweek", 0);
766
- statrep_submenu(&url, "By Weekday", "view", "byweekday", 0);
767
- statrep_submenu(&url, "By User", "view", "byuser", "user");
768
- statrep_submenu(&url, "By File", "view", "byfile", "file");
719
+ zUserName = P("user");
720
+ if( zUserName==0 ) zUserName = P("u");
721
+ if( zUserName && zUserName[0]==0 ) zUserName = 0;
722
+ if( zView==0 ){
723
+ zView = "byuser";
724
+ cgi_replace_query_parameter("view","byuser");
725
+ }
726
+ for(i=0; i<ArraySize(aViewType); i++){
727
+ if( fossil_strcmp(zView, aViewType[i].zVal)==0 ){
728
+ eType = aViewType[i].eType;
729
+ break;
730
+ }
731
+ }
732
+ url_initialize(&url, "reports");
733
+ cgi_query_parameters_to_url(&url);
734
+ if( eType!=RPT_NONE ){
735
+ int nView = 0; /* Slots used in azView[] */
736
+ const char *azView[16]; /* Drop-down menu of view types */
737
+ for(i=0; i<ArraySize(aViewType); i++){
738
+ azView[nView++] = aViewType[i].zVal;
739
+ azView[nView++] = aViewType[i].zName;
740
+ }
741
+ if( eType!=RPT_BYFILE ){
742
+ style_submenu_multichoice("type", ArraySize(azType)/2, azType, 0);
743
+ }
744
+ style_submenu_multichoice("view", nView/2, azView, 0);
745
+ if( eType!=RPT_BYUSER ){
746
+ style_submenu_sql("u","User:",
747
+ "SELECT '', 'All Users' UNION ALL "
748
+ "SELECT x, x FROM ("
749
+ " SELECT DISTINCT trim(coalesce(euser,user)) AS x FROM event %s"
750
+ " ORDER BY 1 COLLATE nocase) WHERE x!=''",
751
+ eType==RPT_BYFILE ? "WHERE type='ci'" : ""
752
+ );
753
+ }
754
+ }
769755
style_submenu_element("Stats", "Stats", "%R/stat");
770756
url_reset(&url);
771757
style_header("Activity Reports");
772
- if(0==fossil_strcmp(zView,"byyear")){
773
- stats_report_by_month_year(0, 0, zUserName);
774
- }else if(0==fossil_strcmp(zView,"bymonth")){
775
- stats_report_by_month_year(1, 0, zUserName);
776
- }else if(0==fossil_strcmp(zView,"byweek")){
777
- stats_report_year_weeks(zUserName);
778
- }else if(0==fossil_strcmp(zView,"byuser")){
779
- stats_report_by_user();
780
- }else if(0==fossil_strcmp(zView,"byweekday")){
781
- stats_report_day_of_week();
782
- }else if(0==fossil_strcmp(zView,"byfile")){
783
- stats_report_by_file();
784
- }else{
785
- @ <h1>Activity Reports:</h1>
786
- @ <ul>
787
- @ <li>%z(href("?view=byyear"))Events by year</a></li>
788
- @ <li>%z(href("?view=bymonth"))Events by month</a></li>
789
- @ <li>%z(href("?view=byweek"))Events by calendar week</a></li>
790
- @ <li>%z(href("?view=byweekday"))Events by day of the week</a></li>
791
- @ <li>%z(href("?view=byuser"))Events by user</a></li>
792
- @ <li>%z(href("?view=byfile"))Events by file</a></li>
793
- @ </ul>
794
- }
795
-
758
+ switch( eType ){
759
+ case RPT_BYYEAR:
760
+ stats_report_by_month_year(0, 0, zUserName);
761
+ break;
762
+ case RPT_BYMONTH:
763
+ stats_report_by_month_year(1, 0, zUserName);
764
+ break;
765
+ case RPT_BYWEEK:
766
+ stats_report_year_weeks(zUserName);
767
+ break;
768
+ default:
769
+ case RPT_BYUSER:
770
+ stats_report_by_user();
771
+ break;
772
+ case RPT_BYWEEKDAY:
773
+ stats_report_day_of_week(zUserName);
774
+ break;
775
+ case RPT_BYFILE:
776
+ stats_report_by_file(zUserName);
777
+ break;
778
+ }
796779
style_footer();
797780
}
798781
--- src/statrep.c
+++ src/statrep.c
@@ -141,63 +141,10 @@
141 default:
142 return "all types";
143 }
144 }
145
146 /*
147 ** A helper for the /reports family of pages which prints out a menu
148 ** of links for the various type=XXX flags. zCurrentViewName must be
149 ** the name/value of the 'view' parameter which is in effect at the
150 ** time this is called. e.g. if called from the 'byuser' view then
151 ** zCurrentViewName must be "byuser". Any URL parameters which need to
152 ** be added to the generated URLs should be passed in zParam. The
153 ** caller is expected to have already encoded any zParam in the %T or
154 ** %t encoding. */
155 static void stats_report_event_types_menu(const char *zCurrentViewName,
156 const char *zParam){
157 char *zTop;
158 if(zParam && !*zParam){
159 zParam = NULL;
160 }
161 zTop = mprintf("%s/reports?view=%s%s%s", g.zTop, zCurrentViewName,
162 zParam ? "&" : "", zParam);
163 cgi_printf("<div>");
164 cgi_printf("<span>Types:</span> ");
165 if('*' == statsReportType){
166 cgi_printf(" <strong>all</strong>", zTop);
167 }else{
168 cgi_printf(" <a href='%s'>all</a>", zTop);
169 }
170 if('c' == statsReportType){
171 cgi_printf(" <strong>check-ins</strong>", zTop);
172 }else{
173 cgi_printf(" <a href='%s&type=ci'>check-ins</a>", zTop);
174 }
175 if('e' == statsReportType){
176 cgi_printf(" <strong>technotes</strong>", zTop);
177 }else{
178 cgi_printf(" <a href='%s&type=e'>technotes</a>", zTop);
179 }
180 if( 't' == statsReportType ){
181 cgi_printf(" <strong>tickets</strong>", zTop);
182 }else{
183 cgi_printf(" <a href='%s&type=t'>tickets</a>", zTop);
184 }
185 if( 'g' == statsReportType ){
186 cgi_printf(" <strong>tags</strong>", zTop);
187 }else{
188 cgi_printf(" <a href='%s&type=g'>tags</a>", zTop);
189 }
190 if( 'w' == statsReportType ){
191 cgi_printf(" <strong>wiki</strong>", zTop);
192 }else{
193 cgi_printf(" <a href='%s&type=w'>wiki</a>", zTop);
194 }
195 fossil_free(zTop);
196 cgi_printf("</div>");
197 }
198
199
200 /*
201 ** Helper for stats_report_by_month_year(), which generates a list of
202 ** week numbers. zTimeframe should be either a timeframe in the form YYYY
203 ** or YYYY-MM.
@@ -227,23 +174,21 @@
227 }
228
229 /*
230 ** Implements the "byyear" and "bymonth" reports for /reports.
231 ** If includeMonth is true then it generates the "bymonth" report,
232 ** else the "byyear" report. If zUserName is not NULL and not empty
233 ** then the report is restricted to events created by the named user
234 ** account.
235 */
236 static void stats_report_by_month_year(char includeMonth,
237 char includeWeeks,
238 const char *zUserName){
239 Stmt query = empty_Stmt;
240 int nRowNumber = 0; /* current TR number */
241 int nEventTotal = 0; /* Total event count */
242 int rowClass = 0; /* counter for alternating
243 row colors */
244 Blob sql = empty_blob; /* SQL */
245 const char *zTimeLabel = includeMonth ? "Year/Month" : "Year";
246 char zPrevYear[5] = {0}; /* For keeping track of when
247 we change years while looping */
248 int nEventsPerYear = 0; /* Total event count for the
249 current year */
@@ -252,39 +197,37 @@
252 Blob header = empty_blob; /* Page header text */
253 int nMaxEvents = 1; /* for calculating length of graph
254 bars. */
255 int iterations = 0; /* number of weeks/months we iterate
256 over */
 
257 stats_report_init_view();
258 stats_report_event_types_menu( includeMonth ? "bymonth" : "byyear", NULL );
259 blob_appendf(&header, "Timeline Events (%s) by year%s",
260 stats_report_label_for_type(),
261 (includeMonth ? "/month" : ""));
262 blob_append_sql(&sql,
263 "SELECT substr(date(mtime),1,%d) AS timeframe, "
264 "count(*) AS eventCount "
265 "FROM v_reports ",
266 includeMonth ? 7 : 4);
267 if(zUserName&&*zUserName){
268 blob_append_sql(&sql, " WHERE user=%Q ", zUserName);
269 blob_appendf(&header," for user %q", zUserName);
270 }
271 blob_append(&sql,
272 " GROUP BY timeframe"
273 " ORDER BY timeframe DESC",
274 -1);
275 db_prepare(&query, "%s", blob_sql_text(&sql));
276 blob_reset(&sql);
277 @ <h1>%b(&header)</h1>
278 @ <table class='statistics-report-table-events' border='0' cellpadding='2'
279 @ cellspacing='0' id='statsTable'>
280 @ <thead>
281 @ <th>%s(zTimeLabel)</th>
282 @ <th>Events</th>
283 @ <th width='90%%'><!-- relative commits graph --></th>
284 @ </thead><tbody>
285 blob_reset(&header);
286 /*
287 Run the query twice. The first time we calculate the maximum
288 number of events for a given row. Maybe someone with better SQL
289 Fu can re-implement this with a single query.
290 */
@@ -335,18 +278,18 @@
335 zTimeframe, nCount,
336 statsReportTimelineYFlag );
337 /* Reminder: n=nCount is not actually correct for bymonth unless
338 that was the only user who caused events.
339 */
340 if( zUserName && *zUserName ){
341 cgi_printf("&u=%t", zUserName);
342 }
343 cgi_printf("' target='_new'>%s</a>",zTimeframe);
344 }else {
345 cgi_printf("<a href='?view=byweek&y=%s&type=%c",
346 zTimeframe, (char)statsReportType);
347 if(zUserName && *zUserName){
348 cgi_printf("&u=%t", zUserName);
349 }
350 cgi_printf("'>%s</a>", zTimeframe);
351 }
352 @ </td><td>%d(nCount)</td>
@@ -403,17 +346,16 @@
403 int rowClass = 0; /* counter for alternating
404 row colors */
405 int nMaxEvents = 1; /* max number of events for
406 all rows. */
407 stats_report_init_view();
408 stats_report_event_types_menu("byuser", NULL);
409 @ <h1>Timeline Events
410 @ (%s(stats_report_label_for_type())) by User</h1>
411 db_multi_exec(
412 "CREATE TEMP TABLE piechart(amt,label);"
413 "INSERT INTO piechart SELECT count(*), user FROM v_reports"
414 " GROUP BY user ORDER BY count(*) DESC;"
415 );
416 if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
417 @ <center><svg width=700 height=400>
418 piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
419 @ </svg></centre><hr/>
@@ -424,14 +366,14 @@
424 @ <th>User</th>
425 @ <th>Events</th>
426 @ <th width='90%%'><!-- relative commits graph --></th>
427 @ </tr></thead><tbody>
428 db_prepare(&query,
429 "SELECT user, "
430 "COUNT(*) AS eventCount "
431 "FROM v_reports "
432 "GROUP BY user ORDER BY eventCount DESC");
433 while( SQLITE_ROW == db_step(&query) ){
434 const int nCount = db_column_int(&query, 1);
435 if(nCount>nMaxEvents){
436 nMaxEvents = nCount;
437 }
@@ -438,20 +380,21 @@
438 }
439 db_reset(&query);
440 while( SQLITE_ROW == db_step(&query) ){
441 const char *zUser = db_column_text(&query, 0);
442 const int nCount = db_column_int(&query, 1);
 
443 int nSize = nCount
444 ? (int)(100 * nCount / nMaxEvents)
445 : 0;
446 if(!nCount) continue /* arguable! Possible? */;
447 else if(!nSize) nSize = 1;
448 rowClass = ++nRowNumber % 2;
449 nEventTotal += nCount;
450 @<tr class='row%d(rowClass)'>
451 @ <td>
452 @ <a href="?view=bymonth&user=%h(zUser)&type=%c((char)statsReportType)">%h(zUser)</a>
453 @ </td><td data-sortkey='%08x(-nCount)'>%d(nCount)</td>
454 @ <td>
455 @ <div class='statistics-report-graph-line'
456 @ style='width:%d(nSize)%%;'>&nbsp;</div>
457 @ </td>
@@ -465,30 +408,37 @@
465 db_finalize(&query);
466 output_table_sorting_javascript("statsTable","tkx",2);
467 }
468
469 /*
470 ** Implements the "byfile" view for /reports.
 
471 */
472 static void stats_report_by_file(){
473 Stmt query;
474 int mxEvent = 1; /* max number of events across all rows */
475 int nRowNumber = 0;
476
477 db_multi_exec(
478 "CREATE TEMP TABLE statrep(filename, cnt);"
479 "INSERT INTO statrep(filename, cnt)"
480 " SELECT filename.name, count(distinct mlink.mid)"
481 " FROM filename, mlink"
482 " WHERE filename.fnid=mlink.fnid"
483 " GROUP BY 1;"
 
 
484 );
485 db_prepare(&query,
486 "SELECT filename, cnt FROM statrep ORDER BY cnt DESC, filename /*sort*/"
487 );
488 mxEvent = db_int(1, "SELECT max(cnt) FROM statrep");
489 @ <h1>Check-ins Per File</h1>
 
 
 
 
490 @ <table class='statistics-report-table-events' border='0'
491 @ cellpadding='2' cellspacing='0' id='statsTable'>
492 @ <thead><tr>
493 @ <th>File</th>
494 @ <th>Check-ins</th>
@@ -514,41 +464,56 @@
514 db_finalize(&query);
515 output_table_sorting_javascript("statsTable","tNx",2);
516 }
517
518 /*
519 ** Implements the "byweekday" view for /reports.
 
520 */
521 static void stats_report_day_of_week(){
522 Stmt query = empty_Stmt;
523 int nRowNumber = 0; /* current TR number */
524 int nEventTotal = 0; /* Total event count */
525 int rowClass = 0; /* counter for alternating
526 row colors */
527 int nMaxEvents = 1; /* max number of events for
528 all rows. */
 
529 static const char *const daysOfWeek[] = {
530 "Monday", "Tuesday", "Wednesday", "Thursday",
531 "Friday", "Saturday", "Sunday"
532 };
533
534 stats_report_init_view();
535 stats_report_event_types_menu("byweekday", NULL);
 
 
536 db_prepare(&query,
537 "SELECT cast(mtime %% 7 AS INTEGER) dow, "
538 "COUNT(*) AS eventCount "
539 "FROM v_reports "
540 "GROUP BY dow ORDER BY dow");
541 @ <h1>Timeline Events
542 @ (%s(stats_report_label_for_type())) by Day of the Week</h1>
 
 
 
 
543 db_multi_exec(
544 "CREATE TEMP TABLE piechart(amt,label);"
545 "INSERT INTO piechart SELECT count(*), cast(mtime %% 7 AS INT) FROM v_reports"
546 " GROUP BY 2 ORDER BY 2;"
547 "UPDATE piechart SET label = CASE label WHEN 0 THEN 'Monday' WHEN 1 THEN 'Tuesday'"
548 " WHEN 2 THEN 'Wednesday' WHEN 3 THEN 'Thursday' WHEN 4 THEN 'Friday'"
549 " WHEN 5 THEN 'Saturday' ELSE 'Sunday' END;"
 
 
 
 
 
 
 
550 );
551 if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
552 @ <center><svg width=700 height=400>
553 piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
554 @ </svg></centre><hr/>
@@ -595,139 +560,114 @@
595
596
597 /*
598 ** Helper for stats_report_by_month_year(), which generates a list of
599 ** week numbers. zTimeframe should be either a timeframe in the form YYYY
600 ** or YYYY-MM.
 
601 */
602 static void stats_report_year_weeks(const char *zUserName){
603 const char *zYear = P("y");
604 int nYear = zYear ? strlen(zYear) : 0;
605 int i = 0;
606 Stmt qYears = empty_Stmt;
607 char *zDefaultYear = NULL;
608 Blob sql = empty_blob;
609 int nMaxEvents = 1; /* max number of events for
610 all rows. */
611 int iterations = 0; /* # of active time periods. */
612 stats_report_init_view();
613 if(4==nYear){
614 Blob urlParams = empty_blob;
615 blob_appendf(&urlParams, "y=%T", zYear);
616 stats_report_event_types_menu("byweek", blob_str(&urlParams));
617 blob_reset(&urlParams);
618 }else{
619 stats_report_event_types_menu("byweek", NULL);
620 }
621 blob_append(&sql,
622 "SELECT DISTINCT substr(date(mtime),1,4) AS y "
623 "FROM v_reports WHERE 1 ", -1);
624 if(zUserName&&*zUserName){
625 blob_append_sql(&sql,"AND user=%Q ", zUserName);
626 }
627 blob_append(&sql,"GROUP BY y ORDER BY y", -1);
628 db_prepare(&qYears, "%s", blob_sql_text(&sql));
629 blob_reset(&sql);
630 cgi_printf("Select year: ");
631 while( SQLITE_ROW == db_step(&qYears) ){
632 const char *zT = db_column_text(&qYears, 0);
633 if( i++ ){
634 cgi_printf(" ");
635 }
636 cgi_printf("<a href='?view=byweek&y=%s&type=%c", zT,
637 (char)statsReportType);
638 if(zUserName && *zUserName){
639 cgi_printf("&user=%t",zUserName);
640 }
641 cgi_printf("'>%s</a>",zT);
642 }
643 db_finalize(&qYears);
644 cgi_printf("<br/>");
645 if(!zYear || !*zYear){
646 zDefaultYear = db_text("????", "SELECT strftime('%%Y')");
647 zYear = zDefaultYear;
648 nYear = 4;
649 }
650 if(4 == nYear){
651 Stmt stWeek = empty_Stmt;
652 int rowCount = 0;
653 int total = 0;
654 Blob header = empty_blob;
655 blob_appendf(&header, "Timeline events (%s) for the calendar weeks "
656 "of %h", stats_report_label_for_type(),
657 zYear);
658 blob_append_sql(&sql,
659 "SELECT DISTINCT strftime('%%W',mtime) AS wk, "
660 "count(*) AS n "
661 "FROM v_reports "
662 "WHERE %Q=substr(date(mtime),1,4) "
663 "AND mtime < current_timestamp ",
664 zYear);
665 if(zUserName&&*zUserName){
666 blob_append_sql(&sql, " AND user=%Q ", zUserName);
667 blob_appendf(&header," for user %h", zUserName);
668 }
669 blob_append_sql(&sql, "GROUP BY wk ORDER BY wk DESC");
670 cgi_printf("<h1>%h</h1>", blob_str(&header));
671 blob_reset(&header);
672 cgi_printf("<table class='statistics-report-table-events' "
673 "border='0' cellpadding='2' width='100%%' "
674 "cellspacing='0' id='statsTable'>");
675 cgi_printf("<thead><tr>"
676 "<th>Week</th>"
677 "<th>Events</th>"
678 "<th width='90%%'><!-- relative commits graph --></th>"
679 "</tr></thead>"
680 "<tbody>");
681 db_prepare(&stWeek, "%s", blob_sql_text(&sql));
682 blob_reset(&sql);
683 while( SQLITE_ROW == db_step(&stWeek) ){
684 const int nCount = db_column_int(&stWeek, 1);
685 if(nCount>nMaxEvents){
686 nMaxEvents = nCount;
687 }
688 ++iterations;
689 }
690 db_reset(&stWeek);
691 while( SQLITE_ROW == db_step(&stWeek) ){
692 const char *zWeek = db_column_text(&stWeek,0);
693 const int nCount = db_column_int(&stWeek,1);
694 int nSize = nCount
695 ? (int)(100 * nCount / nMaxEvents)
696 : 0;
697 if(!nSize) nSize = 1;
698 total += nCount;
699 cgi_printf("<tr class='row%d'>", ++rowCount % 2 );
700 cgi_printf("<td><a href='%R/timeline?yw=%t-%s&n=%d&y=%s",
701 zYear, zWeek, nCount,
702 statsReportTimelineYFlag);
703 if(zUserName && *zUserName){
704 cgi_printf("&u=%t",zUserName);
705 }
706 cgi_printf("'>%s</a></td>",zWeek);
707
708 cgi_printf("<td>%d</td>",nCount);
709 cgi_printf("<td>");
710 if(nCount){
711 cgi_printf("<div class='statistics-report-graph-line'"
712 "style='width:%d%%;'>&nbsp;</div>",
713 nSize);
714 }
715 cgi_printf("</td></tr>");
716 }
717 db_finalize(&stWeek);
718 free(zDefaultYear);
719 cgi_printf("</tbody></table>");
720 if(total){
721 int nAvg = iterations ? (total/iterations) : 0;
722 cgi_printf("<br><div>Total events: %d<br>"
723 "Average per active week: %d</div>",
724 total, nAvg);
725 }
726 output_table_sorting_javascript("statsTable","tnx",-1);
727 }
728 }
729
730 /*
731 ** WEBPAGE: reports
732 **
733 ** Shows activity reports for the repository.
@@ -747,51 +687,94 @@
747 ** y=YYYY The year to report (default is the server's
748 ** current year).
749 */
750 void stats_report_page(){
751 HQuery url; /* URL for various branch links */
752 const char *zView = P("view"); /* Which view/report to show. */
753 const char *zUserName = P("user");
754
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755 login_check_credentials();
756 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
757 if(!zUserName) zUserName = P("u");
758 url_initialize(&url, "reports");
759 if(zUserName && *zUserName){
760 url_add_parameter(&url,"user", zUserName);
761 statrep_submenu(&url, "(Remove User Flag)", "view", zView, "user");
762 }
763 statrep_submenu(&url, "By Year", "view", "byyear", 0);
764 statrep_submenu(&url, "By Month", "view", "bymonth", 0);
765 statrep_submenu(&url, "By Week", "view", "byweek", 0);
766 statrep_submenu(&url, "By Weekday", "view", "byweekday", 0);
767 statrep_submenu(&url, "By User", "view", "byuser", "user");
768 statrep_submenu(&url, "By File", "view", "byfile", "file");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769 style_submenu_element("Stats", "Stats", "%R/stat");
770 url_reset(&url);
771 style_header("Activity Reports");
772 if(0==fossil_strcmp(zView,"byyear")){
773 stats_report_by_month_year(0, 0, zUserName);
774 }else if(0==fossil_strcmp(zView,"bymonth")){
775 stats_report_by_month_year(1, 0, zUserName);
776 }else if(0==fossil_strcmp(zView,"byweek")){
777 stats_report_year_weeks(zUserName);
778 }else if(0==fossil_strcmp(zView,"byuser")){
779 stats_report_by_user();
780 }else if(0==fossil_strcmp(zView,"byweekday")){
781 stats_report_day_of_week();
782 }else if(0==fossil_strcmp(zView,"byfile")){
783 stats_report_by_file();
784 }else{
785 @ <h1>Activity Reports:</h1>
786 @ <ul>
787 @ <li>%z(href("?view=byyear"))Events by year</a></li>
788 @ <li>%z(href("?view=bymonth"))Events by month</a></li>
789 @ <li>%z(href("?view=byweek"))Events by calendar week</a></li>
790 @ <li>%z(href("?view=byweekday"))Events by day of the week</a></li>
791 @ <li>%z(href("?view=byuser"))Events by user</a></li>
792 @ <li>%z(href("?view=byfile"))Events by file</a></li>
793 @ </ul>
794 }
795
796 style_footer();
797 }
798
--- src/statrep.c
+++ src/statrep.c
@@ -141,63 +141,10 @@
141 default:
142 return "all types";
143 }
144 }
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
147 /*
148 ** Helper for stats_report_by_month_year(), which generates a list of
149 ** week numbers. zTimeframe should be either a timeframe in the form YYYY
150 ** or YYYY-MM.
@@ -227,23 +174,21 @@
174 }
175
176 /*
177 ** Implements the "byyear" and "bymonth" reports for /reports.
178 ** If includeMonth is true then it generates the "bymonth" report,
179 ** else the "byyear" report. If zUserName is not NULL then the report is
180 ** restricted to events created by the named user account.
 
181 */
182 static void stats_report_by_month_year(char includeMonth,
183 char includeWeeks,
184 const char *zUserName){
185 Stmt query = empty_Stmt;
186 int nRowNumber = 0; /* current TR number */
187 int nEventTotal = 0; /* Total event count */
188 int rowClass = 0; /* counter for alternating
189 row colors */
 
190 const char *zTimeLabel = includeMonth ? "Year/Month" : "Year";
191 char zPrevYear[5] = {0}; /* For keeping track of when
192 we change years while looping */
193 int nEventsPerYear = 0; /* Total event count for the
194 current year */
@@ -252,39 +197,37 @@
197 Blob header = empty_blob; /* Page header text */
198 int nMaxEvents = 1; /* for calculating length of graph
199 bars. */
200 int iterations = 0; /* number of weeks/months we iterate
201 over */
202 Blob userFilter = empty_blob; /* Optional user=johndoe query string */
203 stats_report_init_view();
204 if( zUserName ){
205 blob_appendf(&userFilter, "user=%s", zUserName);
206 }
207 blob_reset(&userFilter);
208 db_prepare(&query,
209 "SELECT substr(date(mtime),1,%d) AS timeframe,"
210 " count(*) AS eventCount"
211 " FROM v_reports"
212 " WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
213 " GROUP BY timeframe"
214 " ORDER BY timeframe DESC",
215 includeMonth ? 7 : 4, zUserName);
216 @ <h1>Timeline Events (%s(stats_report_label_for_type()))
217 @ by year%s(includeMonth ? "/month" : "")
218 if( zUserName ){
219 @ for user %h(zUserName)
220 }
221 @ </h1>
 
 
222 @ <table class='statistics-report-table-events' border='0' cellpadding='2'
223 @ cellspacing='0' id='statsTable'>
224 @ <thead>
225 @ <th>%s(zTimeLabel)</th>
226 @ <th>Events</th>
227 @ <th width='90%%'><!-- relative commits graph --></th>
228 @ </thead><tbody>
 
229 /*
230 Run the query twice. The first time we calculate the maximum
231 number of events for a given row. Maybe someone with better SQL
232 Fu can re-implement this with a single query.
233 */
@@ -335,18 +278,18 @@
278 zTimeframe, nCount,
279 statsReportTimelineYFlag );
280 /* Reminder: n=nCount is not actually correct for bymonth unless
281 that was the only user who caused events.
282 */
283 if( zUserName ){
284 cgi_printf("&u=%t", zUserName);
285 }
286 cgi_printf("' target='_new'>%s</a>",zTimeframe);
287 }else {
288 cgi_printf("<a href='?view=byweek&y=%s&type=%c",
289 zTimeframe, (char)statsReportType);
290 if( zUserName ){
291 cgi_printf("&u=%t", zUserName);
292 }
293 cgi_printf("'>%s</a>", zTimeframe);
294 }
295 @ </td><td>%d(nCount)</td>
@@ -403,17 +346,16 @@
346 int rowClass = 0; /* counter for alternating
347 row colors */
348 int nMaxEvents = 1; /* max number of events for
349 all rows. */
350 stats_report_init_view();
 
351 @ <h1>Timeline Events
352 @ (%s(stats_report_label_for_type())) by User</h1>
353 db_multi_exec(
354 "CREATE TEMP TABLE piechart(amt,label);"
355 "INSERT INTO piechart SELECT count(*), ifnull(euser,user) FROM v_reports"
356 " GROUP BY ifnull(euser,user) ORDER BY count(*) DESC;"
357 );
358 if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
359 @ <center><svg width=700 height=400>
360 piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
361 @ </svg></centre><hr/>
@@ -424,14 +366,14 @@
366 @ <th>User</th>
367 @ <th>Events</th>
368 @ <th width='90%%'><!-- relative commits graph --></th>
369 @ </tr></thead><tbody>
370 db_prepare(&query,
371 "SELECT ifnull(euser,user), "
372 "COUNT(*) AS eventCount "
373 "FROM v_reports "
374 "GROUP BY ifnull(euser,user) ORDER BY eventCount DESC");
375 while( SQLITE_ROW == db_step(&query) ){
376 const int nCount = db_column_int(&query, 1);
377 if(nCount>nMaxEvents){
378 nMaxEvents = nCount;
379 }
@@ -438,20 +380,21 @@
380 }
381 db_reset(&query);
382 while( SQLITE_ROW == db_step(&query) ){
383 const char *zUser = db_column_text(&query, 0);
384 const int nCount = db_column_int(&query, 1);
385 char y = (char)statsReportType;
386 int nSize = nCount
387 ? (int)(100 * nCount / nMaxEvents)
388 : 0;
389 if(!nCount) continue /* arguable! Possible? */;
390 else if(!nSize) nSize = 1;
391 rowClass = ++nRowNumber % 2;
392 nEventTotal += nCount;
393 @ <tr class='row%d(rowClass)'>
394 @ <td>
395 @ <a href="?view=bymonth&user=%h(zUser)&type=%c(y)">%h(zUser)</a>
396 @ </td><td data-sortkey='%08x(-nCount)'>%d(nCount)</td>
397 @ <td>
398 @ <div class='statistics-report-graph-line'
399 @ style='width:%d(nSize)%%;'>&nbsp;</div>
400 @ </td>
@@ -465,30 +408,37 @@
408 db_finalize(&query);
409 output_table_sorting_javascript("statsTable","tkx",2);
410 }
411
412 /*
413 ** Implements the "byfile" view for /reports. If zUserName is not NULL then the
414 ** report is restricted to events created by the named user account.
415 */
416 static void stats_report_by_file(const char *zUserName){
417 Stmt query;
418 int mxEvent = 1; /* max number of events across all rows */
419 int nRowNumber = 0;
420
421 db_multi_exec(
422 "CREATE TEMP TABLE statrep(filename, cnt);"
423 "INSERT INTO statrep(filename, cnt)"
424 " SELECT filename.name, count(distinct mlink.mid)"
425 " FROM filename, mlink, event"
426 " WHERE filename.fnid=mlink.fnid"
427 " AND mlink.mid=event.objid"
428 " AND ifnull(coalesce(euser,user,'')=%Q,1)"
429 " GROUP BY 1", zUserName
430 );
431 db_prepare(&query,
432 "SELECT filename, cnt FROM statrep ORDER BY cnt DESC, filename /*sort*/"
433 );
434 mxEvent = db_int(1, "SELECT max(cnt) FROM statrep");
435 @ <h1>Check-ins Per File
436 if( zUserName ){
437 @ for user %h(zUserName)
438 }
439 @ </h1>
440 @ <table class='statistics-report-table-events' border='0'
441 @ cellpadding='2' cellspacing='0' id='statsTable'>
442 @ <thead><tr>
443 @ <th>File</th>
444 @ <th>Check-ins</th>
@@ -514,41 +464,56 @@
464 db_finalize(&query);
465 output_table_sorting_javascript("statsTable","tNx",2);
466 }
467
468 /*
469 ** Implements the "byweekday" view for /reports. If zUserName is not NULL then
470 ** the report is restricted to events created by the named user account.
471 */
472 static void stats_report_day_of_week(const char *zUserName){
473 Stmt query = empty_Stmt;
474 int nRowNumber = 0; /* current TR number */
475 int nEventTotal = 0; /* Total event count */
476 int rowClass = 0; /* counter for alternating
477 row colors */
478 int nMaxEvents = 1; /* max number of events for
479 all rows. */
480 Blob userFilter = empty_blob; /* Optional user=johndoe query string */
481 static const char *const daysOfWeek[] = {
482 "Monday", "Tuesday", "Wednesday", "Thursday",
483 "Friday", "Saturday", "Sunday"
484 };
485
486 stats_report_init_view();
487 if( zUserName ){
488 blob_appendf(&userFilter, "user=%s", zUserName);
489 }
490 db_prepare(&query,
491 "SELECT cast(mtime %% 7 AS INTEGER) dow,"
492 " COUNT(*) AS eventCount"
493 " FROM v_reports"
494 " WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
495 " GROUP BY dow ORDER BY dow", zUserName);
496 @ <h1>Timeline Events (%h(stats_report_label_for_type())) by Day of the Week
497 if( zUserName ){
498 @ for user %h(zUserName)
499 }
500 @ </h1>
501 db_multi_exec(
502 "CREATE TEMP TABLE piechart(amt,label);"
503 "INSERT INTO piechart"
504 " SELECT count(*), cast(mtime %% 7 AS INT) FROM v_reports"
505 " WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
506 " GROUP BY 2 ORDER BY 2;"
507 "UPDATE piechart SET label = CASE label"
508 " WHEN 0 THEN 'Monday'"
509 " WHEN 1 THEN 'Tuesday'"
510 " WHEN 2 THEN 'Wednesday'"
511 " WHEN 3 THEN 'Thursday'"
512 " WHEN 4 THEN 'Friday'"
513 " WHEN 5 THEN 'Saturday'"
514 " ELSE 'Sunday' END;", zUserName
515 );
516 if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
517 @ <center><svg width=700 height=400>
518 piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
519 @ </svg></centre><hr/>
@@ -595,139 +560,114 @@
560
561
562 /*
563 ** Helper for stats_report_by_month_year(), which generates a list of
564 ** week numbers. zTimeframe should be either a timeframe in the form YYYY
565 ** or YYYY-MM. If zUserName is not NULL then the report is restricted to events
566 ** created by the named user account.
567 */
568 static void stats_report_year_weeks(const char *zUserName){
569 const char *zYear = P("y"); /* Year for which report shown */
570 int isValidYear = 0; /* True if a valid year */
571 int i = 0;
572 Stmt q;
 
 
573 int nMaxEvents = 1; /* max number of events for
574 all rows. */
575 int iterations = 0; /* # of active time periods. */
576 int n = 0; /* Number of entries in azYear */
577 char **azYear = 0; /* Year dropdown menu */
578 int rowCount = 0;
579 int total = 0;
580
581 stats_report_init_view();
582 style_submenu_sql("y", "Year:",
583 "WITH RECURSIVE a(b) AS ("
584 " SELECT substr(date('now'),1,4) UNION ALL"
585 " SELECT b-1 FROM a"
586 " WHERE b>0+(SELECT substr(date(min(mtime)),1,4) FROM event)"
587 ") SELECT b, b FROM a ORDER BY b DESC");
588 if( zYear==0 || strlen(zYear)!=4 ){
589 zYear = db_text("1970","SELECT substr(date('now'),1,4);");
590 }
591 cgi_printf("<br/>");
592 db_prepare(&q,
593 "SELECT DISTINCT strftime('%%W',mtime) AS wk, "
594 " count(*) AS n "
595 " FROM v_reports "
596 " WHERE %Q=substr(date(mtime),1,4) "
597 " AND mtime < current_timestamp "
598 " AND ifnull(coalesce(euser,user,'')=%Q,1)"
599 " GROUP BY wk ORDER BY wk DESC", zYear, zUserName);
600 @ <h1>Timeline events (%h(stats_report_label_for_type()))
601 @ for the calendar weeks of %h(zYear)
602 if( zUserName ){
603 @ for user %h(zUserName)
604 }
605 @ </h1>
606 cgi_printf("<table class='statistics-report-table-events' "
607 "border='0' cellpadding='2' width='100%%' "
608 "cellspacing='0' id='statsTable'>");
609 cgi_printf("<thead><tr>"
610 "<th>Week</th>"
611 "<th>Events</th>"
612 "<th width='90%%'><!-- relative commits graph --></th>"
613 "</tr></thead>"
614 "<tbody>");
615 while( SQLITE_ROW == db_step(&q) ){
616 const int nCount = db_column_int(&q, 1);
617 if(nCount>nMaxEvents){
618 nMaxEvents = nCount;
619 }
620 ++iterations;
621 }
622 db_reset(&q);
623 while( SQLITE_ROW == db_step(&q) ){
624 const char *zWeek = db_column_text(&q,0);
625 const int nCount = db_column_int(&q,1);
626 int nSize = nCount
627 ? (int)(100 * nCount / nMaxEvents)
628 : 0;
629 if(!nSize) nSize = 1;
630 total += nCount;
631 cgi_printf("<tr class='row%d'>", ++rowCount % 2 );
632 cgi_printf("<td><a href='%R/timeline?yw=%t-%s&n=%d&y=%s",
633 zYear, zWeek, nCount,
634 statsReportTimelineYFlag);
635 if( zUserName ){
636 cgi_printf("&u=%t",zUserName);
637 }
638 cgi_printf("'>%s</a></td>",zWeek);
639
640 cgi_printf("<td>%d</td>",nCount);
641 cgi_printf("<td>");
642 if(nCount){
643 cgi_printf("<div class='statistics-report-graph-line'"
644 "style='width:%d%%;'>&nbsp;</div>",
645 nSize);
646 }
647 cgi_printf("</td></tr>");
648 }
649 db_finalize(&q);
650 cgi_printf("</tbody></table>");
651 if(total){
652 int nAvg = iterations ? (total/iterations) : 0;
653 cgi_printf("<br><div>Total events: %d<br>"
654 "Average per active week: %d</div>",
655 total, nAvg);
656 }
657 output_table_sorting_javascript("statsTable","tnx",-1);
658 }
659
660 /* Report types
661 */
662 #define RPT_BYFILE 1
663 #define RPT_BYMONTH 2
664 #define RPT_BYUSER 3
665 #define RPT_BYWEEK 4
666 #define RPT_BYWEEKDAY 5
667 #define RPT_BYYEAR 6
668 #define RPT_NONE 0 /* None of the above */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
670 /*
671 ** WEBPAGE: reports
672 **
673 ** Shows activity reports for the repository.
@@ -747,51 +687,94 @@
687 ** y=YYYY The year to report (default is the server's
688 ** current year).
689 */
690 void stats_report_page(){
691 HQuery url; /* URL for various branch links */
692 const char *zView = P("view"); /* Which view/report to show. */
693 int eType = RPT_NONE; /* Numeric code for view/report to show */
694 int i; /* Loop counter */
695 const char *zUserName; /* Name of user */
696 const struct {
697 const char *zName; /* Name of view= screen type */
698 const char *zVal; /* Value of view= query parameter */
699 int eType; /* Corresponding RPT_* define */
700 } aViewType[] = {
701 { "File Changes","byfile", RPT_BYFILE },
702 { "By Month", "bymonth", RPT_BYMONTH },
703 { "By User", "byuser", RPT_BYUSER },
704 { "By Week", "byweek", RPT_BYWEEK },
705 { "By Weekday", "byweekday", RPT_BYWEEKDAY },
706 { "By Year", "byyear", RPT_BYYEAR },
707 };
708 const char *azType[] = {
709 "a", "All Changes",
710 "ci", "Check-ins",
711 "g", "Tags",
712 "e", "Tech Notes",
713 "t", "Tickets",
714 "w", "Wiki"
715 };
716
717 login_check_credentials();
718 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
719 zUserName = P("user");
720 if( zUserName==0 ) zUserName = P("u");
721 if( zUserName && zUserName[0]==0 ) zUserName = 0;
722 if( zView==0 ){
723 zView = "byuser";
724 cgi_replace_query_parameter("view","byuser");
725 }
726 for(i=0; i<ArraySize(aViewType); i++){
727 if( fossil_strcmp(zView, aViewType[i].zVal)==0 ){
728 eType = aViewType[i].eType;
729 break;
730 }
731 }
732 url_initialize(&url, "reports");
733 cgi_query_parameters_to_url(&url);
734 if( eType!=RPT_NONE ){
735 int nView = 0; /* Slots used in azView[] */
736 const char *azView[16]; /* Drop-down menu of view types */
737 for(i=0; i<ArraySize(aViewType); i++){
738 azView[nView++] = aViewType[i].zVal;
739 azView[nView++] = aViewType[i].zName;
740 }
741 if( eType!=RPT_BYFILE ){
742 style_submenu_multichoice("type", ArraySize(azType)/2, azType, 0);
743 }
744 style_submenu_multichoice("view", nView/2, azView, 0);
745 if( eType!=RPT_BYUSER ){
746 style_submenu_sql("u","User:",
747 "SELECT '', 'All Users' UNION ALL "
748 "SELECT x, x FROM ("
749 " SELECT DISTINCT trim(coalesce(euser,user)) AS x FROM event %s"
750 " ORDER BY 1 COLLATE nocase) WHERE x!=''",
751 eType==RPT_BYFILE ? "WHERE type='ci'" : ""
752 );
753 }
754 }
755 style_submenu_element("Stats", "Stats", "%R/stat");
756 url_reset(&url);
757 style_header("Activity Reports");
758 switch( eType ){
759 case RPT_BYYEAR:
760 stats_report_by_month_year(0, 0, zUserName);
761 break;
762 case RPT_BYMONTH:
763 stats_report_by_month_year(1, 0, zUserName);
764 break;
765 case RPT_BYWEEK:
766 stats_report_year_weeks(zUserName);
767 break;
768 default:
769 case RPT_BYUSER:
770 stats_report_by_user();
771 break;
772 case RPT_BYWEEKDAY:
773 stats_report_day_of_week(zUserName);
774 break;
775 case RPT_BYFILE:
776 stats_report_by_file(zUserName);
777 break;
778 }
 
 
 
779 style_footer();
780 }
781
+37
--- src/style.c
+++ src/style.c
@@ -285,11 +285,45 @@
285285
aSubmenuCtrl[nSubmenuCtrl].azChoice = azChoice;
286286
aSubmenuCtrl[nSubmenuCtrl].isDisabled = isDisabled;
287287
aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
288288
nSubmenuCtrl++;
289289
}
290
+void style_submenu_sql(
291
+ const char *zName, /* Query parameter name */
292
+ const char *zLabel, /* Label on the control */
293
+ const char *zFormat, /* Format string for SQL command for choices */
294
+ ... /* Arguments to the format string */
295
+){
296
+ Stmt q;
297
+ int n = 0;
298
+ int nAlloc = 0;
299
+ char **az = 0;
300
+ va_list ap;
290301
302
+ va_start(ap, zFormat);
303
+ db_vprepare(&q, 0, zFormat, ap);
304
+ va_end(ap);
305
+ while( SQLITE_ROW==db_step(&q) ){
306
+ if( n+2>=nAlloc ){
307
+ nAlloc += nAlloc + 20;
308
+ az = fossil_realloc(az, sizeof(char*)*nAlloc);
309
+ }
310
+ az[n++] = fossil_strdup(db_column_text(&q,0));
311
+ az[n++] = fossil_strdup(db_column_text(&q,1));
312
+ }
313
+ db_finalize(&q);
314
+ if( n>0 ){
315
+ aSubmenuCtrl[nSubmenuCtrl].zName = zName;
316
+ aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
317
+ aSubmenuCtrl[nSubmenuCtrl].iSize = n/2;
318
+ aSubmenuCtrl[nSubmenuCtrl].azChoice = (const char**)az;
319
+ aSubmenuCtrl[nSubmenuCtrl].isDisabled = 0;
320
+ aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
321
+ nSubmenuCtrl++;
322
+ }
323
+}
324
+
291325
292326
/*
293327
** Compare two submenu items for sorting purposes
294328
*/
295329
static int submenuCompare(const void *a, const void *b){
@@ -514,10 +548,13 @@
514548
break;
515549
}
516550
case FF_MULTI: {
517551
int j;
518552
const char *zVal = P(zQPN);
553
+ if( aSubmenuCtrl[i].zLabel ){
554
+ cgi_printf("&nbsp;%h", aSubmenuCtrl[i].zLabel);
555
+ }
519556
cgi_printf(
520557
"<select class='submenuctrl' size='1' name='%s'%s "
521558
"onchange='gebi(\"f01\").submit();'>\n",
522559
zQPN, zDisabled
523560
);
524561
--- src/style.c
+++ src/style.c
@@ -285,11 +285,45 @@
285 aSubmenuCtrl[nSubmenuCtrl].azChoice = azChoice;
286 aSubmenuCtrl[nSubmenuCtrl].isDisabled = isDisabled;
287 aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
288 nSubmenuCtrl++;
289 }
 
 
 
 
 
 
 
 
 
 
 
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
292 /*
293 ** Compare two submenu items for sorting purposes
294 */
295 static int submenuCompare(const void *a, const void *b){
@@ -514,10 +548,13 @@
514 break;
515 }
516 case FF_MULTI: {
517 int j;
518 const char *zVal = P(zQPN);
 
 
 
519 cgi_printf(
520 "<select class='submenuctrl' size='1' name='%s'%s "
521 "onchange='gebi(\"f01\").submit();'>\n",
522 zQPN, zDisabled
523 );
524
--- src/style.c
+++ src/style.c
@@ -285,11 +285,45 @@
285 aSubmenuCtrl[nSubmenuCtrl].azChoice = azChoice;
286 aSubmenuCtrl[nSubmenuCtrl].isDisabled = isDisabled;
287 aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
288 nSubmenuCtrl++;
289 }
290 void style_submenu_sql(
291 const char *zName, /* Query parameter name */
292 const char *zLabel, /* Label on the control */
293 const char *zFormat, /* Format string for SQL command for choices */
294 ... /* Arguments to the format string */
295 ){
296 Stmt q;
297 int n = 0;
298 int nAlloc = 0;
299 char **az = 0;
300 va_list ap;
301
302 va_start(ap, zFormat);
303 db_vprepare(&q, 0, zFormat, ap);
304 va_end(ap);
305 while( SQLITE_ROW==db_step(&q) ){
306 if( n+2>=nAlloc ){
307 nAlloc += nAlloc + 20;
308 az = fossil_realloc(az, sizeof(char*)*nAlloc);
309 }
310 az[n++] = fossil_strdup(db_column_text(&q,0));
311 az[n++] = fossil_strdup(db_column_text(&q,1));
312 }
313 db_finalize(&q);
314 if( n>0 ){
315 aSubmenuCtrl[nSubmenuCtrl].zName = zName;
316 aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
317 aSubmenuCtrl[nSubmenuCtrl].iSize = n/2;
318 aSubmenuCtrl[nSubmenuCtrl].azChoice = (const char**)az;
319 aSubmenuCtrl[nSubmenuCtrl].isDisabled = 0;
320 aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
321 nSubmenuCtrl++;
322 }
323 }
324
325
326 /*
327 ** Compare two submenu items for sorting purposes
328 */
329 static int submenuCompare(const void *a, const void *b){
@@ -514,10 +548,13 @@
548 break;
549 }
550 case FF_MULTI: {
551 int j;
552 const char *zVal = P(zQPN);
553 if( aSubmenuCtrl[i].zLabel ){
554 cgi_printf("&nbsp;%h", aSubmenuCtrl[i].zLabel);
555 }
556 cgi_printf(
557 "<select class='submenuctrl' size='1' name='%s'%s "
558 "onchange='gebi(\"f01\").submit();'>\n",
559 zQPN, zDisabled
560 );
561
--- www/changes.wiki
+++ www/changes.wiki
@@ -40,10 +40,13 @@
4040
symlink. Additionally show the UUID for files whose types have changed
4141
without changing contents or symlink target.
4242
* Have [/help?cmd=changes|fossil changes] and
4343
[/help?cmd=status|fossil status] report when executable or symlink status
4444
changes on otherwise unmodified files.
45
+ * Permit filtering weekday and file [/help?cmd=/reports|reports] by user.
46
+ Also ensure the user parameter is preserved when changing types. Add a
47
+ field for direct entry of the user name to each applicable report.
4548
* Create parent directories of [/help?cmd=settings|empty-dirs] if they don't
4649
already exist.
4750
* Inhibit timeline links to wiki pages that have been deleted.
4851
4952
<h2>Changes for Version 1.32 (2015-03-14)</h2>
5053
--- www/changes.wiki
+++ www/changes.wiki
@@ -40,10 +40,13 @@
40 symlink. Additionally show the UUID for files whose types have changed
41 without changing contents or symlink target.
42 * Have [/help?cmd=changes|fossil changes] and
43 [/help?cmd=status|fossil status] report when executable or symlink status
44 changes on otherwise unmodified files.
 
 
 
45 * Create parent directories of [/help?cmd=settings|empty-dirs] if they don't
46 already exist.
47 * Inhibit timeline links to wiki pages that have been deleted.
48
49 <h2>Changes for Version 1.32 (2015-03-14)</h2>
50
--- www/changes.wiki
+++ www/changes.wiki
@@ -40,10 +40,13 @@
40 symlink. Additionally show the UUID for files whose types have changed
41 without changing contents or symlink target.
42 * Have [/help?cmd=changes|fossil changes] and
43 [/help?cmd=status|fossil status] report when executable or symlink status
44 changes on otherwise unmodified files.
45 * Permit filtering weekday and file [/help?cmd=/reports|reports] by user.
46 Also ensure the user parameter is preserved when changing types. Add a
47 field for direct entry of the user name to each applicable report.
48 * Create parent directories of [/help?cmd=settings|empty-dirs] if they don't
49 already exist.
50 * Inhibit timeline links to wiki pages that have been deleted.
51
52 <h2>Changes for Version 1.32 (2015-03-14)</h2>
53

Keyboard Shortcuts

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