|
1
|
/* |
|
2
|
** Copyright (c) 2013 Stephan Beal |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
|
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code to implement the /reports web page. |
|
19
|
** |
|
20
|
*/ |
|
21
|
#include "config.h" |
|
22
|
#include <string.h> |
|
23
|
#include <time.h> |
|
24
|
#include "statrep.h" |
|
25
|
|
|
26
|
|
|
27
|
/* |
|
28
|
** Used by stats_report_xxxxx() to remember which type of events |
|
29
|
** to show. Populated by stats_report_init_view() and holds the |
|
30
|
** return value of that function. |
|
31
|
*/ |
|
32
|
static int statsReportType = 0; |
|
33
|
|
|
34
|
/* |
|
35
|
** Set by stats_report_init_view() to one of the y=XXXX values |
|
36
|
** accepted by /timeline?y=XXXX. |
|
37
|
*/ |
|
38
|
static const char *statsReportTimelineYFlag = NULL; |
|
39
|
|
|
40
|
|
|
41
|
/* |
|
42
|
** Creates a TEMP VIEW named v_reports which is a wrapper around the |
|
43
|
** EVENT table filtered on event.type. It looks for the request |
|
44
|
** parameter 'type' (reminder: we "should" use 'y' for consistency |
|
45
|
** with /timeline, but /reports uses 'y' for the year) and expects it |
|
46
|
** to contain one of the conventional values from event.type or the |
|
47
|
** value "all", which is treated as equivalent to "*". By default (if |
|
48
|
** no 'y' is specified), "*" is assumed (that is also the default for |
|
49
|
** invalid/unknown filter values). That 'y' filter is the one used for |
|
50
|
** the event list. Note that a filter of "*" or "all" is equivalent to |
|
51
|
** querying against the full event table. The view, however, adds an |
|
52
|
** abstraction level to simplify the implementation code for the |
|
53
|
** various /reports pages. |
|
54
|
** |
|
55
|
** Returns one of: 'c', 'f', 'w', 'g', 't', 'e', representing the type of |
|
56
|
** filter it applies, or '*' if no filter is applied (i.e. if "all" is |
|
57
|
** used). |
|
58
|
*/ |
|
59
|
static int stats_report_init_view(){ |
|
60
|
const char *zType = PD("type","*"); /* analog to /timeline?y=... */ |
|
61
|
const char *zRealType = NULL; /* normalized form of zType */ |
|
62
|
int rc = 0; /* result code */ |
|
63
|
char *zTimeSpan; /* Time span */ |
|
64
|
assert( !statsReportType && "Must not be called more than once." ); |
|
65
|
switch( (zType && *zType) ? *zType : 0 ){ |
|
66
|
case 'c': |
|
67
|
case 'C': |
|
68
|
zRealType = "ci"; |
|
69
|
rc = *zRealType; |
|
70
|
break; |
|
71
|
case 'e': |
|
72
|
case 'E': |
|
73
|
zRealType = "e"; |
|
74
|
rc = *zRealType; |
|
75
|
break; |
|
76
|
case 'f': |
|
77
|
case 'F': |
|
78
|
zRealType = "f"; |
|
79
|
rc = *zRealType; |
|
80
|
break; |
|
81
|
case 'g': |
|
82
|
case 'G': |
|
83
|
zRealType = "g"; |
|
84
|
rc = *zRealType; |
|
85
|
break; |
|
86
|
case 'm': |
|
87
|
case 'M': |
|
88
|
zRealType = "m"; |
|
89
|
rc = *zRealType; |
|
90
|
break; |
|
91
|
case 'n': |
|
92
|
case 'N': |
|
93
|
zRealType = "n"; |
|
94
|
rc = *zRealType; |
|
95
|
break; |
|
96
|
case 't': |
|
97
|
case 'T': |
|
98
|
zRealType = "t"; |
|
99
|
rc = *zRealType; |
|
100
|
break; |
|
101
|
case 'w': |
|
102
|
case 'W': |
|
103
|
zRealType = "w"; |
|
104
|
rc = *zRealType; |
|
105
|
break; |
|
106
|
default: |
|
107
|
rc = '*'; |
|
108
|
break; |
|
109
|
} |
|
110
|
assert(0 != rc); |
|
111
|
if( P("from")!=0 && P("to")!=0 ){ |
|
112
|
zTimeSpan = mprintf( |
|
113
|
" (event.mtime BETWEEN julianday(%Q) AND julianday(%Q))", |
|
114
|
P("from"), P("to")); |
|
115
|
}else{ |
|
116
|
zTimeSpan = " 1"; |
|
117
|
} |
|
118
|
if( zRealType==0 ){ |
|
119
|
statsReportTimelineYFlag = "a"; |
|
120
|
db_multi_exec("CREATE TEMP VIEW v_reports AS " |
|
121
|
"SELECT * FROM event WHERE %s", zTimeSpan/*safe-for-%s*/); |
|
122
|
}else if( rc!='n' && rc!='m' ){ |
|
123
|
statsReportTimelineYFlag = zRealType; |
|
124
|
db_multi_exec("CREATE TEMP VIEW v_reports AS " |
|
125
|
"SELECT * FROM event WHERE (type GLOB %Q) AND %s", |
|
126
|
zRealType, zTimeSpan/*safe-for-%s*/); |
|
127
|
}else{ |
|
128
|
const char *zNot = rc=='n' ? "NOT" : ""; |
|
129
|
statsReportTimelineYFlag = "ci"; |
|
130
|
db_multi_exec( |
|
131
|
"CREATE TEMP VIEW v_reports AS " |
|
132
|
"SELECT * FROM event WHERE type='ci' AND %s" |
|
133
|
" AND objid %s IN (SELECT cid FROM plink WHERE NOT isprim)", |
|
134
|
zTimeSpan/*safe-for-%s*/, zNot/*safe-for-%s*/ |
|
135
|
); |
|
136
|
} |
|
137
|
return statsReportType = rc; |
|
138
|
} |
|
139
|
|
|
140
|
/* |
|
141
|
** Returns a string suitable (for a given value of suitable) for |
|
142
|
** use in a label with the header of the /reports pages, dependent |
|
143
|
** on the 'type' flag. See stats_report_init_view(). |
|
144
|
** The returned bytes are static. |
|
145
|
*/ |
|
146
|
static const char *stats_report_label_for_type(){ |
|
147
|
assert( statsReportType && "Must call stats_report_init_view() first." ); |
|
148
|
switch( statsReportType ){ |
|
149
|
case 'c': |
|
150
|
return "check-ins"; |
|
151
|
case 'm': |
|
152
|
return "merge check-ins"; |
|
153
|
case 'n': |
|
154
|
return "non-merge check-ins"; |
|
155
|
case 'e': |
|
156
|
return "technotes"; |
|
157
|
case 'f': |
|
158
|
return "forum posts"; |
|
159
|
case 'w': |
|
160
|
return "wiki changes"; |
|
161
|
case 't': |
|
162
|
return "ticket changes"; |
|
163
|
case 'g': |
|
164
|
return "tag changes"; |
|
165
|
default: |
|
166
|
return "all types"; |
|
167
|
} |
|
168
|
} |
|
169
|
|
|
170
|
|
|
171
|
/* |
|
172
|
** Implements the "byyear" and "bymonth" reports for /reports. |
|
173
|
** If includeMonth is true then it generates the "bymonth" report, |
|
174
|
** else the "byyear" report. If zUserName is not NULL then the report is |
|
175
|
** restricted to events created by the named user account. |
|
176
|
*/ |
|
177
|
static void stats_report_by_month_year( |
|
178
|
char includeMonth, /* 0 for stats-by-year. 1 for stats-by-month */ |
|
179
|
const char *zUserName /* Only report events by this user */ |
|
180
|
){ |
|
181
|
Stmt query = empty_Stmt; |
|
182
|
int nRowNumber = 0; /* current TR number */ |
|
183
|
int nEventTotal = 0; /* Total event count */ |
|
184
|
int rowClass = 0; /* counter for alternating |
|
185
|
row colors */ |
|
186
|
const char *zTimeLabel = includeMonth ? "Year/Month" : "Year"; |
|
187
|
char zPrevYear[5] = {0}; /* For keeping track of when |
|
188
|
we change years while looping */ |
|
189
|
int nEventsPerYear = 0; /* Total event count for the |
|
190
|
current year */ |
|
191
|
char showYearTotal = 0; /* Flag telling us when to show |
|
192
|
the per-year event totals */ |
|
193
|
int nMaxEvents = 1; /* for calculating length of graph |
|
194
|
bars. */ |
|
195
|
int iterations = 0; /* number of weeks/months we iterate |
|
196
|
over */ |
|
197
|
|
|
198
|
char *zCurrentTF; /* The timeframe in which 'now' lives */ |
|
199
|
double rNowFraction; /* Fraction of 'now' timeframe that has |
|
200
|
passed */ |
|
201
|
int nTFChar; /* Prefix of date() for timeframe */ |
|
202
|
|
|
203
|
nTFChar = includeMonth ? 7 : 4; |
|
204
|
stats_report_init_view(); |
|
205
|
db_prepare(&query, |
|
206
|
"SELECT substr(date(mtime),1,%d) AS timeframe," |
|
207
|
" count(*) AS eventCount" |
|
208
|
" FROM v_reports" |
|
209
|
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
|
210
|
" GROUP BY timeframe" |
|
211
|
" ORDER BY timeframe DESC", |
|
212
|
nTFChar, zUserName); |
|
213
|
@ <h1>Timeline Events (%s(stats_report_label_for_type())) |
|
214
|
@ by year%s(includeMonth ? "/month" : "") |
|
215
|
if( zUserName ){ |
|
216
|
@ for user %h(zUserName) |
|
217
|
} |
|
218
|
@ </h1> |
|
219
|
@ <table border='0' cellpadding='2' cellspacing='0' \ |
|
220
|
zCurrentTF = db_text(0, "SELECT substr(date(),1,%d)", nTFChar); |
|
221
|
if( !includeMonth ){ |
|
222
|
@ class='statistics-report-table-events sortable' \ |
|
223
|
@ data-column-types='tnx' data-init-sort='0'> |
|
224
|
style_table_sorter(); |
|
225
|
rNowFraction = db_double(0.5, |
|
226
|
"SELECT (unixepoch() - unixepoch('now','start of year'))*1.0/" |
|
227
|
" (unixepoch('now','start of year','+1 year') - " |
|
228
|
" unixepoch('now','start of year'));"); |
|
229
|
}else{ |
|
230
|
@ class='statistics-report-table-events'> |
|
231
|
rNowFraction = db_double(0.5, |
|
232
|
"SELECT (unixepoch() - unixepoch('now','start of month'))*1.0/" |
|
233
|
" (unixepoch('now','start of month','+1 month') - " |
|
234
|
" unixepoch('now','start of month'));"); |
|
235
|
} |
|
236
|
@ <thead> |
|
237
|
@ <th>%s(zTimeLabel)</th> |
|
238
|
@ <th>Events</th> |
|
239
|
@ <th width='90%%'><!-- relative commits graph --></th> |
|
240
|
@ </thead><tbody> |
|
241
|
/* |
|
242
|
Run the query twice. The first time we calculate the maximum |
|
243
|
number of events for a given row. Maybe someone with better SQL |
|
244
|
Fu can re-implement this with a single query. |
|
245
|
*/ |
|
246
|
while( SQLITE_ROW == db_step(&query) ){ |
|
247
|
int nCount = db_column_int(&query, 1); |
|
248
|
if( strcmp(db_column_text(&query,0),zCurrentTF)==0 |
|
249
|
&& rNowFraction>0.05 |
|
250
|
){ |
|
251
|
nCount = (int)(((double)nCount)/rNowFraction); |
|
252
|
} |
|
253
|
if(nCount>nMaxEvents){ |
|
254
|
nMaxEvents = nCount; |
|
255
|
} |
|
256
|
++iterations; |
|
257
|
} |
|
258
|
db_reset(&query); |
|
259
|
while( SQLITE_ROW == db_step(&query) ){ |
|
260
|
const char *zTimeframe = db_column_text(&query, 0); |
|
261
|
const int nCount = db_column_int(&query, 1); |
|
262
|
int nSize = (nCount>0 && nMaxEvents>0) |
|
263
|
? (int)(100 * nCount / nMaxEvents) |
|
264
|
: 1; |
|
265
|
showYearTotal = 0; |
|
266
|
if(!nSize) nSize = 1; |
|
267
|
if(includeMonth){ |
|
268
|
/* For Month/year view, add a separator for each distinct year. */ |
|
269
|
if(!*zPrevYear || |
|
270
|
(0!=fossil_strncmp(zPrevYear,zTimeframe,4))){ |
|
271
|
showYearTotal = *zPrevYear; |
|
272
|
if(showYearTotal){ |
|
273
|
rowClass = ++nRowNumber % 2; |
|
274
|
@ <tr class='row%d(rowClass)'> |
|
275
|
@ <td></td> |
|
276
|
@ <td colspan='2'>Yearly total: %d(nEventsPerYear)</td> |
|
277
|
@</tr> |
|
278
|
showYearTotal = 0; |
|
279
|
} |
|
280
|
nEventsPerYear = 0; |
|
281
|
memcpy(zPrevYear,zTimeframe,4); |
|
282
|
rowClass = ++nRowNumber % 2; |
|
283
|
@ <tr class='row%d(rowClass)'> |
|
284
|
@ <th colspan='3' class='statistics-report-row-year'>%s(zPrevYear)</th> |
|
285
|
@ </tr> |
|
286
|
} |
|
287
|
} |
|
288
|
rowClass = ++nRowNumber % 2; |
|
289
|
nEventTotal += nCount; |
|
290
|
nEventsPerYear += nCount; |
|
291
|
@<tr class='row%d(rowClass)'> |
|
292
|
@ <td> |
|
293
|
if(includeMonth){ |
|
294
|
cgi_printf("<a href='%R/timeline?" |
|
295
|
"ym=%t&y=%s", |
|
296
|
zTimeframe, |
|
297
|
statsReportTimelineYFlag ); |
|
298
|
/* Reminder: n=nCount is not actually correct for bymonth unless |
|
299
|
that was the only user who caused events. |
|
300
|
*/ |
|
301
|
if( zUserName ){ |
|
302
|
cgi_printf("&u=%t", zUserName); |
|
303
|
} |
|
304
|
cgi_printf("' target='_new'>%s</a>",zTimeframe); |
|
305
|
}else { |
|
306
|
cgi_printf("<a href='?view=byweek&y=%s&type=%c", |
|
307
|
zTimeframe, (char)statsReportType); |
|
308
|
if( zUserName ){ |
|
309
|
cgi_printf("&u=%t", zUserName); |
|
310
|
} |
|
311
|
cgi_printf("'>%s</a>", zTimeframe); |
|
312
|
} |
|
313
|
@ </td><td>%d(nCount)</td> |
|
314
|
@ <td style='white-space: nowrap;'> |
|
315
|
if( strcmp(zTimeframe, zCurrentTF)==0 |
|
316
|
&& rNowFraction>0.05 |
|
317
|
&& nCount>0 |
|
318
|
&& nMaxEvents>0 |
|
319
|
){ |
|
320
|
/* If the timespan covered by this row contains "now", then project |
|
321
|
** the number of changes until the completion of the timespan and |
|
322
|
** show a dashed box of that projection. */ |
|
323
|
int nProj = (int)(((double)nCount)/rNowFraction); |
|
324
|
int nExtra = (int)(((double)nCount)/rNowFraction) - nCount; |
|
325
|
int nXSize = (100 * nExtra)/nMaxEvents; |
|
326
|
@ <span class='statistics-report-graph-line' \ |
|
327
|
@ style='display:inline-block;min-width:%d(nSize)%%;'> </span>\ |
|
328
|
@ <span class='statistics-report-graph-extra' title='%d(nProj)' \ |
|
329
|
@ style='display:inline-block;min-width:%d(nXSize)%%;'> </span>\ |
|
330
|
}else{ |
|
331
|
@ <div class='statistics-report-graph-line' \ |
|
332
|
@ style='width:%d(nSize)%%;'> </div> \ |
|
333
|
} |
|
334
|
@ </td> |
|
335
|
@ </tr> |
|
336
|
/* |
|
337
|
Potential improvement: calculate the min/max event counts and |
|
338
|
use percent-based graph bars. |
|
339
|
*/ |
|
340
|
} |
|
341
|
db_finalize(&query); |
|
342
|
if(includeMonth && !showYearTotal && *zPrevYear){ |
|
343
|
/* Add final year total separator. */ |
|
344
|
rowClass = ++nRowNumber % 2; |
|
345
|
@ <tr class='row%d(rowClass)'> |
|
346
|
@ <td></td> |
|
347
|
@ <td colspan='2'>Yearly total: %d(nEventsPerYear)</td> |
|
348
|
@</tr> |
|
349
|
} |
|
350
|
@ </tbody></table> |
|
351
|
if(nEventTotal){ |
|
352
|
const char *zAvgLabel = includeMonth ? "month" : "year"; |
|
353
|
int nAvg = iterations ? (nEventTotal/iterations) : 0; |
|
354
|
@ <br><div>Total events: %d(nEventTotal) |
|
355
|
@ <br>Average per active %s(zAvgLabel): %d(nAvg) |
|
356
|
@ </div> |
|
357
|
} |
|
358
|
} |
|
359
|
|
|
360
|
/* |
|
361
|
** Implements the "byuser" view for /reports. |
|
362
|
*/ |
|
363
|
static void stats_report_by_user(){ |
|
364
|
Stmt query = empty_Stmt; |
|
365
|
int nRowNumber = 0; /* current TR number */ |
|
366
|
int rowClass = 0; /* counter for alternating |
|
367
|
row colors */ |
|
368
|
int nMaxEvents = 1; /* max number of events for |
|
369
|
all rows. */ |
|
370
|
stats_report_init_view(); |
|
371
|
@ <h1>Timeline Events |
|
372
|
@ (%s(stats_report_label_for_type())) by User</h1> |
|
373
|
db_multi_exec( |
|
374
|
"CREATE TEMP VIEW piechart(amt,label) AS" |
|
375
|
" SELECT count(*), ifnull(euser,user) FROM v_reports" |
|
376
|
" GROUP BY ifnull(euser,user) ORDER BY count(*) DESC;" |
|
377
|
); |
|
378
|
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){ |
|
379
|
@ <center><svg width=700 height=400> |
|
380
|
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT); |
|
381
|
@ </svg></centre><hr> |
|
382
|
} |
|
383
|
style_table_sorter(); |
|
384
|
@ <table class='statistics-report-table-events sortable' border='0' \ |
|
385
|
@ cellpadding='2' cellspacing='0' data-column-types='tkx' data-init-sort='2'> |
|
386
|
@ <thead><tr> |
|
387
|
@ <th>User</th> |
|
388
|
@ <th>Events</th> |
|
389
|
@ <th width='90%%'><!-- relative commits graph --></th> |
|
390
|
@ </tr></thead><tbody> |
|
391
|
db_prepare(&query, |
|
392
|
"SELECT ifnull(euser,user), " |
|
393
|
"COUNT(*) AS eventCount " |
|
394
|
"FROM v_reports " |
|
395
|
"GROUP BY ifnull(euser,user) ORDER BY eventCount DESC"); |
|
396
|
while( SQLITE_ROW == db_step(&query) ){ |
|
397
|
const int nCount = db_column_int(&query, 1); |
|
398
|
if(nCount>nMaxEvents){ |
|
399
|
nMaxEvents = nCount; |
|
400
|
} |
|
401
|
} |
|
402
|
db_reset(&query); |
|
403
|
while( SQLITE_ROW == db_step(&query) ){ |
|
404
|
const char *zUser = db_column_text(&query, 0); |
|
405
|
const int nCount = db_column_int(&query, 1); |
|
406
|
char y = (char)statsReportType; |
|
407
|
int nSize = nCount |
|
408
|
? (int)(100 * nCount / nMaxEvents) |
|
409
|
: 0; |
|
410
|
if(!nCount) continue /* arguable! Possible? */; |
|
411
|
else if(!nSize) nSize = 1; |
|
412
|
rowClass = ++nRowNumber % 2; |
|
413
|
@ <tr class='row%d(rowClass)'> |
|
414
|
@ <td> |
|
415
|
@ <a href="?view=bymonth&user=%h(zUser)&type=%c(y)">%h(zUser)</a> |
|
416
|
@ </td><td data-sortkey='%08x(-nCount)'>%d(nCount)</td> |
|
417
|
@ <td> |
|
418
|
@ <div class='statistics-report-graph-line' |
|
419
|
@ style='width:%d(nSize)%%;'> </div> |
|
420
|
@ </td> |
|
421
|
@</tr> |
|
422
|
/* |
|
423
|
Potential improvement: calculate the min/max event counts and |
|
424
|
use percent-based graph bars. |
|
425
|
*/ |
|
426
|
} |
|
427
|
@ </tbody></table> |
|
428
|
db_finalize(&query); |
|
429
|
} |
|
430
|
|
|
431
|
/* |
|
432
|
** Implements the "byfile" view for /reports. If zUserName is not NULL then the |
|
433
|
** report is restricted to events created by the named user account. |
|
434
|
*/ |
|
435
|
static void stats_report_by_file(const char *zUserName){ |
|
436
|
Stmt query; |
|
437
|
int mxEvent = 1; /* max number of events across all rows */ |
|
438
|
int nRowNumber = 0; |
|
439
|
|
|
440
|
db_multi_exec( |
|
441
|
"CREATE TEMP TABLE statrep(filename, cnt);" |
|
442
|
"INSERT INTO statrep(filename, cnt)" |
|
443
|
" SELECT filename.name, count(distinct mlink.mid)" |
|
444
|
" FROM filename, mlink, event" |
|
445
|
" WHERE filename.fnid=mlink.fnid" |
|
446
|
" AND mlink.mid=event.objid" |
|
447
|
" AND ifnull(coalesce(euser,user,'')=%Q,1)" |
|
448
|
" GROUP BY 1", zUserName |
|
449
|
); |
|
450
|
db_prepare(&query, |
|
451
|
"SELECT filename, cnt FROM statrep ORDER BY cnt DESC, filename /*sort*/" |
|
452
|
); |
|
453
|
mxEvent = db_int(1, "SELECT max(cnt) FROM statrep"); |
|
454
|
@ <h1>Check-ins Per File |
|
455
|
if( zUserName ){ |
|
456
|
@ for user %h(zUserName) |
|
457
|
} |
|
458
|
@ </h1> |
|
459
|
style_table_sorter(); |
|
460
|
@ <table class='statistics-report-table-events sortable' border='0' \ |
|
461
|
@ cellpadding='2' cellspacing='0' data-column-types='tNx' data-init-sort='2'> |
|
462
|
@ <thead><tr> |
|
463
|
@ <th>File</th> |
|
464
|
@ <th>Check-ins</th> |
|
465
|
@ <th width='90%%'><!-- relative commits graph --></th> |
|
466
|
@ </tr></thead><tbody> |
|
467
|
while( SQLITE_ROW == db_step(&query) ){ |
|
468
|
const char *zFile = db_column_text(&query, 0); |
|
469
|
const int n = db_column_int(&query, 1); |
|
470
|
int sz; |
|
471
|
if( n<=0 ) continue; |
|
472
|
sz = (int)(100*n/mxEvent); |
|
473
|
if( sz==0 ) sz = 1; |
|
474
|
@<tr class='row%d(++nRowNumber%2)'> |
|
475
|
@ <td>%z(href("%R/finfo?name=%T",zFile))%h(zFile)</a></td> |
|
476
|
@ <td>%d(n)</td> |
|
477
|
@ <td> |
|
478
|
@ <div class='statistics-report-graph-line' |
|
479
|
@ style='width:%d(sz)%%;'> </div> |
|
480
|
@ </td> |
|
481
|
@</tr> |
|
482
|
} |
|
483
|
@ </tbody></table> |
|
484
|
db_finalize(&query); |
|
485
|
|
|
486
|
} |
|
487
|
|
|
488
|
/* |
|
489
|
** Implements the "byweekday" view for /reports. If zUserName is not NULL then |
|
490
|
** the report is restricted to events created by the named user account. |
|
491
|
*/ |
|
492
|
static void stats_report_day_of_week(const char *zUserName){ |
|
493
|
Stmt query = empty_Stmt; |
|
494
|
int nRowNumber = 0; /* current TR number */ |
|
495
|
int rowClass = 0; /* counter for alternating |
|
496
|
row colors */ |
|
497
|
int nMaxEvents = 1; /* max number of events for |
|
498
|
all rows. */ |
|
499
|
static const char *const daysOfWeek[] = { |
|
500
|
"Sunday", "Monday", "Tuesday", "Wednesday", |
|
501
|
"Thursday", "Friday", "Saturday" |
|
502
|
}; |
|
503
|
|
|
504
|
stats_report_init_view(); |
|
505
|
db_prepare(&query, |
|
506
|
"SELECT cast(strftime('%%w', mtime) AS INTEGER) dow," |
|
507
|
" COUNT(*) AS eventCount" |
|
508
|
" FROM v_reports" |
|
509
|
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
|
510
|
" GROUP BY dow ORDER BY dow", zUserName); |
|
511
|
@ <h1>Timeline Events (%h(stats_report_label_for_type())) by Day of the Week |
|
512
|
if( zUserName ){ |
|
513
|
@ for user %h(zUserName) |
|
514
|
} |
|
515
|
@ </h1> |
|
516
|
db_multi_exec( |
|
517
|
"CREATE TEMP VIEW piechart(amt,label) AS" |
|
518
|
" SELECT count(*)," |
|
519
|
" CASE cast(strftime('%%w', mtime) AS INT)" |
|
520
|
" WHEN 0 THEN 'Sunday'" |
|
521
|
" WHEN 1 THEN 'Monday'" |
|
522
|
" WHEN 2 THEN 'Tuesday'" |
|
523
|
" WHEN 3 THEN 'Wednesday'" |
|
524
|
" WHEN 4 THEN 'Thursday'" |
|
525
|
" WHEN 5 THEN 'Friday'" |
|
526
|
" WHEN 6 THEN 'Saturday'" |
|
527
|
" ELSE 'ERROR'" |
|
528
|
" END" |
|
529
|
" FROM v_reports" |
|
530
|
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
|
531
|
" GROUP BY 2 ORDER BY cast(strftime('%%w', mtime) AS INT);" |
|
532
|
, zUserName |
|
533
|
); |
|
534
|
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){ |
|
535
|
@ <center><svg width=700 height=400> |
|
536
|
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT); |
|
537
|
@ </svg></centre><hr> |
|
538
|
} |
|
539
|
style_table_sorter(); |
|
540
|
@ <table class='statistics-report-table-events sortable' border='0' \ |
|
541
|
@ cellpadding='2' cellspacing='0' data-column-types='ntnx' data-init-sort='1'> |
|
542
|
@ <thead><tr> |
|
543
|
@ <th>DoW</th> |
|
544
|
@ <th>Day</th> |
|
545
|
@ <th>Events</th> |
|
546
|
@ <th width='90%%'><!-- relative commits graph --></th> |
|
547
|
@ </tr></thead><tbody> |
|
548
|
while( SQLITE_ROW == db_step(&query) ){ |
|
549
|
const int nCount = db_column_int(&query, 1); |
|
550
|
if(nCount>nMaxEvents){ |
|
551
|
nMaxEvents = nCount; |
|
552
|
} |
|
553
|
} |
|
554
|
db_reset(&query); |
|
555
|
while( SQLITE_ROW == db_step(&query) ){ |
|
556
|
const int dayNum =db_column_int(&query, 0); |
|
557
|
const int nCount = db_column_int(&query, 1); |
|
558
|
int nSize = nCount |
|
559
|
? (int)(100 * nCount / nMaxEvents) |
|
560
|
: 0; |
|
561
|
if(!nCount) continue /* arguable! Possible? */; |
|
562
|
else if(!nSize) nSize = 1; |
|
563
|
rowClass = ++nRowNumber % 2; |
|
564
|
@<tr class='row%d(rowClass)'> |
|
565
|
@ <td>%d(dayNum)</td> |
|
566
|
@ <td>%s(daysOfWeek[dayNum])</td> |
|
567
|
@ <td>%d(nCount)</td> |
|
568
|
@ <td> |
|
569
|
@ <div class='statistics-report-graph-line' |
|
570
|
@ style='width:%d(nSize)%%;'> </div> |
|
571
|
@ </td> |
|
572
|
@</tr> |
|
573
|
} |
|
574
|
@ </tbody></table> |
|
575
|
db_finalize(&query); |
|
576
|
} |
|
577
|
|
|
578
|
/* |
|
579
|
** Implements the "byhour" view for /reports. If zUserName is not NULL |
|
580
|
** then the report is restricted to events created by the named user |
|
581
|
** account. |
|
582
|
*/ |
|
583
|
static void stats_report_hour_of_day(const char *zUserName){ |
|
584
|
Stmt query = empty_Stmt; |
|
585
|
int nRowNumber = 0; /* current TR number */ |
|
586
|
int rowClass = 0; /* counter for alternating |
|
587
|
row colors */ |
|
588
|
int nMaxEvents = 1; /* max number of events for |
|
589
|
all rows. */ |
|
590
|
|
|
591
|
stats_report_init_view(); |
|
592
|
db_prepare(&query, |
|
593
|
"SELECT cast(strftime('%%H', mtime) AS INTEGER) hod," |
|
594
|
" COUNT(*) AS eventCount" |
|
595
|
" FROM v_reports" |
|
596
|
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
|
597
|
" GROUP BY hod ORDER BY hod", zUserName); |
|
598
|
@ <h1>Timeline Events (%h(stats_report_label_for_type())) by Hour of Day |
|
599
|
if( zUserName ){ |
|
600
|
@ for user %h(zUserName) |
|
601
|
} |
|
602
|
@ </h1> |
|
603
|
db_multi_exec( |
|
604
|
"CREATE TEMP VIEW piechart(amt,label) AS" |
|
605
|
" SELECT count(*), strftime('%%H', mtime) hod" |
|
606
|
" FROM v_reports" |
|
607
|
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
|
608
|
" GROUP BY 2 ORDER BY hod;", |
|
609
|
zUserName |
|
610
|
); |
|
611
|
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){ |
|
612
|
@ <center><svg width=700 height=400> |
|
613
|
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT); |
|
614
|
@ </svg></centre><hr> |
|
615
|
} |
|
616
|
style_table_sorter(); |
|
617
|
@ <table class='statistics-report-table-events sortable' border='0' \ |
|
618
|
@ cellpadding='2' cellspacing='0' data-column-types='nnx' data-init-sort='1'> |
|
619
|
@ <thead><tr> |
|
620
|
@ <th>Hour</th> |
|
621
|
@ <th>Events</th> |
|
622
|
@ <th width='90%%'><!-- relative commits graph --></th> |
|
623
|
@ </tr></thead><tbody> |
|
624
|
while( SQLITE_ROW == db_step(&query) ){ |
|
625
|
const int nCount = db_column_int(&query, 1); |
|
626
|
if(nCount>nMaxEvents){ |
|
627
|
nMaxEvents = nCount; |
|
628
|
} |
|
629
|
} |
|
630
|
db_reset(&query); |
|
631
|
while( SQLITE_ROW == db_step(&query) ){ |
|
632
|
const int hourNum =db_column_int(&query, 0); |
|
633
|
const int nCount = db_column_int(&query, 1); |
|
634
|
int nSize = nCount |
|
635
|
? (int)(100 * nCount / nMaxEvents) |
|
636
|
: 0; |
|
637
|
if(!nCount) continue /* arguable! Possible? */; |
|
638
|
else if(!nSize) nSize = 1; |
|
639
|
rowClass = ++nRowNumber % 2; |
|
640
|
@<tr class='row%d(rowClass)'> |
|
641
|
@ <td>%d(hourNum)</td> |
|
642
|
@ <td>%d(nCount)</td> |
|
643
|
@ <td> |
|
644
|
@ <div class='statistics-report-graph-line' |
|
645
|
@ style='width:%d(nSize)%%;'> </div> |
|
646
|
@ </td> |
|
647
|
@</tr> |
|
648
|
} |
|
649
|
@ </tbody></table> |
|
650
|
db_finalize(&query); |
|
651
|
} |
|
652
|
|
|
653
|
|
|
654
|
/* |
|
655
|
** Helper for stats_report_by_month_year(), which generates a list of |
|
656
|
** week numbers. The "y" query parameter is the year in format YYYY. |
|
657
|
** If zUserName is not NULL then the report is restricted to events |
|
658
|
** created by the named user account. |
|
659
|
*/ |
|
660
|
static void stats_report_year_weeks(const char *zUserName){ |
|
661
|
const char *zYear = P("y"); /* Year for which report shown */ |
|
662
|
Stmt q; |
|
663
|
int nMaxEvents = 1; /* max number of events for |
|
664
|
all rows. */ |
|
665
|
int iterations = 0; /* # of active time periods. */ |
|
666
|
int rowCount = 0; |
|
667
|
int total = 0; |
|
668
|
char *zCurrentWeek; /* Current week number */ |
|
669
|
double rNowFraction = 0.0; /* Fraction of current week that has |
|
670
|
** passed */ |
|
671
|
|
|
672
|
stats_report_init_view(); |
|
673
|
style_submenu_sql("y", "Year:", |
|
674
|
"WITH RECURSIVE a(b) AS (" |
|
675
|
" SELECT substr(date('now'),1,4) UNION ALL" |
|
676
|
" SELECT b-1 FROM a" |
|
677
|
" WHERE b>0+(SELECT substr(date(min(mtime)),1,4) FROM event)" |
|
678
|
") SELECT b, b FROM a ORDER BY b DESC"); |
|
679
|
if( zYear==0 || strlen(zYear)!=4 ){ |
|
680
|
zYear = db_text("1970","SELECT substr(date('now'),1,4);"); |
|
681
|
} |
|
682
|
cgi_printf("<br>\n"); |
|
683
|
db_prepare(&q, |
|
684
|
"SELECT DISTINCT strftime('%%W',mtime) AS wk, " |
|
685
|
" count(*) AS n " |
|
686
|
" FROM v_reports " |
|
687
|
" WHERE %Q=substr(date(mtime),1,4) " |
|
688
|
" AND mtime < current_timestamp " |
|
689
|
" AND ifnull(coalesce(euser,user,'')=%Q,1)" |
|
690
|
" GROUP BY wk ORDER BY wk DESC", zYear, zUserName); |
|
691
|
@ <h1>Timeline events (%h(stats_report_label_for_type())) |
|
692
|
@ for the calendar weeks of %h(zYear) |
|
693
|
if( zUserName ){ |
|
694
|
@ for user %h(zUserName) |
|
695
|
} |
|
696
|
@ </h1> |
|
697
|
zCurrentWeek = db_text(0, |
|
698
|
"SELECT strftime('%%W','now') WHERE date() LIKE '%q%%'", |
|
699
|
zYear); |
|
700
|
if( zCurrentWeek ){ |
|
701
|
rNowFraction = db_double(0.5, |
|
702
|
"SELECT (unixepoch()-unixepoch('now','weekday 0','-7 days'))/604800.0;"); |
|
703
|
} |
|
704
|
style_table_sorter(); |
|
705
|
cgi_printf("<table class='statistics-report-table-events sortable' " |
|
706
|
"border='0' cellpadding='2' width='100%%' " |
|
707
|
"cellspacing='0' data-column-types='tnx' data-init-sort='0'>\n"); |
|
708
|
cgi_printf("<thead><tr>" |
|
709
|
"<th>Week</th>" |
|
710
|
"<th>Events</th>" |
|
711
|
"<th width='90%%'><!-- relative commits graph --></th>" |
|
712
|
"</tr></thead>\n" |
|
713
|
"<tbody>\n"); |
|
714
|
while( SQLITE_ROW == db_step(&q) ){ |
|
715
|
int nCount = db_column_int(&q, 1); |
|
716
|
if( zCurrentWeek!=0 |
|
717
|
&& strcmp(db_column_text(&q,0),zCurrentWeek)==0 |
|
718
|
&& rNowFraction>0.05 |
|
719
|
){ |
|
720
|
nCount = (int)(((double)nCount)/rNowFraction); |
|
721
|
} |
|
722
|
if(nCount>nMaxEvents){ |
|
723
|
nMaxEvents = nCount; |
|
724
|
} |
|
725
|
++iterations; |
|
726
|
} |
|
727
|
db_reset(&q); |
|
728
|
while( SQLITE_ROW == db_step(&q) ){ |
|
729
|
const char *zWeek = db_column_text(&q,0); |
|
730
|
const int nCount = db_column_int(&q,1); |
|
731
|
int nSize = (nCount>0 && nMaxEvents>0) |
|
732
|
? (int)(100 * nCount / nMaxEvents) |
|
733
|
: 0; |
|
734
|
if(!nSize) nSize = 1; |
|
735
|
total += nCount; |
|
736
|
cgi_printf("<tr class='row%d'>", ++rowCount % 2 ); |
|
737
|
cgi_printf("<td><a href='%R/timeline?yw=%t%s&y=%s", |
|
738
|
zYear, zWeek, |
|
739
|
statsReportTimelineYFlag); |
|
740
|
if( zUserName ){ |
|
741
|
cgi_printf("&u=%t",zUserName); |
|
742
|
} |
|
743
|
cgi_printf("'>%s</a></td>",zWeek); |
|
744
|
|
|
745
|
cgi_printf("<td>%d</td>",nCount); |
|
746
|
cgi_printf("<td style='white-space: nowrap;'>"); |
|
747
|
if( nCount ){ |
|
748
|
if( zCurrentWeek!=0 |
|
749
|
&& strcmp(zWeek, zCurrentWeek)==0 |
|
750
|
&& rNowFraction>0.05 |
|
751
|
&& nMaxEvents>0 |
|
752
|
){ |
|
753
|
/* If the timespan covered by this row contains "now", then project |
|
754
|
** the number of changes until the completion of the week and |
|
755
|
** show a dashed box of that projection. */ |
|
756
|
int nProj = (int)(((double)nCount)/rNowFraction); |
|
757
|
int nExtra = (int)(((double)nCount)/rNowFraction) - nCount; |
|
758
|
int nXSize = (100 * nExtra)/nMaxEvents; |
|
759
|
@ <span class='statistics-report-graph-line' \ |
|
760
|
@ style='display:inline-block;min-width:%d(nSize)%%;'> </span>\ |
|
761
|
@ <span class='statistics-report-graph-extra' title='%d(nProj)' \ |
|
762
|
@ style='display:inline-block;min-width:%d(nXSize)%%;'> </span>\ |
|
763
|
}else{ |
|
764
|
@ <div class='statistics-report-graph-line' \ |
|
765
|
@ style='width:%d(nSize)%%;'> </div> \ |
|
766
|
} |
|
767
|
} |
|
768
|
cgi_printf("</td></tr>\n"); |
|
769
|
} |
|
770
|
db_finalize(&q); |
|
771
|
cgi_printf("</tbody></table>"); |
|
772
|
if(total){ |
|
773
|
int nAvg = iterations ? (total/iterations) : 0; |
|
774
|
cgi_printf("<br><div>Total events: %d<br>" |
|
775
|
"Average per active week: %d</div>", |
|
776
|
total, nAvg); |
|
777
|
} |
|
778
|
} |
|
779
|
|
|
780
|
|
|
781
|
/* |
|
782
|
** Generate a report that shows the most recent change for each user. |
|
783
|
*/ |
|
784
|
static void stats_report_last_change(void){ |
|
785
|
Stmt s; |
|
786
|
double rNow; |
|
787
|
char *zBaseUrl; |
|
788
|
|
|
789
|
stats_report_init_view(); |
|
790
|
style_table_sorter(); |
|
791
|
@ <h1>Event Summary |
|
792
|
@ (%s(stats_report_label_for_type())) by User</h1> |
|
793
|
@ <table border=1 class='statistics-report-table-events sortable' \ |
|
794
|
@ cellpadding=2 cellspacing=0 data-column-types='tNK' data-init-sort='3'> |
|
795
|
@ <thead><tr> |
|
796
|
@ <th>User<th>Total Changes<th>Last Change</tr></thead> |
|
797
|
@ <tbody> |
|
798
|
zBaseUrl = mprintf("%R/timeline?y=%t&u=", PD("type","ci")); |
|
799
|
db_prepare(&s, |
|
800
|
"SELECT coalesce(euser,user)," |
|
801
|
" count(*)," |
|
802
|
" max(mtime)" |
|
803
|
" FROM v_reports" |
|
804
|
" GROUP BY 1" |
|
805
|
" ORDER BY 3 DESC" |
|
806
|
); |
|
807
|
rNow = db_double(0.0, "SELECT julianday('now');"); |
|
808
|
while( db_step(&s)==SQLITE_ROW ){ |
|
809
|
const char *zUser = db_column_text(&s, 0); |
|
810
|
int cnt = db_column_int(&s, 1); |
|
811
|
double rMTime = db_column_double(&s,2); |
|
812
|
char *zAge = human_readable_age(rNow - rMTime); |
|
813
|
@ <tr> |
|
814
|
@ <td><a href='%s(zBaseUrl)%t(zUser)'>%h(zUser)</a> |
|
815
|
@ <td>%d(cnt) |
|
816
|
@ <td data-sortkey='%f(rMTime)' style='white-space:nowrap'>%s(zAge?zAge:"") |
|
817
|
@ </tr> |
|
818
|
fossil_free(zAge); |
|
819
|
} |
|
820
|
@ </tbody></table> |
|
821
|
db_finalize(&s); |
|
822
|
} |
|
823
|
|
|
824
|
|
|
825
|
/* Report types |
|
826
|
*/ |
|
827
|
#define RPT_BYFILE 1 |
|
828
|
#define RPT_BYMONTH 2 |
|
829
|
#define RPT_BYUSER 3 |
|
830
|
#define RPT_BYWEEK 4 |
|
831
|
#define RPT_BYWEEKDAY 5 |
|
832
|
#define RPT_BYYEAR 6 |
|
833
|
#define RPT_LASTCHNG 7 /* Last change made for each user */ |
|
834
|
#define RPT_BYHOUR 8 /* hour-of-day */ |
|
835
|
#define RPT_NONE 0 /* None of the above */ |
|
836
|
|
|
837
|
/* |
|
838
|
** WEBPAGE: reports |
|
839
|
** |
|
840
|
** Shows activity reports for the repository. |
|
841
|
** |
|
842
|
** Query Parameters: |
|
843
|
** |
|
844
|
** view=REPORT_NAME Valid REPORT_NAME values: |
|
845
|
** * byyear |
|
846
|
** * bymonth |
|
847
|
** * byweek |
|
848
|
** * byweekday |
|
849
|
** * byhour |
|
850
|
** * byuser |
|
851
|
** * byfile |
|
852
|
** * lastchng |
|
853
|
** user=NAME Restricts statistics to the given user |
|
854
|
** type=TYPE Restricts the report to a specific event type: |
|
855
|
** * all (everything), |
|
856
|
** * ci (check-in) |
|
857
|
** * m (merge check-in), |
|
858
|
** * n (non-merge check-in) |
|
859
|
** * f (forum post) |
|
860
|
** * w (wiki page change) |
|
861
|
** * t (ticket change) |
|
862
|
** * g (tag added or removed) |
|
863
|
** Defaulting to all event types. |
|
864
|
** from=DATETIME Consider only events after this timestamp (requires to) |
|
865
|
** to=DATETIME Consider only events before this timestamp (requires from) |
|
866
|
** |
|
867
|
** |
|
868
|
** The view-specific query parameters include: |
|
869
|
** |
|
870
|
** view=byweek: |
|
871
|
** |
|
872
|
** y=YYYY The year to report (default is the server's |
|
873
|
** current year). |
|
874
|
*/ |
|
875
|
void stats_report_page(){ |
|
876
|
const char *zView = P("view"); /* Which view/report to show. */ |
|
877
|
int eType = RPT_NONE; /* Numeric code for view/report to show */ |
|
878
|
int i; /* Loop counter */ |
|
879
|
const char *zUserName; /* Name of user */ |
|
880
|
const char *azView[16]; /* Drop-down menu of view types */ |
|
881
|
static const struct { |
|
882
|
const char *zName; /* Name of view= screen type */ |
|
883
|
const char *zVal; /* Value of view= query parameter */ |
|
884
|
int eType; /* Corresponding RPT_* define */ |
|
885
|
} aViewType[] = { |
|
886
|
{ "File Changes","byfile", RPT_BYFILE }, |
|
887
|
{ "Last Change", "lastchng", RPT_LASTCHNG }, |
|
888
|
{ "By Month", "bymonth", RPT_BYMONTH }, |
|
889
|
{ "By User", "byuser", RPT_BYUSER }, |
|
890
|
{ "By Week", "byweek", RPT_BYWEEK }, |
|
891
|
{ "By Weekday", "byweekday", RPT_BYWEEKDAY }, |
|
892
|
{ "By Year", "byyear", RPT_BYYEAR }, |
|
893
|
{ "By Hour", "byhour", RPT_BYHOUR }, |
|
894
|
}; |
|
895
|
static const char *const azType[] = { |
|
896
|
"a", "All Changes", |
|
897
|
"ci", "Check-ins", |
|
898
|
"f", "Forum Posts", |
|
899
|
"m", "Merge check-ins", |
|
900
|
"n", "Non-merge check-ins", |
|
901
|
"g", "Tags", |
|
902
|
"e", "Tech Notes", |
|
903
|
"t", "Tickets", |
|
904
|
"w", "Wiki" |
|
905
|
}; |
|
906
|
|
|
907
|
login_check_credentials(); |
|
908
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
909
|
zUserName = P("user"); |
|
910
|
if( zUserName==0 ) zUserName = P("u"); |
|
911
|
if( zUserName && zUserName[0]==0 ) zUserName = 0; |
|
912
|
if( zView==0 ){ |
|
913
|
zView = "byuser"; |
|
914
|
cgi_replace_query_parameter("view","byuser"); |
|
915
|
} |
|
916
|
for(i=0; i<count(aViewType); i++){ |
|
917
|
if( fossil_strcmp(zView, aViewType[i].zVal)==0 ){ |
|
918
|
eType = aViewType[i].eType; |
|
919
|
break; |
|
920
|
} |
|
921
|
} |
|
922
|
cgi_check_for_malice(); |
|
923
|
if( eType!=RPT_NONE ){ |
|
924
|
int nView = 0; /* Slots used in azView[] */ |
|
925
|
for(i=0; i<count(aViewType); i++){ |
|
926
|
azView[nView++] = aViewType[i].zVal; |
|
927
|
azView[nView++] = aViewType[i].zName; |
|
928
|
} |
|
929
|
if( eType!=RPT_BYFILE ){ |
|
930
|
style_submenu_multichoice("type", count(azType)/2, azType, 0); |
|
931
|
} |
|
932
|
style_submenu_multichoice("view", nView/2, azView, 0); |
|
933
|
if( eType!=RPT_BYUSER && eType!=RPT_LASTCHNG ){ |
|
934
|
style_submenu_sql("user","User:", |
|
935
|
"SELECT '', 'All Users' UNION ALL " |
|
936
|
"SELECT x, x FROM (" |
|
937
|
" SELECT DISTINCT trim(coalesce(euser,user)) AS x FROM event %s" |
|
938
|
" ORDER BY 1 COLLATE nocase) WHERE x!=''", |
|
939
|
eType==RPT_BYFILE ? "WHERE type='ci'" : "" |
|
940
|
); |
|
941
|
} |
|
942
|
} |
|
943
|
style_submenu_element("Stats", "%R/stat"); |
|
944
|
style_header("Activity Reports"); |
|
945
|
switch( eType ){ |
|
946
|
case RPT_BYYEAR: |
|
947
|
stats_report_by_month_year(0, zUserName); |
|
948
|
break; |
|
949
|
case RPT_BYMONTH: |
|
950
|
stats_report_by_month_year(1, zUserName); |
|
951
|
break; |
|
952
|
case RPT_BYWEEK: |
|
953
|
stats_report_year_weeks(zUserName); |
|
954
|
break; |
|
955
|
default: |
|
956
|
case RPT_BYUSER: |
|
957
|
stats_report_by_user(); |
|
958
|
break; |
|
959
|
case RPT_BYWEEKDAY: |
|
960
|
stats_report_day_of_week(zUserName); |
|
961
|
break; |
|
962
|
case RPT_BYFILE: |
|
963
|
stats_report_by_file(zUserName); |
|
964
|
break; |
|
965
|
case RPT_BYHOUR: |
|
966
|
stats_report_hour_of_day(zUserName); |
|
967
|
break; |
|
968
|
case RPT_LASTCHNG: |
|
969
|
stats_report_last_change(); |
|
970
|
break; |
|
971
|
} |
|
972
|
style_finish_page(); |
|
973
|
} |
|
974
|
|