<?php /** * A special page extension to display monthly edit statistics * * @package MediaWiki * @subpackage Extensions * * @author Sam Stafford * @copyright Copyright 2009, Sam Stafford * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later * Some bits adapted from Matt Grinberg's Usage Statistics extension. */ # Alert the user that this is not a valid entry point to MediaWiki # if they try to access the extension file directly. if (!defined('MEDIAWIKI')) { echo <<<EOT To install the MonthlyStats extension, put the following line in LocalSettings.php: require_once( "$IP/extensions/MonthlyStats/MonthlyStats.php" ); EOT; exit( 1 ); } $dir = dirname(__FILE__) . '/'; require_once('SpecialPage.php'); $wgAutoloadClasses['MonthlyStats'] = $dir . 'MonthlyStats.php'; $wgExtensionMessagesFiles['MonthlyStats'] = $dir . 'MonthlyStats.msg.php'; $wgSpecialPages['MonthlyStats'] = 'MonthlyStats'; $version = '$Change: 7308 $'; $version = substr( $version, 9, -2 ); $wgExtensionCredits['specialpage'][] = array ( 'name' => 'MonthlyStats (@'.$version.')', 'url' => 'http://public.perforce.com/wiki/Monthly_Stats', 'author' => 'Sam Stafford', 'description' => 'Special page with monthly activity statistics' ); class MonthlyStats extends SpecialPage { function MonthlyStats() { SpecialPage::SpecialPage('MonthlyStats'); } function execute( $param ) { global $wgOut; global $wgScriptPath; $this->setHeaders(); $wgOut->setPagetitle( 'Monthly Stats' ); $args = explode( '@@', $param ); $par = $args[0]; if ( $par == "" ) $par = 1; if ( $par < 1 ) $par = 1; if ( $par > 10 ) $par = 10; $path = $args[1]; $maxUsers = 18; # number of users that can fit in the chart $dates = array(); # n => date $d_cur = getdate(); $d_end = $d_cur; $d_cur["year"] = $d_cur["year"] - $par; $t_cur = mktime( 0, 0, 0, $d_cur["mon"], 1, $d_cur["year"] ); $t_end = mktime( 0, 0, 0, $d_end["mon"], 1, $d_end["year"] ); $t_begin = $t_cur; $s_begin = date( 'Ym00000000', $t_begin ); $i = 0; while ( $t_cur < $t_end ) { $dates[$i] = $t_cur; $i++; $d_cur["mon"] = $d_cur["mon"] + 1; if ( $d_cur["mon"] > 12 ) { $d_cur["year"] = $d_cur["year"] + 1; $d_cur["mon"] = 1; } $t_cur = mktime( 0, 0, 0, $d_cur["mon"], 1, $d_cur["year"] ); } $dates[$i] = $t_cur; $i_max = $i; # This is the current month; $i_max-1 is last month. $db = wfGetDB( DB_SLAVE ); $sql = "SELECT rev_user_text,rev_timestamp FROM ". $db->tableName('revision'). " WHERE rev_timestamp>$s_begin". " ORDER BY rev_timestamp"; $res = $db->query($sql, __METHOD__); $userPages = array(); # user => ( n => count ) $totalPages = array(); # n => counts $i = 0; $maxPages = 0; for ($j=0; $j<$db->numRows($res); $j++) { $row = $db->fetchRow($res); $u = $row[0]; # Figure out which date bucket this row falls into. while ( $i < $i_max && date( 'Ym00000000', $dates[$i+1] ) < $row[1] ) { $i++; } if ( !isset( $userPages[$u] ) ) $userPages[$u] = array(); if ( !isset( $userPages[$u][$i] ) ) $userPages[$u][$i] = 0; if ( !isset( $totalPages[$i] ) ) $totalPages[$i] = 0; $userPages[$u][$i]++; $totalPages[$i]++; if ( $totalPages[$i] > $maxPages ) $maxPages = $totalPages[$i]; } ksort( $userPages ); # Figure out which users are worth showing. $tempUsers = array(); # temp user list $tempCounts = array(); # corresponding edit counts foreach( $userPages as $u => $a ) { $tempUsers[] = $u; $tempCounts[] = max($a); } array_multisort( $tempCounts, $tempUsers ); foreach( $tempUsers as $i => $u ) { if ( count( $tempUsers ) <= $maxUsers ) break; if ( $userPages[$u][$i_max] || $userPages[$u][$i_max-1] ) continue; unset( $tempUsers[$i] ); } $tempUsers = array_slice( $tempUsers, -$maxUsers ); # If it's in tempUsers it's worth showing. foreach( $userPages as $u => $a ) { if ( !in_array( $u, $tempUsers ) ) { if ( !isset( $userPages['(other)'] ) ) $userPages['(other)'] = array_fill( 0, $i_max + 1, 0 ); $userPages['(other)'] = array_sum_values( $userPages['(other)'], $a ); unset( $userPages[$u] ); } } global $wgP4EXEC; global $wgP4PORT; global $wgP4USER; global $wgP4PASSWD; global $wgP4WEBURL; if ( $wgP4EXEC != "" ) { $cmdline = $wgP4EXEC . " -u " . $wgP4USER . " -p " . $wgP4PORT ; if ( $wgP4PASSWD != "" ) $cmdline .= " -P " . $wgP4PASSWD ; $cmdline .= ' changes '; if ( $path != '' ) $cmdline .= '-i '; $cmdline .= $path.'@'.date( 'Y/m/01', $dates[0] ).',@now'; $cmdline .= ' 2>&1'; $userChanges = array(); # user => ( n => count ) $totalChanges = array(); # n => counts $i = 0; $maxChanges = 0; $changes = array(); exec( $cmdline, $changes ); $changes = array_reverse( $changes ); foreach( $changes as $c ) { $cmatch = array(); if ( !preg_match( '/Change \d+ on (\d+)\/(\d+)\/(\d+) by (\S+)@\S+ /', $c, $cmatch ) ) { continue; } $u = $cmatch[4]; #Find the date bucket. $dc = $cmatch[1].$cmatch[2].$cmatch[3]; if ( $dc < date( 'Ym00', $dates[0] ) ) continue; while( $i < $i_max && date( 'Ym00', $dates[$i+1] ) < $dc ) { $i++; } if ( !isset( $userChanges[$u] ) ) $userChanges[$u] = array(); if ( !isset( $userChanges[$u][$i] ) ) $userChanges[$u][$i] = 0; if ( !isset( $totalChanges[$i] ) ) $totalChanges[$i] = 0; $userChanges[$u][$i]++; $totalChanges[$i]++; if ( $totalChanges[$i] > $maxChanges ) $maxChanges = $totalChanges[$i]; } ksort( $userChanges ); # Figure out which users are worth showing. $tempUsers = array(); # temp user list $tempCounts = array(); # corresponding edit counts foreach( $userChanges as $u => $a ) { $tempUsers[] = $u; $tempCounts[] = max($a); } array_multisort( $tempCounts, $tempUsers ); foreach( $tempUsers as $i => $u ) { if ( count( $tempUsers ) <= $maxUsers ) break; if ( $userChanges[$u][$i_max] || $userChanges[$u][$i_max-1] ) continue; unset( $tempUsers[$i] ); } $tempUsers = array_slice( $tempUsers, -$maxUsers ); # If it's in tempUsers it's worth showing. foreach( $userChanges as $u => $a ) { if ( !in_array( $u, $tempUsers ) ) { if ( !isset( $userChanges['(other)'] ) ) $userChanges['(other)'] = array_fill( 0, $i_max + 1, 0 ); $userChanges['(other)'] = array_sum_values( $userChanges['(other)'], $a ); unset( $userChanges[$u] ); } } } #end wgP4EXEC block # Generate colors for users. $userColors = array(); foreach ( $userPages as $u => $a ) $userColors[unorm($u)] = '000000'; if ( isset( $wgP4EXEC ) ) foreach ( $userChanges as $u => $a ) $userColors[unorm($u)] = '000000'; if ( isset( $userColors['(other)'] ) ) { $other = 'DDDDDD'; unset($userColors['(other)']); } ksort( $userColors ); $i = 0; $colorCount = count( $userColors ); foreach( $userColors as $u => $c ) { $userColors[$u] = HSV_TO_RGB ( 0.5 + ( $i * 1.0 / count( $userColors ) ), 0.4, 0.9 ); $i++; } if ( isset( $other ) ) $userColors['(other)'] = $other; # Wiki stats chart. $height = 200; if ( count( $userPages ) * 20 > $height ) $height = count( $userPages ) * 20; if ( $height > 360 ) $height = 360; $url = 'http://chart.apis.google.com/chart?'; $url .= 'chs=800x'.$height.'&'; $url .= 'cht=bvs&chbh=a&'; $url .= 'chds=0,'.$maxPages.'&'; $url .= 'chxt=y,x&'; $url .= 'chma=9,150,9,9&chldp=r&'; $url .= 'chxr=0,0,'.$maxPages.'&'; $url .= 'chxl=1:|'; $interval = $i_max / 12; if ( $interval == 0 ) $interval = 1; for( $i = 0 ; $i < $i_max ; $i++ ) { if ( ( $i + 1 ) % ( $interval ) == 0 ) $url .= date( 'M y', $dates[$i] ); $url .= '|'; } $url = substr( $url, 0, -1 ); $url .= '&'; $data = 'chd=t:'; foreach ( $userPages as $u => $a ) { for ( $i = 0 ; $i < $i_max ; $i++ ) { if ( !isset( $a[$i] ) ) $a[$i] = 0; $data .= "$a[$i],"; } $data = substr( $data, 0, -1 ); $data .= '|'; } $data = substr( $data, 0, -1 ); $data .= '&'; $data .= 'chco='; foreach ( $userPages as $u => $a ) { $data .= $userColors[unorm($u)]; $data .= ','; } $data = substr( $data, 0, -1 ); $data .= '&'; $data .= 'chdl='; foreach( $userPages as $u => $a ) { $data .= $u; $data .= '|'; } $data = substr( $data, 0, -1 ); $data = str_replace( ' ', '%20', $data ); if ( strlen( $url ) + strlen( $data ) > 2048 ) { $data = 'chd=t:'; for( $i = 0 ; $i < $i_max ; $i++ ) { if ( !isset( $totalPages[$i] ) ) $totalPages[$i] = 0; $data .= "$totalPages[$i],"; } $data = substr( $data, 0, -1 ); $data .= '&'; $data .= 'chdl=All Users'; $url = str_replace( 'chs=800x'.$height.'&', 'chs=800x200&', $url ); } $mwa = $url.$data; #Last month's wiki edits (pie) $i = $i_max - 1; $mwl = 'http://chart.apis.google.com/chart?'; $mwl .= 'chs=390x100&'; $mwl .= 'cht=p3&'; $mwl .= 'chtt='.date('M y',$dates[$i]).'&'; $dat = ""; $col = ""; $lab = ""; foreach ( $userPages as $u => $a ) { if ( !isset( $a[$i] ) || !$a[$i] ) continue; $dat .= "$a[$i],"; $col .= $userColors[unorm($u)].","; $lab .= "$u ($a[$i])|"; } if ( strlen( $dat ) ) { $dat = substr( $dat, 0, -1 ); $col = substr( $col, 0, -1 ); $lab = substr( $lab, 0, -1 ); } else { $dat = '1'; $col = 'EEEEEE'; $lab = 'No pages edited'; } $mwl .= 'chd=t:'.$dat.'&'; $mwl .= 'chco='.$col.'&'; $mwl .= 'chl='.$lab; $mwlu = $wgScriptPath.'/index.php?title=Special:Recentchanges'; $mwlu .= '&from='.date( 'Ym01000000', $dates[$i] ); $mwlu .= '&days=60&limit=500'; #This month's wiki edits to date (pie) $i = $i_max; $mwt = 'http://chart.apis.google.com/chart?'; $mwt .= 'chs=390x100&'; $mwt .= 'cht=p3&'; $mwt .= 'chtt='.date('M y',$dates[$i]).' to date&'; $dat = ""; $col = ""; $lab = ""; foreach ( $userPages as $u => $a ) { if ( !isset( $a[$i] ) || !$a[$i] ) continue; $dat .= "$a[$i],"; $col .= $userColors[unorm($u)].","; $lab .= "$u ($a[$i])|"; } if ( strlen( $dat ) ) { $dat = substr( $dat, 0, -1 ); $col = substr( $col, 0, -1 ); $lab = substr( $lab, 0, -1 ); } else { $dat = '1'; $col = 'EEEEEE'; $lab = 'No pages edited yet'; } $mwt .= 'chd=t:'.$dat.'&'; $mwt .= 'chco='.$col.'&'; $mwt .= 'chl='.$lab; $mwtu = $wgScriptPath.'/index.php?title=Special:Recentchanges'; $mwtu .= '&from='.date( 'Ym01000000', $dates[$i] ); $mwtu .= '&days=30&limit=500'; if ( $wgP4EXEC != "" ) { # Perforce stats chart. $height = 200; if ( count( $userChanges ) * 20 > $height ) $height = count( $userChanges ) * 20; if ( $height > 360 ) $height = 360; $url = 'http://chart.apis.google.com/chart?'; $url .= 'chs=800x'.$height.'&'; $url .= 'cht=bvs&chbh=a&'; $url .= 'chds=0,'.$maxChanges.'&'; $url .= 'chxt=y,x&'; $url .= 'chma=9,150,9,9&chldp=r&'; $url .= 'chxr=0,0,'.$maxChanges.'&'; $url .= 'chxl=1:|'; $interval = $i_max / 12; if ( $interval == 0 ) $interval = 1; for( $i = 0 ; $i < $i_max ; $i++ ) { if ( ( $i + 1 ) % ( $interval ) == 0 ) $url .= date( 'M y', $dates[$i] ); $url .= '|'; } $url = substr( $url, 0, -1 ); $url .= '&'; $data = 'chd=t:'; foreach ( $userChanges as $u => $a ) { for ( $i = 0 ; $i < $i_max ; $i++ ) { if ( !isset( $a[$i] ) ) $a[$i] = 0; $data .= "$a[$i],"; } $data = substr( $data, 0, -1 ); $data .= '|'; } $data = substr( $data, 0, -1 ); $data .= '&'; $data .= 'chco='; foreach ( $userChanges as $u => $a ) { $data .= $userColors[unorm($u)]; $data .= ','; } $data = substr( $data, 0, -1 ); $data .= '&'; $data .= 'chdl='; foreach( $userChanges as $u => $a ) { $data .= $u; $data .= '|'; } $data = substr( $data, 0, -1 ); $data = str_replace( ' ', '%20', $data ); if ( strlen( $url ) + strlen( $data ) > 2048 ) { $data = 'chd=t:'; for( $i = 0 ; $i < $i_max ; $i++ ) { if ( !isset( $totalChanges[$i] ) ) $totalChanges[$i] = 0; $data .= "$totalChanges[$i],"; } $data = substr( $data, 0, -1 ); $data .= '&'; $data .= 'chdl=All Users'; $url = str_replace( 'chs=800x'.$height.'&', 'chs=800x200&', $url ); } $p4a = $url.$data; #Last month's Perforce changes (pie) $i = $i_max - 1; $p4l = 'http://chart.apis.google.com/chart?'; $p4l .= 'chs=390x100&'; $p4l .= 'cht=p3&'; $p4l .= 'chtt='.date('M y',$dates[$i]).'&'; $dat = ""; $col = ""; $lab = ""; foreach ( $userChanges as $u => $a ) { if ( !isset( $a[$i] ) || !$a[$i] ) continue; $dat .= "$a[$i],"; $col .= $userColors[unorm($u)].","; $lab .= "$u ($a[$i])|"; } if ( strlen( $dat ) ) { $dat = substr( $dat, 0, -1 ); $col = substr( $col, 0, -1 ); $lab = substr( $lab, 0, -1 ); } else { $dat = '1'; $col = 'EEEEEE'; $lab = 'No changes submitted'; } $p4l .= 'chd=t:'.$dat.'&'; $p4l .= 'chco='.$col.'&'; $p4l .= 'chl='.$lab; $p4lu = $wgP4WEBURL . '?ac=43&sr='; $p4lu .= date( 'Y/m/01', $dates[$i] ) . '&sr2='; $p4lu .= date( 'Y/m/00', $dates[$i+1] ); $p4lu .= '&al=y'; #This month's Perforce changes (pie) $i = $i_max; $p4t = 'http://chart.apis.google.com/chart?'; $p4t .= 'chs=390x100&'; $p4t .= 'cht=p3&'; $p4t .= 'chtt='.date('M y',$dates[$i]).' to date&'; $dat = ""; $col = ""; $lab = ""; foreach ( $userChanges as $u => $a ) { if ( !isset( $a[$i] ) || !$a[$i] ) continue; $dat .= "$a[$i],"; $col .= $userColors[unorm($u)].","; $lab .= "$u ($a[$i])|"; } if ( strlen( $dat ) ) { $dat = substr( $dat, 0, -1 ); $col = substr( $col, 0, -1 ); $lab = substr( $lab, 0, -1 ); } else { $dat = '1'; $col = 'EEEEEE'; $lab = 'No changes submitted yet'; } $p4t .= 'chd=t:'.$dat.'&'; $p4t .= 'chco='.$col.'&'; $p4t .= 'chl='.$lab; $p4tu = $wgP4WEBURL . '?ac=43&sr='; $p4tu .= date( 'Y/m/01', $dates[$i] ) . '&sr2=now'; $p4tu .= '&al=y'; } #end P4EXEC check $wgOut->addHTML('<h2>Wiki Page Edits by All Users</h2>'."\n"); $wgOut->addHTML('<p><a href="'.$mwtu.'"><img src="'.$mwt.'"/></a>'); $wgOut->addHTML('<a href="'.$mwlu.'"><img src="'.$mwl.'"/></a></p>'."\n"); $wgOut->addHTML('<p><img src="'.$mwa.'"/></p>'."\n"); if ( $wgP4EXEC != "" ) { $wgOut->addHTML('<p><br/><br/></p>'."\n"); $wgOut->addHTML('<h2>Perforce Changelist Submissions by All Users</h2>'."\n"); $wgOut->addHTML('<p><a href="'.$p4tu.'"><img src="'.$p4t.'"/></a>'); $wgOut->addHTML('<a href="'.$p4lu.'"><img src="'.$p4l.'"/></a></p>'."\n"); $wgOut->addHTML('<p><img src="'.$p4a.'"/></p>'."\n"); } } } function unorm( $u ) # Normalization function for folding wiki and Perforce usernames together. # This works nicely for the Perforce Public Depot; your mileage may vary. { return strtr( strtolower( $u ), '_', ' ' ); } # Slightly tweaked function from actionscript.org forum poster petefs. function HSV_TO_RGB ($H, $S, $V) // HSV Values:Number 0-1 { while ( $H < 0 ) { $H++; } while ( $H > 1 ) { $H--; } if($S == 0) { $R = $G = $B = $V * 255; } else { $var_H = $H * 6; $var_i = floor( $var_H ); $var_1 = $V * ( 1 - $S ); $var_2 = $V * ( 1 - $S * ( $var_H - $var_i ) ); $var_3 = $V * ( 1 - $S * (1 - ( $var_H - $var_i ) ) ); if ($var_i == 0) { $var_R = $V ; $var_G = $var_3 ; $var_B = $var_1 ; } else if ($var_i == 1) { $var_R = $var_2 ; $var_G = $V ; $var_B = $var_1 ; } else if ($var_i == 2) { $var_R = $var_1 ; $var_G = $V ; $var_B = $var_3 ; } else if ($var_i == 3) { $var_R = $var_1 ; $var_G = $var_2 ; $var_B = $V ; } else if ($var_i == 4) { $var_R = $var_3 ; $var_G = $var_1 ; $var_B = $V ; } else { $var_R = $V ; $var_G = $var_1 ; $var_B = $var_2 ; } $R = $var_R * 255; $G = $var_G * 255; $B = $var_B * 255; } return strtoupper( dechex($R).dechex($G).dechex($B) ); } #From Tobias Schlemmer at php.net. /** * Sums the values of the arrays be there keys (PHP 4, PHP 5) * array array_sum_values ( array array1 [, array array2 [, array ...]] ) */ function array_sum_values() { $return = array(); $intArgs = func_num_args(); $arrArgs = func_get_args(); if($intArgs < 1) trigger_error('Warning: Wrong parameter count for array_sum_values()', E_USER_WARNING); foreach($arrArgs as $arrItem) { if(!is_array($arrItem)) trigger_error('Warning: Wrong parameter values for array_sum_values()', E_USER_WARNING); foreach($arrItem as $k => $v) { $return[$k] += $v; } } return $return; }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#10 | 7308 | Sam Stafford |
Just for fun, added a way to specify a path for the changelist query in Special:MonthlyStats. With the "-i" option this actually generates some interesting pictures of overall contributions to a project branch. Might break this out into a distinct parser function at some point so changelist bar charts can be inserted into project or user pages. |
||
#9 | 7303 | Sam Stafford |
Replace URL encoding with more selective " "/"%20" replacement. Full URL encoding seems a little more aggressive than what is necessary and causes the character limit to get overrun faster than it should. |
||
#8 | 7302 | Sam Stafford | URL-encode chart data before checking it for the 2048-character limit. | ||
#7 | 7301 | Sam Stafford |
Fine-tune the logic for which users are color-coded and listed in the graphs and which aren't. Rather than a fixed percent, users are now sorted by greatest number of submissions in a month and the top N are selected over two passes (the first with exemptions for users active in the pie chart months, the second without if the first wasn't enough to whittle it down to N). N is currently set to 18 since that seems to be just enough to fill up the bar chart legend. |
||
#6 | 7299 | Sam Stafford | Have Monthly Stats default to a 1 year period instead of 2. | ||
#5 | 7122 | Sam Stafford |
Fix off-by-one error that was messing up the stats for this month. Not sure why I didn't see this problem last month. Need to keep an eye on it. |
||
#4 | 7111 | Sam Stafford |
Clump users with relatively low counts into a single "(other)" bucket. This potentially cuts down the number of data sets for the bar graph way down, and therefore increases the odds that it'll be possible to show the per-user bar graph. Currently the criteria for clumping a user are: 1) No changes/edits in the last or current month (so pie charts will never show "other"). 2) No bar in the chart would be more than 1% of the overall chart height. 1% is pretty low, so another pass at this might be to raise the bar iteratively until only a certain number of users (TBD) are left standing. |
||
#3 | 7110 | Sam Stafford |
Add links to appropriate Special:Recentchanges and P4Web pages from the pie charts. |
||
#2 | 7109 | Sam Stafford |
Add a bit of logic to scale the height of the bar chart by the number of users in the legend. |
||
#1 | 7108 | Sam Stafford | New Monthly Stats extension. |