summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'ContributionScores/src')
-rw-r--r--ContributionScores/src/ContributionScores.php421
1 files changed, 421 insertions, 0 deletions
diff --git a/ContributionScores/src/ContributionScores.php b/ContributionScores/src/ContributionScores.php
new file mode 100644
index 00000000..2a9d7a5a
--- /dev/null
+++ b/ContributionScores/src/ContributionScores.php
@@ -0,0 +1,421 @@
+<?php
+/** \file
+ * \brief Contains code for the ContributionScores Class (extends SpecialPage).
+ */
+
+use MediaWiki\MediaWikiServices;
+
+/// Special page class for the Contribution Scores extension
+/**
+ * Special page that generates a list of wiki contributors based
+ * on edit diversity (unique pages edited) and edit volume (total
+ * number of edits.
+ *
+ * @ingroup Extensions
+ * @author Tim Laqua <t.laqua@gmail.com>
+ */
+class ContributionScores extends IncludableSpecialPage {
+ const CONTRIBUTIONSCORES_MAXINCLUDELIMIT = 50;
+
+ public function __construct() {
+ parent::__construct( 'ContributionScores' );
+ }
+
+ public static function onParserFirstCallInit( Parser $parser ) {
+ $parser->setFunctionHook( 'cscore', [ self::class, 'efContributionScoresRender' ] );
+ }
+
+ public static function efContributionScoresRender( $parser, $usertext, $metric = 'score' ) {
+ global $wgContribScoreDisableCache, $wgContribScoreUseRoughEditCount;
+
+ if ( $wgContribScoreDisableCache ) {
+ $parser->getOutput()->updateCacheExpiry( 0 );
+ }
+
+ $user = User::newFromName( $usertext );
+ $dbr = wfGetDB( DB_REPLICA );
+
+ if ( $user instanceof User && $user->isRegistered() ) {
+ global $wgLang;
+ $revVar = $wgContribScoreUseRoughEditCount ? 'user_editcount' : 'COUNT(rev_id)';
+
+ $revWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $user );
+ if ( $metric == 'score' ) {
+ $row = $dbr->selectRow(
+ [ 'revision' ] + $revWhere['tables'],
+ [ 'wiki_rank' => "COUNT(DISTINCT rev_page)+SQRT($revVar-COUNT(DISTINCT rev_page))*2" ],
+ $revWhere['conds'],
+ __METHOD__,
+ [],
+ $revWhere['joins']
+ );
+ $output = $wgLang->formatNum( round( $row->wiki_rank, 0 ) );
+ } elseif ( $metric == 'changes' ) {
+ $row = $dbr->selectRow(
+ [ 'revision' ] + $revWhere['tables'],
+ [ 'rev_count' => $revVar ],
+ $revWhere['conds'],
+ __METHOD__,
+ [],
+ $revWhere['joins']
+ );
+ $output = $wgLang->formatNum( $row->rev_count );
+ } elseif ( $metric == 'pages' ) {
+ $row = $dbr->selectRow(
+ [ 'revision' ] + $revWhere['tables'],
+ [ 'page_count' => 'COUNT(DISTINCT rev_page)' ],
+ $revWhere['conds'],
+ __METHOD__,
+ [],
+ $revWhere['joins']
+ );
+ $output = $wgLang->formatNum( $row->page_count );
+ } else {
+ $output = wfMessage( 'contributionscores-invalidmetric' )->text();
+ }
+ } else {
+ $output = wfMessage( 'contributionscores-invalidusername' )->text();
+ }
+ return $parser->insertStripItem( $output, $parser->mStripState );
+ }
+
+ /**
+ * Function fetch Contribution Scores data from database
+ *
+ * @param int $days Days in the past to run report for
+ * @param int $limit Maximum number of users to return (default 50)
+ * @return array Data including the requested Contribution Scores.
+ */
+ public static function getContributionScoreData( $days, $limit ) {
+ global $wgContribScoreIgnoreBots, $wgContribScoreIgnoreBlockedUsers, $wgContribScoreIgnoreUsernames,
+ $wgContribScoreUseRoughEditCount;
+
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $revQuery = ActorMigration::newMigration()->getJoin( 'rev_user' );
+ $revQuery['tables'] = array_merge( [ 'revision' ], $revQuery['tables'] );
+
+ $revUser = $revQuery['fields']['rev_user'];
+ $revUsername = $revQuery['fields']['rev_user_text'];
+
+ $sqlWhere = [];
+
+ if ( $days > 0 ) {
+ $date = time() - ( 60 * 60 * 24 * $days );
+ $sqlWhere[] = 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $date ) );
+ }
+
+ $sqlVars = [
+ 'rev_user' => $revUser,
+ 'page_count' => 'COUNT(DISTINCT rev_page)'
+ ];
+ if ( $wgContribScoreUseRoughEditCount ) {
+ $revQuery['tables'][] = 'user';
+ $revQuery['joins']['user'] = [ 'LEFT JOIN', [ "$revUser != 0", "user_id = $revUser" ] ];
+ $sqlVars['rev_count'] = 'user_editcount';
+ } else {
+ $sqlVars['rev_count'] = 'COUNT(rev_id)';
+ }
+
+ if ( $wgContribScoreIgnoreBlockedUsers ) {
+ $sqlWhere[] = "{$revUser} NOT IN " .
+ $dbr->buildSelectSubquery( 'ipblocks', 'ipb_user', 'ipb_user <> 0', __METHOD__ );
+ }
+
+ if ( $wgContribScoreIgnoreBots ) {
+ $sqlWhere[] = "{$revUser} NOT IN " .
+ $dbr->buildSelectSubquery( 'user_groups', 'ug_user', [
+ 'ug_group' => 'bot',
+ 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
+ ], __METHOD__ );
+ }
+
+ if ( count( $wgContribScoreIgnoreUsernames ) ) {
+ $listIgnoredUsernames = $dbr->makeList( $wgContribScoreIgnoreUsernames );
+ $sqlWhere[] = "{$revUsername} NOT IN ($listIgnoredUsernames)";
+ }
+
+ if ( $dbr->unionSupportsOrderAndLimit() ) {
+ $order = [
+ 'GROUP BY' => 'rev_user',
+ 'ORDER BY' => 'page_count DESC',
+ 'LIMIT' => $limit
+ ];
+ } else {
+ $order = [ 'GROUP BY' => 'rev_user' ];
+ }
+
+ $sqlMostPages = $dbr->selectSQLText(
+ $revQuery['tables'],
+ $sqlVars,
+ $sqlWhere,
+ __METHOD__,
+ $order,
+ $revQuery['joins']
+ );
+
+ if ( $dbr->unionSupportsOrderAndLimit() ) {
+ $order['ORDER BY'] = 'rev_count DESC';
+ }
+
+ $sqlMostRevs = $dbr->selectSQLText(
+ $revQuery['tables'],
+ $sqlVars,
+ $sqlWhere,
+ __METHOD__,
+ $order,
+ $revQuery['joins']
+ );
+
+ $sqlMostPagesOrRevs = $dbr->unionQueries( [ $sqlMostPages, $sqlMostRevs ], false );
+ $res = $dbr->select(
+ [
+ 'u' => 'user',
+ 's' => new Wikimedia\Rdbms\Subquery( $sqlMostPagesOrRevs ),
+ ],
+ [
+ 'user_id',
+ 'user_name',
+ 'user_real_name',
+ 'page_count',
+ 'rev_count',
+ 'wiki_rank' => 'page_count+SQRT(rev_count-page_count)*2',
+ ],
+ [],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'wiki_rank DESC',
+ 'GROUP BY' => 'user_name',
+ 'LIMIT' => $limit,
+ ],
+ [
+ 's' => [
+ 'JOIN',
+ 'user_id=rev_user'
+ ]
+ ]
+ );
+ $ret = iterator_to_array( $res );
+ return $ret;
+ }
+
+ /// Generates a "Contribution Scores" table for a given LIMIT and date range
+
+ /**
+ * Function generates Contribution Scores tables in HTML format (not wikiText)
+ *
+ * @param int $days Days in the past to run report for
+ * @param int $limit Maximum number of users to return (default 50)
+ * @param string|null $title The title of the table
+ * @param array $options array of options (default none; nosort/notools)
+ * @return string Html Table representing the requested Contribution Scores.
+ */
+ function genContributionScoreTable( $days, $limit, $title = null, $options = 'none' ) {
+ global $wgContribScoresUseRealName, $wgContribScoreCacheTTL;
+
+ $opts = explode( ',', strtolower( $options ) );
+
+ $sortable = in_array( 'nosort', $opts ) ? '' : ' sortable';
+
+ $output = "<table class=\"wikitable contributionscores plainlinks{$sortable}\" >\n" .
+ "<tr class='header'>\n" .
+ Html::element( 'th', [], $this->msg( 'contributionscores-rank' )->text() ) .
+ Html::element( 'th', [], $this->msg( 'contributionscores-score' )->text() ) .
+ Html::element( 'th', [], $this->msg( 'contributionscores-pages' )->text() ) .
+ Html::element( 'th', [], $this->msg( 'contributionscores-changes' )->text() ) .
+ Html::element( 'th', [], $this->msg( 'contributionscores-username' )->text() );
+
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $data = $cache->getWithSetCallback(
+ $cache->makeKey( 'contributionscores', 'data-' . (string)$days ),
+ $wgContribScoreCacheTTL * 60,
+ function () use ( $days ) {
+ // Use max limit, as limit doesn't matter with performance.
+ // Avoid purge multiple times since limit on transclusion can be vary.
+ return self::getContributionScoreData( $days, self::CONTRIBUTIONSCORES_MAXINCLUDELIMIT );
+ } );
+
+ $lang = $this->getLanguage();
+
+ $altrow = '';
+ $user_rank = 1;
+
+ foreach ( $data as $row ) {
+ if ( $user_rank > $limit ) {
+ break;
+ }
+
+ // Use real name if option used and real name present.
+ if ( $wgContribScoresUseRealName && $row->user_real_name !== '' ) {
+ $userLink = Linker::userLink(
+ $row->user_id,
+ $row->user_name,
+ $row->user_real_name
+ );
+ } else {
+ $userLink = Linker::userLink(
+ $row->user_id,
+ $row->user_name
+ );
+ }
+
+ $output .= Html::closeElement( 'tr' );
+ $output .= "<tr class='{$altrow}'>\n" .
+ "<td class='content' style='padding-right:10px;text-align:right;'>" .
+ $lang->formatNum( $user_rank ) .
+ "\n</td><td class='content' style='padding-right:10px;text-align:right;'>" .
+ $lang->formatNum( round( $row->wiki_rank, 0 ) ) .
+ "\n</td><td class='content' style='padding-right:10px;text-align:right;'>" .
+ $lang->formatNum( $row->page_count ) .
+ "\n</td><td class='content' style='padding-right:10px;text-align:right;'>" .
+ $lang->formatNum( $row->rev_count ) .
+ "\n</td><td class='content'>" .
+ $userLink;
+
+ # Option to not display user tools
+ if ( !in_array( 'notools', $opts ) ) {
+ $output .= Linker::userToolLinks( $row->user_id, $row->user_name );
+ }
+
+ $output .= Html::closeElement( 'td' ) . "\n";
+
+ if ( $altrow == '' && empty( $sortable ) ) {
+ $altrow = 'odd ';
+ } else {
+ $altrow = '';
+ }
+
+ $user_rank++;
+ }
+ $output .= Html::closeElement( 'tr' );
+ $output .= Html::closeElement( 'table' );
+
+ // Transcluded on a normal wiki page.
+ if ( !empty( $title ) ) {
+ $output = Html::rawElement( 'table',
+ [
+ 'style' => 'border-spacing: 0; padding: 0',
+ 'class' => 'contributionscores-wrapper',
+ 'lang' => htmlspecialchars( $lang->getCode() ),
+ 'dir' => $lang->getDir()
+ ],
+ "\n" .
+ "<tr>\n" .
+ "<td style='padding: 0px;'>{$title}</td>\n" .
+ "</tr>\n" .
+ "<tr>\n" .
+ "<td style='padding: 0px;'>{$output}</td>\n" .
+ "</tr>\n"
+ );
+ }
+
+ return $output;
+ }
+
+ function execute( $par ) {
+ $this->setHeaders();
+
+ if ( $this->including() ) {
+ $this->showInclude( $par );
+ } else {
+ $this->showPage();
+ }
+
+ return true;
+ }
+
+ /**
+ * Called when being included on a normal wiki page.
+ * Cache is disabled so it can depend on the user language.
+ * @param string|null $par A subpage give to the special page
+ */
+ function showInclude( $par ) {
+ $days = null;
+ $limit = null;
+ $options = 'none';
+
+ if ( !empty( $par ) ) {
+ $params = explode( '/', $par );
+
+ $limit = intval( $params[0] );
+
+ if ( isset( $params[1] ) ) {
+ $days = intval( $params[1] );
+ }
+
+ if ( isset( $params[2] ) ) {
+ $options = $params[2];
+ }
+ }
+
+ if ( empty( $limit ) || $limit < 1 || $limit > self::CONTRIBUTIONSCORES_MAXINCLUDELIMIT ) {
+ $limit = 10;
+ }
+ if ( $days === null || $days < 0 ) {
+ $days = 7;
+ }
+
+ if ( $days > 0 ) {
+ $reportTitle = $this->msg( 'contributionscores-days' )->numParams( $days )->text();
+ } else {
+ $reportTitle = $this->msg( 'contributionscores-allrevisions' )->text();
+ }
+ $reportTitle .= ' ' . $this->msg( 'contributionscores-top' )->numParams( $limit )->text();
+ $title = Xml::element( 'h4',
+ [ 'class' => 'contributionscores-title' ],
+ $reportTitle
+ ) . "\n";
+ $this->getOutput()->addHTML( $this->genContributionScoreTable(
+ $days,
+ $limit,
+ $title,
+ $options
+ ) );
+ }
+
+ /**
+ * Show the special page
+ */
+ function showPage() {
+ global $wgContribScoreReports;
+
+ if ( !is_array( $wgContribScoreReports ) ) {
+ $wgContribScoreReports = [
+ [ 7, 50 ],
+ [ 30, 50 ],
+ [ 0, 50 ]
+ ];
+ }
+
+ $out = $this->getOutput();
+ $out->addWikiMsg( 'contributionscores-info' );
+
+ foreach ( $wgContribScoreReports as $scoreReport ) {
+ list( $days, $revs ) = $scoreReport;
+ if ( $days > 0 ) {
+ $reportTitle = $this->msg( 'contributionscores-days' )->numParams( $days )->text();
+ } else {
+ $reportTitle = $this->msg( 'contributionscores-allrevisions' )->text();
+ }
+ $reportTitle .= ' ' . $this->msg( 'contributionscores-top' )->numParams( $revs )->text();
+ $title = Xml::element( 'h2',
+ [ 'class' => 'contributionscores-title' ],
+ $reportTitle
+ ) . "\n";
+ $out->addHTML( $title );
+ $out->addHTML( $this->genContributionScoreTable( $days, $revs ) );
+ }
+ }
+
+ public function maxIncludeCacheTime() {
+ global $wgContribScoreDisableCache, $wgContribScoreCacheTTL;
+ return $wgContribScoreDisableCache ? 0 : $wgContribScoreCacheTTL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function getGroupName() {
+ return 'wiki';
+ }
+}