Merge "RC Filters: support multiple namespaces"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 2 May 2017 19:58:27 +0000 (19:58 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 2 May 2017 19:58:27 +0000 (19:58 +0000)
1  2 
includes/specialpage/ChangesListSpecialPage.php
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php

@@@ -392,95 -392,6 +392,95 @@@ abstract class ChangesListSpecialPage e
                                        ],
                                ],
                        ],
 +
 +                      [
 +                              'name' => 'watchlist',
 +                              'title' => 'rcfilters-filtergroup-watchlist',
 +                              'class' => ChangesListStringOptionsFilterGroup::class,
 +                              'isFullCoverage' => true,
 +                              'filters' => [
 +                                      [
 +                                              'name' => 'watched',
 +                                              'label' => 'rcfilters-filter-watchlist-watched-label',
 +                                              'description' => 'rcfilters-filter-watchlist-watched-description',
 +                                              'cssClassSuffix' => 'watched',
 +                                              'isRowApplicableCallable' => function ( $ctx, $rc ) {
 +                                                      return $rc->getAttribute( 'wl_user' );
 +                                              }
 +                                      ],
 +                                      [
 +                                              'name' => 'watchednew',
 +                                              'label' => 'rcfilters-filter-watchlist-watchednew-label',
 +                                              'description' => 'rcfilters-filter-watchlist-watchednew-description',
 +                                              'cssClassSuffix' => 'watchednew',
 +                                              'isRowApplicableCallable' => function ( $ctx, $rc ) {
 +                                                      return $rc->getAttribute( 'wl_user' ) &&
 +                                                              $rc->getAttribute( 'rc_timestamp' ) > $rc->getAttribute( 'wl_notificationtimestamp' );
 +                                              },
 +                                      ],
 +                                      [
 +                                              'name' => 'notwatched',
 +                                              'label' => 'rcfilters-filter-watchlist-notwatched-label',
 +                                              'description' => 'rcfilters-filter-watchlist-notwatched-description',
 +                                              'cssClassSuffix' => 'notwatched',
 +                                              'isRowApplicableCallable' => function ( $ctx, $rc ) {
 +                                                      return $rc->getAttribute( 'wl_user' ) === null;
 +                                              },
 +                                      ]
 +                              ],
 +                              'default' => ChangesListStringOptionsFilterGroup::NONE,
 +                              'queryCallable' => function ( $specialPageClassName, $context, $dbr,
 +                                      &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
 +                                      sort( $selectedValues );
 +                                      $notwatchedCond = 'wl_user IS NULL';
 +                                      $watchedCond = 'wl_user IS NOT NULL';
 +                                      $newCond = 'rc_timestamp >= wl_notificationtimestamp';
 +
 +                                      if ( $selectedValues === [ 'notwatched' ] ) {
 +                                              $conds[] = $notwatchedCond;
 +                                              return;
 +                                      }
 +
 +                                      if ( $selectedValues === [ 'watched' ] ) {
 +                                              $conds[] = $watchedCond;
 +                                              return;
 +                                      }
 +
 +                                      if ( $selectedValues === [ 'watchednew' ] ) {
 +                                              $conds[] = $dbr->makeList( [
 +                                                      $watchedCond,
 +                                                      $newCond
 +                                              ], LIST_AND );
 +                                              return;
 +                                      }
 +
 +                                      if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
 +                                              // no filters
 +                                              return;
 +                                      }
 +
 +                                      if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
 +                                              $conds[] = $dbr->makeList( [
 +                                                      $notwatchedCond,
 +                                                      $dbr->makeList( [
 +                                                              $watchedCond,
 +                                                              $newCond
 +                                                      ], LIST_AND )
 +                                              ], LIST_OR );
 +                                              return;
 +                                      }
 +
 +                                      if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
 +                                              $conds[] = $watchedCond;
 +                                              return;
 +                                      }
 +
 +                                      if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
 +                                              // no filters
 +                                              return;
 +                                      }
 +                              },
 +                      ],
                ];
  
                $this->reviewStatusFilterGroupDefinition = [
                        'rcfilters-hideminor-conflicts-typeofchange',
                        'rcfilters-typeofchange-conflicts-hideminor'
                );
 +
 +              $watchlistGroup = $this->getFilterGroup( 'watchlist' );
 +              $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
 +                      $watchlistGroup->getFilter( 'watchednew' )
 +              );
        }
  
        /**
                        }
                }
  
-               $opts->add( 'namespace', '', FormOptions::INTNULL );
+               $opts->add( 'namespace', '', FormOptions::STRING );
                $opts->add( 'invert', false );
                $opts->add( 'associated', false );
  
                }
  
                // Namespace filtering
-               if ( $opts['namespace'] !== '' ) {
-                       $selectedNS = $dbr->addQuotes( $opts['namespace'] );
-                       $operator = $opts['invert'] ? '!=' : '=';
-                       $boolean = $opts['invert'] ? 'AND' : 'OR';
-                       // Namespace association (T4429)
-                       if ( !$opts['associated'] ) {
-                               $condition = "rc_namespace $operator $selectedNS";
-                       } else {
-                               // Also add the associated namespace
-                               $associatedNS = $dbr->addQuotes(
-                                       MWNamespace::getAssociated( $opts['namespace'] )
+               if ( $opts[ 'namespace' ] !== '' ) {
+                       $namespaces = explode( ',', $opts[ 'namespace' ] );
+                       if ( $opts[ 'associated' ] ) {
+                               $associatedNamespaces = array_map(
+                                       function ( $ns ) {
+                                               return MWNamespace::getAssociated( $ns );
+                                       },
+                                       $namespaces
                                );
-                               $condition = "(rc_namespace $operator $selectedNS "
-                                       . $boolean
-                                       . " rc_namespace $operator $associatedNS)";
+                               $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
                        }
  
-                       $conds[] = $condition;
+                       if ( count( $namespaces ) === 1 ) {
+                               $operator = $opts[ 'invert' ] ? '!=' : '=';
+                               $value = $dbr->addQuotes( reset( $namespaces ) );
+                       } else {
+                               $operator = $opts[ 'invert' ] ? 'NOT IN' : 'IN';
+                               sort( $namespaces );
+                               $value = '(' . $dbr->makeList( $namespaces ) . ')';
+                       }
+                       $conds[] = "rc_namespace $operator $value";
                }
        }
  
         * @param array $selectedExpLevels The allowed active values, sorted
         */
        public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr,
 -              &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels ) {
 +              &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0 ) {
  
                global $wgLearnerEdits,
                        $wgExperiencedUserEdits,
                $tables[] = 'user';
                $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
  
 -              $now = time();
 +              if ( $now === 0 ) {
 +                      $now = time();
 +              }
                $secondsPerDay = 86400;
                $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay;
                $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay;
@@@ -109,7 -109,7 +109,7 @@@ class ChangesListSpecialPageTest extend
                        [
                                'namespace' => NS_MAIN,
                        ],
-                       "rc conditions with no options (aka default setting)"
+                       "rc conditions with one namespace"
                );
        }
  
                );
        }
  
-       /**
-        * T4429
-        * @dataProvider provideNamespacesAssociations
-        */
-       public function testRcNsFilterAssociation( $ns1, $ns2 ) {
+       public function testRcNsFilterMultiple() {
                $this->assertConditions(
                        [ # expected
-                               "(rc_namespace = '$ns1' OR rc_namespace = '$ns2')",
+                               "rc_namespace IN ('1','2','3')",
                        ],
                        [
-                               'namespace' => $ns1,
+                               'namespace' => '1,2,3',
+                       ],
+                       "rc conditions with multiple namespaces"
+               );
+       }
+       public function testRcNsFilterMultipleAssociated() {
+               $this->assertConditions(
+                       [ # expected
+                               "rc_namespace IN ('0','1','4','5','6','7')",
+                       ],
+                       [
+                               'namespace' => '1,4,7',
                                'associated' => 1,
                        ],
-                       "rc conditions with namespace inverted"
+                       "rc conditions with multiple namespaces and associated"
                );
        }
  
-       /**
-        * T4429
-        * @dataProvider provideNamespacesAssociations
-        */
-       public function testRcNsFilterAssociationWithInversion( $ns1, $ns2 ) {
+       public function testRcNsFilterMultipleAssociatedInvert() {
                $this->assertConditions(
                        [ # expected
-                               "(rc_namespace != '$ns1' AND rc_namespace != '$ns2')",
+                               "rc_namespace NOT IN ('2','3','8','9')",
                        ],
                        [
-                               'namespace' => $ns1,
+                               'namespace' => '2,3,9',
                                'associated' => 1,
-                               'invert' => 1,
+                               'invert' => 1
                        ],
-                       "rc conditions with namespace inverted"
+                       "rc conditions with multiple namespaces, associated and inverted"
                );
        }
  
-       /**
-        * Provides associated namespaces to test recent changes
-        * namespaces association filtering.
-        */
-       public static function provideNamespacesAssociations() {
-               return [ # (NS => Associated_NS)
-                       [ NS_MAIN, NS_TALK ],
-                       [ NS_TALK, NS_MAIN ],
-               ];
+       public function testRcNsFilterMultipleInvert() {
+               $this->assertConditions(
+                       [ # expected
+                               "rc_namespace NOT IN ('1','2','3')",
+                       ],
+                       [
+                               'namespace' => '1,2,3',
+                               'invert' => 1,
+                       ],
+                       "rc conditions with multiple namespaces inverted"
+               );
        }
  
        public function testRcHidemyselfFilter() {
        }
  
        public function testFilterUserExpLevel() {
 +              $now = time();
                $this->setMwGlobals( [
                        'wgLearnerEdits' => 10,
                        'wgLearnerMemberSince' => 4,
                        'Learner3' => [ 'edits' => 460, 'days' => 33 ],
                        'Learner4' => [ 'edits' => 525, 'days' => 28 ],
                        'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
 -              ] );
 +              ], $now );
  
                // newcomers only
                $this->assertArrayEquals(
                        [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
 -                      $this->fetchUsers( [ 'newcomer' ] )
 +                      $this->fetchUsers( [ 'newcomer' ], $now )
                );
  
                // newcomers and learner
                                'Newcomer1', 'Newcomer2', 'Newcomer3',
                                'Learner1', 'Learner2', 'Learner3', 'Learner4',
                        ],
 -                      $this->fetchUsers( [ 'newcomer', 'learner' ] )
 +                      $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
                );
  
                // newcomers and more learner
                                'Newcomer1', 'Newcomer2', 'Newcomer3',
                                'Experienced1',
                        ],
 -                      $this->fetchUsers( [ 'newcomer', 'experienced' ] )
 +                      $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
                );
  
                // learner only
                $this->assertArrayEquals(
                        [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
 -                      $this->fetchUsers( [ 'learner' ] )
 +                      $this->fetchUsers( [ 'learner' ], $now )
                );
  
                // more experienced only
                $this->assertArrayEquals(
                        [ 'Experienced1' ],
 -                      $this->fetchUsers( [ 'experienced' ] )
 +                      $this->fetchUsers( [ 'experienced' ], $now )
                );
  
                // learner and more experienced
                                'Learner1', 'Learner2', 'Learner3', 'Learner4',
                                'Experienced1',
                        ],
 -                      $this->fetchUsers( [ 'learner', 'experienced' ] ),
 +                      $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
                        'Learner and more experienced'
                );
  
                /*              'Learner1', 'Learner2', 'Learner3', 'Learner4', */
                /*              'Experienced1', */
                /*      ], */
 -              /*      $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ] ) */
 +              /*      $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ], $now ) */
                /* ); */
        }
  
 -      private function createUsers( $specs ) {
 +      private function createUsers( $specs, $now ) {
                $dbw = wfGetDB( DB_MASTER );
                foreach ( $specs as $name => $spec ) {
                        User::createNew(
                                $name,
                                [
                                        'editcount' => $spec['edits'],
 -                                      'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'] ) ),
 +                                      'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
                                        'email' => 'ut',
                                ]
                        );
                }
        }
  
 -      private function fetchUsers( $filters ) {
 +      private function fetchUsers( $filters, $now ) {
                $tables = [];
                $conds = [];
                $fields = [];
                                &$conds,
                                &$query_options,
                                &$join_conds,
 -                              $filters
 +                              $filters,
 +                              $now
                        ]
                );
  
                $result = wfGetDB( DB_MASTER )->select(
 -                      'user',
 +                      $tables,
                        'user_name',
                        array_filter( $conds ) + [ 'user_email' => 'ut' ]
                );
                return $usernames;
        }
  
 -      private function daysAgo( $days ) {
 +      private function daysAgo( $days, $now ) {
                $secondsPerDay = 86400;
 -              return time() - $days * $secondsPerDay;
 +              return $now - $days * $secondsPerDay;
        }
  
        public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {