RC Filters: combine user registration and experience level filters
authorStephane Bisson <sbisson@wikimedia.org>
Fri, 14 Jul 2017 12:07:41 +0000 (08:07 -0400)
committerStephane Bisson <sbisson@wikimedia.org>
Tue, 18 Jul 2017 15:15:46 +0000 (11:15 -0400)
Add 'registered' and 'unregistered' filters to
user experience group.

Keep minimal definitions of user registration to
support hideanons and hideliu for no-js and
users who are not using ERI filters.

Reword all user experience levels description according
to the phab task.

Bug: T165160
Change-Id: Ie6b1795d7cbdb1692f8eeb13db7afb89ea4e5bbc

includes/specialpage/ChangesListSpecialPage.php
languages/i18n/en.json
languages/i18n/qqq.json
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php

index b85d272..645fbb2 100644 (file)
@@ -86,8 +86,6 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                'filters' => [
                                        [
                                                'name' => 'hideliu',
-                                               'label' => 'rcfilters-filter-registered-label',
-                                               'description' => 'rcfilters-filter-registered-description',
                                                // rcshowhideliu-show, rcshowhideliu-hide,
                                                // wlshowhideliu
                                                'showHideSuffix' => 'showhideliu',
@@ -97,16 +95,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                ) {
                                                        $conds[] = 'rc_user = 0';
                                                },
-                                               'cssClassSuffix' => 'liu',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return $rc->getAttribute( 'rc_user' );
-                                               },
+                                               'isReplacedInStructuredUi' => true,
 
                                        ],
                                        [
                                                'name' => 'hideanons',
-                                               'label' => 'rcfilters-filter-unregistered-label',
-                                               'description' => 'rcfilters-filter-unregistered-description',
                                                // rcshowhideanons-show, rcshowhideanons-hide,
                                                // wlshowhideanons
                                                'showHideSuffix' => 'showhideanons',
@@ -116,10 +109,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                ) {
                                                        $conds[] = 'rc_user != 0';
                                                },
-                                               'cssClassSuffix' => 'anon',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return !$rc->getAttribute( 'rc_user' );
-                                               },
+                                               'isReplacedInStructuredUi' => true,
                                        ]
                                ],
                        ],
@@ -128,9 +118,26 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                'name' => 'userExpLevel',
                                'title' => 'rcfilters-filtergroup-userExpLevel',
                                'class' => ChangesListStringOptionsFilterGroup::class,
-                               // Excludes unregistered users
-                               'isFullCoverage' => false,
+                               'isFullCoverage' => true,
                                'filters' => [
+                                       [
+                                               'name' => 'unregistered',
+                                               'label' => 'rcfilters-filter-user-experience-level-unregistered-label',
+                                               'description' => 'rcfilters-filter-user-experience-level-unregistered-description',
+                                               'cssClassSuffix' => 'user-unregistered',
+                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                                       return !$rc->getAttribute( 'rc_user' );
+                                               }
+                                       ],
+                                       [
+                                               'name' => 'registered',
+                                               'label' => 'rcfilters-filter-user-experience-level-registered-label',
+                                               'description' => 'rcfilters-filter-user-experience-level-registered-description',
+                                               'cssClassSuffix' => 'user-registered',
+                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                                       return $rc->getAttribute( 'rc_user' );
+                                               }
+                                       ],
                                        [
                                                'name' => 'newcomer',
                                                'label' => 'rcfilters-filter-user-experience-level-newcomer-label',
@@ -632,19 +639,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                $this->registerFiltersFromDefinitions( [ $unstructuredGroupDefinition ] );
 
                $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' );
-
-               $registration = $this->getFilterGroup( 'registration' );
-               $anons = $registration->getFilter( 'hideanons' );
-
-               // This means there is a conflict between any item in user experience level
-               // being checked and only anons being *shown* (hideliu=1&hideanons=0 in the
-               // URL, or equivalent).
-               $userExperienceLevel->conflictsWith(
-                       $anons,
-                       'rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global',
-                       'rcfilters-filtergroup-user-experience-level-conflicts-unregistered',
-                       'rcfilters-filter-unregistered-conflicts-user-experience-level'
-               );
+               $registered = $userExperienceLevel->getFilter( 'registered' );
+               $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) );
+               $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) );
+               $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) );
 
                $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' );
                $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' );
@@ -1337,15 +1335,35 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        $wgLearnerMemberSince,
                        $wgExperiencedUserMemberSince;
 
-               $LEVEL_COUNT = 3;
+               $LEVEL_COUNT = 5;
 
-               // If all levels are selected, all logged-in users are included (but no
-               // anons), so we can short-circuit.
+               // If all levels are selected, don't filter
                if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
+                       return;
+               }
+
+               // both 'registered' and 'unregistered', experience levels, if any, are included in 'registered'
+               if (
+                       in_array( 'registered', $selectedExpLevels ) &&
+                       in_array( 'unregistered', $selectedExpLevels )
+               ) {
+                       return;
+               }
+
+               // 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
+               if (
+                       in_array( 'registered', $selectedExpLevels ) &&
+                       !in_array( 'unregistered', $selectedExpLevels )
+               ) {
                        $conds[] = 'rc_user != 0';
                        return;
                }
 
+               if ( $selectedExpLevels === [ 'unregistered' ] ) {
+                       $conds[] = 'rc_user = 0';
+                       return;
+               }
+
                $tables[] = 'user';
                $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
 
@@ -1373,24 +1391,39 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        IDatabase::LIST_AND
                );
 
+               $conditions = [];
+
+               if ( in_array( 'unregistered', $selectedExpLevels ) ) {
+                       $selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
+                       $conditions[] = 'rc_user = 0';
+               }
+
                if ( $selectedExpLevels === [ 'newcomer' ] ) {
-                       $conds[] = "NOT ( $aboveNewcomer )";
+                       $conditions[] = "NOT ( $aboveNewcomer )";
                } elseif ( $selectedExpLevels === [ 'learner' ] ) {
-                       $conds[] = $dbr->makeList(
+                       $conditions[] = $dbr->makeList(
                                [ $aboveNewcomer, "NOT ( $aboveLearner )" ],
                                IDatabase::LIST_AND
                        );
                } elseif ( $selectedExpLevels === [ 'experienced' ] ) {
-                       $conds[] = $aboveLearner;
+                       $conditions[] = $aboveLearner;
                } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) {
-                       $conds[] = "NOT ( $aboveLearner )";
+                       $conditions[] = "NOT ( $aboveLearner )";
                } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) {
-                       $conds[] = $dbr->makeList(
+                       $conditions[] = $dbr->makeList(
                                [ "NOT ( $aboveNewcomer )", $aboveLearner ],
                                IDatabase::LIST_OR
                        );
                } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
-                       $conds[] = $aboveNewcomer;
+                       $conditions[] = $aboveNewcomer;
+               } elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
+                       $conditions[] = 'rc_user != 0';
+               }
+
+               if ( count( $conditions ) > 1 ) {
+                       $conds[] = $dbr->makeList( $conditions, IDatabase::LIST_OR );
+               } elseif ( count( $conditions ) === 1 ) {
+                       $conds[] = reset( $conditions );
                }
        }
 }
index 64f9b7b..1d39569 100644 (file)
        "rcfilters-noresults-conflict": "No results found because the search criteria are in conflict",
        "rcfilters-state-message-subset": "This filter has no effect because its results are included with those of the following, broader {{PLURAL:$2|filter|filters}} (try highlighting to distinguish it): $1",
        "rcfilters-state-message-fullcoverage": "Selecting all filters in a group is the same as selecting none, so this filter has no effect. Group includes: $1",
-       "rcfilters-filtergroup-registration": "User registration",
-       "rcfilters-filter-registered-label": "Registered",
-       "rcfilters-filter-registered-description": "Logged-in editors.",
-       "rcfilters-filter-unregistered-label": "Unregistered",
-       "rcfilters-filter-unregistered-description": "Editors who aren’t logged in.",
-       "rcfilters-filter-unregistered-conflicts-user-experience-level": "This filter conflicts with the following Experience {{PLURAL:$2|filter|filters}}, which {{PLURAL:$2|finds|find}} only registered users: $1",
        "rcfilters-filtergroup-authorship": "Contribution authorship",
        "rcfilters-filter-editsbyself-label": "Changes by you",
        "rcfilters-filter-editsbyself-description": "Your own contributions.",
        "rcfilters-filter-editsbyother-label": "Changes by others",
        "rcfilters-filter-editsbyother-description": "All changes except your own.",
-       "rcfilters-filtergroup-userExpLevel": "Experience level (for registered users only)",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered": "Experience filters find only registered users, so this filter conflicts with the “Unregistered” filter.",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global": "The \"Unregistered\" filter conflicts with one or more Experience filters, which find registered users only. The conflicting filters are marked in the Active Filters area, above.",
+       "rcfilters-filtergroup-userExpLevel": "Experience registration and experience",
+       "rcfilters-filter-user-experience-level-registered-label": "Registered",
+       "rcfilters-filter-user-experience-level-registered-description": "Logged-in editors.",
+       "rcfilters-filter-user-experience-level-unregistered-label": "Unregistered",
+       "rcfilters-filter-user-experience-level-unregistered-description": "Editors who aren't logged-in.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Newcomers",
-       "rcfilters-filter-user-experience-level-newcomer-description": "Fewer than 10 edits and 4 days of activity.",
+       "rcfilters-filter-user-experience-level-newcomer-description": "Registered editors with fewer than 10 edits and 4 days of activity.",
        "rcfilters-filter-user-experience-level-learner-label": "Learners",
-       "rcfilters-filter-user-experience-level-learner-description": "More experience than \"Newcomers\" but less than \"Experienced users\".",
+       "rcfilters-filter-user-experience-level-learner-description": "Registered editors whose experience falls between \"Newcomers\" and \"Experienced users.\"",
        "rcfilters-filter-user-experience-level-experienced-label": "Experienced users",
-       "rcfilters-filter-user-experience-level-experienced-description": "More than 30 days of activity and 500 edits.",
+       "rcfilters-filter-user-experience-level-experienced-description": "Registered editors with more than 500 edits and 30 days of activity.",
        "rcfilters-filtergroup-automated": "Automated contributions",
        "rcfilters-filter-bots-label": "Bot",
        "rcfilters-filter-bots-description": "Edits made by automated tools.",
index d98f8b0..3287cfe 100644 (file)
        "rcfilters-noresults-conflict": "A message displayed in the results area when no results found because there are filters in conflict with one another.",
        "rcfilters-state-message-subset": "Tooltip shown when hovering over a filter tag when one or more broader filters that contain the hovered filter are also selected. This indicates that the hovered filter has no effect because all the results it matches are also matched by the broader filter(s).  Parameters:\n* $1 - Comma-separated string of selected broader filters that this filter is a subset of\n* $2 - Count of filters in $1, for PLURAL",
        "rcfilters-state-message-fullcoverage": "Tooltip shown when hovering over a filter tag when all the filters in its group are selected. This indicates that the hovered filter has no effect because the selected filters in the group cover all changes. Parameters:\n* $1 - Comma-separated string of selected filters in the group\n* $2 - Count of filters in $1, for PLURAL",
-       "rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
-       "rcfilters-filter-registered-label": "Label for the filter for showing edits made by logged-in users.\n{{Identical|Registered}}",
-       "rcfilters-filter-registered-description": "Description for the filter for showing edits made by logged-in users.",
-       "rcfilters-filter-unregistered-label": "Label for the filter for showing edits made by logged-out users.\n{{Identical|Unregistered}}",
-       "rcfilters-filter-unregistered-description": "Description for the filter for showing edits made by logged-out users.",
-       "rcfilters-filter-unregistered-conflicts-user-experience-level": "Tooltip shown when hovering over a Unregistered filter tag, when a User Experience Level filter is also selected.\n\n\"Unregistered\" is {{msg-mw|Rcfilters-filter-unregistered-label}}.\n\n\"Experience\" is based on {{msg-mw|Rcfilters-filtergroup-userExpLevel}}.\n\nThis indicates that no results will be shown, because users matched by the User Experience Level groups are never unregistered.  Parameters:\n* $1 - Comma-separated string of selected User Experience Level filters, e.g. \"Newcomer, Experienced\"\n* $2 - Count of selected User Experience Level filters, for PLURAL",
        "rcfilters-filtergroup-authorship": "Title for the filter group for edit authorship. This filter group allows the user to choose between \"Your own edits\" and \"Edits by others\". More info: https://phabricator.wikimedia.org/T149859",
        "rcfilters-filter-editsbyself-label": "Label for the filter for showing edits made by the current user.",
        "rcfilters-filter-editsbyself-description": "Description for the filter for showing edits made by the current user.",
        "rcfilters-filter-editsbyother-label": "Label for the filter for showing edits made by anyone other than the current user.",
        "rcfilters-filter-editsbyother-description": "Description for the filter for showing edits made by anyone other than the current user.",
        "rcfilters-filtergroup-userExpLevel": "Title for the filter group for user experience levels.",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered": "Tooltip shown when hovering over a User Experience Level filter tag, when only Unregistered users are being shown.  This indicates that no results will be shown, because users matched by the User Experience Level groups are never unregistered.\n\n\"Unregistered\" is {{msg-mw|Rcfilters-filter-unregistered-label}}.",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global": "Message shown in the result area when both a User Experience Level filter and the Unregistered filter are selected.  This indicates that no results will be shown because users selected by the User Experience Filter are never unregistered.\n\n\"Unregistered\" is {{msg-mw|Rcfilters-filter-unregistered-label}}.\n\n\"Experience\" is based on {{msg-mw|Rcfilters-filtergroup-userExpLevel}}.",
+       "rcfilters-filter-user-experience-level-registered-label": "Label for the filter for showing edits made by logged-in editors.",
+       "rcfilters-filter-user-experience-level-registered-description": "Description for the filter for showing edits made by logged-in editors.",
+       "rcfilters-filter-user-experience-level-unregistered-label": "Label for the filter for showing edits made by anonymous editors.",
+       "rcfilters-filter-user-experience-level-unregistered-description": "Description for the filter for showing edits made by anonymous editors.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Label for the filter for showing edits made by new editors.",
        "rcfilters-filter-user-experience-level-newcomer-description": "Description for the filter for showing edits made by new editors.",
        "rcfilters-filter-user-experience-level-learner-label": "Label for the filter for showing edits made by learning editors.",
index e2209eb..4070bc0 100644 (file)
@@ -37,11 +37,8 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                return $mock;
        }
 
-       /** helper to test SpecialRecentchanges::buildMainQueryConds() */
-       private function assertConditions(
-               $expected,
+       private function buildQuery(
                $requestOptions = null,
-               $message = '',
                $user = null
        ) {
                $context = new RequestContext;
@@ -81,6 +78,18 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                        'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
                );
 
+               return $queryConditions;
+       }
+
+       /** helper to test SpecialRecentchanges::buildQuery() */
+       private function assertConditions(
+               $expected,
+               $requestOptions = null,
+               $message = '',
+               $user = null
+       ) {
+               $queryConditions = $this->buildQuery( $requestOptions, $user );
+
                $this->assertEquals(
                        self::normalizeCondition( $expected ),
                        self::normalizeCondition( $queryConditions ),
@@ -373,6 +382,104 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                );
        }
 
+       public function testFilterUserExpLevelAll() {
+               $this->assertConditions(
+                       [
+                               # expected
+                       ],
+                       [
+                               'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
+                       ],
+                       "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
+               );
+       }
+
+       public function testFilterUserExpLevelRegisteredUnregistered() {
+               $this->assertConditions(
+                       [
+                               # expected
+                       ],
+                       [
+                               'userExpLevel' => 'registered;unregistered',
+                       ],
+                       "rc conditions: userExpLevel=registered;unregistered"
+               );
+       }
+
+       public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
+               $this->assertConditions(
+                       [
+                               # expected
+                       ],
+                       [
+                               'userExpLevel' => 'registered;unregistered;learner',
+                       ],
+                       "rc conditions: userExpLevel=registered;unregistered;learner"
+               );
+       }
+
+       public function testFilterUserExpLevelAllExperienceLevels() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user != 0',
+                       ],
+                       [
+                               'userExpLevel' => 'newcomer;learner;experienced',
+                       ],
+                       "rc conditions: userExpLevel=newcomer;learner;experienced"
+               );
+       }
+
+       public function testFilterUserExpLevelRegistrered() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user != 0',
+                       ],
+                       [
+                               'userExpLevel' => 'registered',
+                       ],
+                       "rc conditions: userExpLevel=registered"
+               );
+       }
+
+       public function testFilterUserExpLevelUnregistrered() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user' => 0,
+                       ],
+                       [
+                               'userExpLevel' => 'unregistered',
+                       ],
+                       "rc conditions: userExpLevel=unregistered"
+               );
+       }
+
+       public function testFilterUserExpLevelRegistreredOrLearner() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user != 0',
+                       ],
+                       [
+                               'userExpLevel' => 'registered;learner',
+                       ],
+                       "rc conditions: userExpLevel=registered;learner"
+               );
+       }
+
+       public function testFilterUserExpLevelUnregistreredOrExperienced() {
+               $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
+
+               $this->assertRegExp(
+                       '/\(rc_user = 0\) OR \(\(user_editcount >= 500\) AND \(user_registration <= \'\d+\'\)\)/',
+                       reset( $conds ),
+                       "rc conditions: userExpLevel=unregistered;experienced"
+               );
+       }
+
        public function testFilterUserExpLevel() {
                $now = time();
                $this->setMwGlobals( [
@@ -438,18 +545,6 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                        $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
                        'Learner and more experienced'
                );
-
-               // newcomers, learner, and more experienced
-               // TOOD: Fix test.  This needs to test that anons are excluded,
-               // and right now the join fails.
-               /* $this->assertArrayEquals( */
-               /*      [ */
-               /*              'Newcomer1', 'Newcomer2', 'Newcomer3', */
-               /*              'Learner1', 'Learner2', 'Learner3', 'Learner4', */
-               /*              'Experienced1', */
-               /*      ], */
-               /*      $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ], $now ) */
-               /* ); */
        }
 
        private function createUsers( $specs, $now ) {
@@ -798,7 +893,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                                        "hideliu" => true,
                                        "userExpLevel" => "newcomer",
                                ],
-                               "expectedConflicts" => true,
+                               "expectedConflicts" => false,
                        ],
                        [
                                "parameters" => [