From a795d54810bea9827d3da8e4b8879852f5761e4e Mon Sep 17 00:00:00 2001 From: Stephane Bisson Date: Tue, 15 Nov 2016 16:35:54 -0500 Subject: [PATCH] Special:RC filter: userExpLevel Allows filtering changes based on user experience level. Supports the following levels: 'newcomer', 'learner', 'experienced' Will be used by the ERI project. Bug: T149637 Change-Id: Ib2ac92925836ce2f3706d898968538aa18d14d5d --- includes/DefaultSettings.php | 17 +++ includes/specials/SpecialRecentchanges.php | 65 +++++++++ .../specials/SpecialRecentchangesTest.php | 132 ++++++++++++++++++ 3 files changed, 214 insertions(+) diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index f419b77425..327448019f 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -8460,6 +8460,23 @@ $wgCSPFalsePositiveUrls = [ 'https://d5p.de17a.com' => true, ]; +/** + * The following variables define 3 user experience levels: + * + * - newcomer: has not yet reached the 'learner' level + * + * - learner: has at least $wgLearnerEdits and has been + * a member for $wgLearnerMemberSince days + * but has not yet reached the 'experienced' level. + * + * - experienced: has at least $wgExperiencedUserEdits edits and + * has been a member for $wgExperiencedUserMemberSince days. + */ +$wgLearnerEdits = 10; +$wgLearnerMemberSince = 4; # days +$wgExperiencedUserEdits = 500; +$wgExperiencedUserMemberSince = 30; # days + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 8530eb10f1..1ce61e307c 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -91,6 +91,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $opts->add( 'categories_any', false ); $opts->add( 'tagfilter', '' ); + $opts->add( 'userExpLevel', 'all' ); + return $opts; } @@ -240,6 +242,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $opts['tagfilter'] ); + $this->filterOnUserExperienceLevel( $tables, $conds, $join_conds, $opts ); + if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) ) { @@ -803,4 +807,65 @@ class SpecialRecentChanges extends ChangesListSpecialPage { return 60 * 5; } + function filterOnUserExperienceLevel( &$tables, &$conds, &$join_conds, $opts ) { + global $wgLearnerEdits, + $wgExperiencedUserEdits, + $wgLearnerMemberSince, + $wgExperiencedUserMemberSince; + + $selectedExpLevels = explode( ',', strtolower( $opts['userExpLevel'] ) ); + // remove values that are not recognized + $selectedExpLevels = array_intersect( + $selectedExpLevels, + [ 'newcomer', 'learner', 'experienced' ] + ); + sort( $selectedExpLevels ); + + if ( $selectedExpLevels ) { + $tables[] = 'user'; + $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ]; + + $now = time(); + $secondsPerDay = 86400; + $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay; + $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay; + + $aboveNewcomer = $this->getDB()->makeList( + [ + 'user_editcount >= ' . intval( $wgLearnerEdits ), + 'user_registration <= ' . $this->getDB()->timestamp( $learnerCutoff ), + ], + IDatabase::LIST_AND + ); + + $aboveLearner = $this->getDB()->makeList( + [ + 'user_editcount >= ' . intval( $wgExperiencedUserEdits ), + 'user_registration <= ' . $this->getDB()->timestamp( $experiencedUserCutoff ), + ], + IDatabase::LIST_AND + ); + + if ( $selectedExpLevels === [ 'newcomer' ] ) { + $conds[] = "NOT ( $aboveNewcomer )"; + } elseif ( $selectedExpLevels === [ 'learner' ] ) { + $conds[] = $this->getDB()->makeList( + [ $aboveNewcomer, "NOT ( $aboveLearner )" ], + IDatabase::LIST_AND + ); + } elseif ( $selectedExpLevels === [ 'experienced' ] ) { + $conds[] = $aboveLearner; + } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) { + $conds[] = "NOT ( $aboveLearner )"; + } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) { + $conds[] = $this->getDB()->makeList( + [ "NOT ( $aboveNewcomer )", $aboveLearner ], + IDatabase::LIST_OR + ); + } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) { + $conds[] = $aboveNewcomer; + } + } + } + } diff --git a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php index c11e6a3927..ab92aeeb3f 100644 --- a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php +++ b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php @@ -393,4 +393,136 @@ class SpecialRecentchangesTest extends MediaWikiTestCase { $user ); } + + public function testFilterUserExpLevel() { + $this->setMwGlobals( [ + 'wgLearnerEdits' => 10, + 'wgLearnerMemberSince' => 4, + 'wgExperiencedUserEdits' => 500, + 'wgExperiencedUserMemberSince' => 30, + ] ); + + $this->createUsers( [ + 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ], + 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ], + 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ], + 'Learner1' => [ 'edits' => 15, 'days' => 10 ], + 'Learner2' => [ 'edits' => 450, 'days' => 20 ], + 'Learner3' => [ 'edits' => 460, 'days' => 33 ], + 'Learner4' => [ 'edits' => 525, 'days' => 28 ], + 'Experienced1' => [ 'edits' => 538, 'days' => 33 ], + ] ); + + // newcomers only + $this->assertArrayEquals( + [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ], + $this->fetchUsers( [ 'userExpLevel' => 'newcomer' ] ) + ); + + // newcomers and learner + $this->assertArrayEquals( + [ + 'Newcomer1', 'Newcomer2', 'Newcomer3', + 'Learner1', 'Learner2', 'Learner3', 'Learner4', + ], + $this->fetchUsers( [ 'userExpLevel' => 'newcomer,learner' ] ) + ); + + // newcomers and more learner + $this->assertArrayEquals( + [ + 'Newcomer1', 'Newcomer2', 'Newcomer3', + 'Experienced1', + ], + $this->fetchUsers( [ 'userExpLevel' => 'newcomer,experienced' ] ) + ); + + // learner only + $this->assertArrayEquals( + [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ], + $this->fetchUsers( [ 'userExpLevel' => 'learner' ] ) + ); + + // more experienced only + $this->assertArrayEquals( + [ 'Experienced1' ], + $this->fetchUsers( [ 'userExpLevel' => 'experienced' ] ) + ); + + // learner and more experienced + $this->assertArrayEquals( + [ + 'Learner1', 'Learner2', 'Learner3', 'Learner4', + 'Experienced1', + ], + $this->fetchUsers( [ 'userExpLevel' => 'learner,experienced' ] ) + ); + + // newcomers, learner, and more experienced + $this->assertArrayEquals( + [ + 'Newcomer1', 'Newcomer2', 'Newcomer3', + 'Learner1', 'Learner2', 'Learner3', 'Learner4', + 'Experienced1', + ], + $this->fetchUsers( [ 'userExpLevel' => 'newcomer,learner,experienced' ] ) + ); + + // 'all' + $this->assertArrayEquals( + [ + 'Newcomer1', 'Newcomer2', 'Newcomer3', + 'Learner1', 'Learner2', 'Learner3', 'Learner4', + 'Experienced1', + ], + $this->fetchUsers( [ 'userExpLevel' => 'all' ] ) + ); + } + + private function createUsers( $specs ) { + $dbw = wfGetDB( DB_MASTER ); + foreach ( $specs as $name => $spec ) { + User::createNew( + $name, + [ + 'editcount' => $spec['edits'], + 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'] ) ), + 'email' => 'ut', + ] + ); + } + } + + private function fetchUsers( $filters ) { + $specialRC = new SpecialRecentChanges(); + + $tables = []; + $conds = []; + $join_conds = []; + + $specialRC->filterOnUserExperienceLevel( + $tables, + $conds, + $join_conds, + $filters + ); + + $result = wfGetDB( DB_MASTER )->select( + 'user', + 'user_name', + array_filter( $conds ) + [ 'user_email' => 'ut' ] + ); + + $usernames = []; + foreach ( $result as $row ) { + $usernames[] = $row->user_name; + } + + return $usernames; + } + + private function daysAgo( $days ) { + $secondsPerDay = 86400; + return time() - $days * $secondsPerDay; + } } -- 2.20.1