X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fspecialpage%2FChangesListSpecialPage.php;h=9074d3014513b233fbc4730b5700bfb6a477c0e2;hb=393798a0b3e16181e4df0789c0bba0232b838bc1;hp=8e9629dca4ecb16d42d0d541cd97f42b5fbdc01b;hpb=97fb2ab408f4bba361b5cc87bb9a42bdcb9a370e;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index 8e9629dca4..9074d30145 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -22,6 +22,7 @@ */ use MediaWiki\Logger\LoggerFactory; use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * Special page which uses a ChangesList to show query results. @@ -58,6 +59,13 @@ abstract class ChangesListSpecialPage extends SpecialPage { */ private $filterGroupDefinitions; + // Same format as filterGroupDefinitions, but for a single group (reviewStatus) + // that is registered conditionally. + private $reviewStatusFilterGroupDefinition; + + // Single filter registered conditionally + private $hideCategorizationFilterDefinition; + /** * Filter groups, and their contained filters * This is an associative array (with group name as key) of ChangesListFilterGroup objects. @@ -177,11 +185,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { &$query_options, &$join_conds ) { $user = $ctx->getUser(); - if ( $user->getId() ) { - $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() ); - } else { - $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() ); - } + $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() ); }, 'cssClassSuffix' => 'self', 'isRowApplicableCallable' => function ( $ctx, $rc ) { @@ -197,11 +201,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { &$query_options, &$join_conds ) { $user = $ctx->getUser(); - if ( $user->getId() ) { - $conds[] = 'rc_user = ' . $dbr->addQuotes( $user->getId() ); - } else { - $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() ); - } + $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() ); }, 'cssClassSuffix' => 'others', 'isRowApplicableCallable' => function ( $ctx, $rc ) { @@ -252,57 +252,50 @@ abstract class ChangesListSpecialPage extends SpecialPage { ] ], + // reviewStatus (conditional) + [ - 'name' => 'reviewStatus', - 'title' => 'rcfilters-filtergroup-reviewstatus', + 'name' => 'lastRevision', + 'title' => 'rcfilters-filtergroup-lastRevision', 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -7, 'filters' => [ [ - 'name' => 'hidepatrolled', - 'label' => 'rcfilters-filter-patrolled-label', - 'description' => 'rcfilters-filter-patrolled-description', - // rcshowhidepatr-show, rcshowhidepatr-hide - // wlshowhidepatr - 'showHideSuffix' => 'showhidepatr', + 'name' => 'hidelastrevision', + 'label' => 'rcfilters-filter-lastrevision-label', + 'description' => 'rcfilters-filter-lastrevision-description', 'default' => false, - 'isAllowedCallable' => function ( $pageClassName, $context ) { - return $context->getUser()->useRCPatrol(); - }, 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { - - $conds[] = 'rc_patrolled = 0'; + $conds[] = 'rc_this_oldid <> page_latest'; }, - 'cssClassSuffix' => 'patrolled', + 'cssClassSuffix' => 'last', 'isRowApplicableCallable' => function ( $ctx, $rc ) { - return $rc->getAttribute( 'rc_patrolled' ); - }, + return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' ); + } ], [ - 'name' => 'hideunpatrolled', - 'label' => 'rcfilters-filter-unpatrolled-label', - 'description' => 'rcfilters-filter-unpatrolled-description', + 'name' => 'hidepreviousrevisions', + 'label' => 'rcfilters-filter-previousrevision-label', + 'description' => 'rcfilters-filter-previousrevision-description', 'default' => false, - 'isAllowedCallable' => function ( $pageClassName, $context ) { - return $context->getUser()->useRCPatrol(); - }, 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { - - $conds[] = 'rc_patrolled = 1'; + $conds[] = 'rc_this_oldid = page_latest'; }, - 'cssClassSuffix' => 'unpatrolled', + 'cssClassSuffix' => 'previous', 'isRowApplicableCallable' => function ( $ctx, $rc ) { - return !$rc->getAttribute( 'rc_patrolled' ); - }, - ], - ], + return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' ); + } + ] + ] ], [ 'name' => 'significance', 'title' => 'rcfilters-filtergroup-significance', 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -6, 'filters' => [ [ 'name' => 'hideminor', @@ -351,6 +344,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { 'label' => 'rcfilters-filter-pageedits-label', 'description' => 'rcfilters-filter-pageedits-description', 'default' => false, + 'priority' => -2, 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { @@ -366,6 +360,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { 'label' => 'rcfilters-filter-newpages-label', 'description' => 'rcfilters-filter-newpages-description', 'default' => false, + 'priority' => -3, 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { @@ -376,47 +371,227 @@ abstract class ChangesListSpecialPage extends SpecialPage { return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW; }, ], + + // hidecategorization + + [ + 'name' => 'hidelog', + 'label' => 'rcfilters-filter-logactions-label', + 'description' => 'rcfilters-filter-logactions-description', + 'default' => false, + 'priority' => -5, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG ); + }, + 'cssClassSuffix' => 'src-mw-log', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG; + } + ], + ], + ], + + [ + 'name' => 'watchlist', + 'title' => 'rcfilters-filtergroup-watchlist', + 'class' => ChangesListStringOptionsFilterGroup::class, + 'isFullCoverage' => true, + 'filters' => [ [ - 'name' => 'hidecategorization', - 'label' => 'rcfilters-filter-categorization-label', - 'description' => 'rcfilters-filter-categorization-description', - // rcshowhidecategorization-show, rcshowhidecategorization-hide. - // wlshowhidecategorization - 'showHideSuffix' => 'showhidecategorization', - 'isAllowedCallable' => function ( $pageClassName, $context ) { - return $context->getConfig()->get( 'RCWatchCategoryMembership' ); + '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 = [ + [ + 'name' => 'reviewStatus', + 'title' => 'rcfilters-filtergroup-reviewstatus', + 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -5, + 'filters' => [ + [ + 'name' => 'hidepatrolled', + 'label' => 'rcfilters-filter-patrolled-label', + 'description' => 'rcfilters-filter-patrolled-description', + // rcshowhidepatr-show, rcshowhidepatr-hide + // wlshowhidepatr + 'showHideSuffix' => 'showhidepatr', 'default' => false, 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { - $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ); + $conds[] = 'rc_patrolled = 0'; }, - 'cssClassSuffix' => 'src-mw-categorize', + 'cssClassSuffix' => 'patrolled', 'isRowApplicableCallable' => function ( $ctx, $rc ) { - return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE; + return $rc->getAttribute( 'rc_patrolled' ); }, ], [ - 'name' => 'hidelog', - 'label' => 'rcfilters-filter-logactions-label', - 'description' => 'rcfilters-filter-logactions-description', + 'name' => 'hideunpatrolled', + 'label' => 'rcfilters-filter-unpatrolled-label', + 'description' => 'rcfilters-filter-unpatrolled-description', 'default' => false, 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { - $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG ); + $conds[] = 'rc_patrolled = 1'; }, - 'cssClassSuffix' => 'src-mw-log', + 'cssClassSuffix' => 'unpatrolled', 'isRowApplicableCallable' => function ( $ctx, $rc ) { - return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG; - } + return !$rc->getAttribute( 'rc_patrolled' ); + }, ], ], - ], + ] + ]; + + $this->hideCategorizationFilterDefinition = [ + 'name' => 'hidecategorization', + 'label' => 'rcfilters-filter-categorization-label', + 'description' => 'rcfilters-filter-categorization-description', + // rcshowhidecategorization-show, rcshowhidecategorization-hide. + // wlshowhidecategorization + 'showHideSuffix' => 'showhidecategorization', + 'default' => false, + 'priority' => -4, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ); + }, + 'cssClassSuffix' => 'src-mw-categorize', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE; + }, ]; } + /** + * Check if filters are in conflict and guaranteed to return no results. + * + * @return bool + */ + protected function areFiltersInConflict() { + $opts = $this->getOptions(); + /** @var ChangesListFilterGroup $group */ + foreach ( $this->getFilterGroups() as $group ) { + + if ( $group->getConflictingGroups() ) { + wfLogWarning( + $group->getName() . + " specifies conflicts with other groups but these are not supported yet." + ); + } + + /** @var ChangesListFilter $conflictingFilter */ + foreach ( $group->getConflictingFilters() as $conflictingFilter ) { + if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) { + return true; + } + } + + /** @var ChangesListFilter $filter */ + foreach ( $group->getFilters() as $filter ) { + + /** @var ChangesListFilter $conflictingFilter */ + foreach ( $filter->getConflictingFilters() as $conflictingFilter ) { + if ( + $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) && + $filter->activelyInConflictWithFilter( $conflictingFilter, $opts ) + ) { + return true; + } + } + + } + + } + + return false; + } + /** * Main execution point * @@ -434,6 +609,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { if ( $rows === false ) { if ( !$this->including() ) { $this->doHeader( $opts, 0 ); + $this->outputNoResults(); $this->getOutput()->setStatusCode( 404 ); } @@ -453,7 +629,6 @@ abstract class ChangesListSpecialPage extends SpecialPage { } } $batch->execute(); - $this->webOutput( $rows, $opts ); $rows->free(); @@ -467,6 +642,17 @@ abstract class ChangesListSpecialPage extends SpecialPage { } } + /** + * Add the "no results" message to the output + */ + protected function outputNoResults() { + $this->getOutput()->addHTML( + '
' . + $this->msg( 'recentchanges-noresult' )->parse() . + '
' + ); + } + /** * Get the database result for this special page instance. Used by ApiFeedRecentChanges. * @@ -499,7 +685,8 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Register all filters and their groups, plus conflicts + * Register all filters and their groups (including those from hooks), plus handle + * conflicts and defaults. * * You might want to customize these in the same method, in subclasses. You can * call getFilterGroup to access a group, and (on the group) getFilter to access a @@ -509,6 +696,27 @@ abstract class ChangesListSpecialPage extends SpecialPage { protected function registerFilters() { $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions ); + // Make sure this is not being transcluded (we don't want to show this + // information to all users just because the user that saves the edit can + // patrol) + if ( !$this->including() && $this->getUser()->useRCPatrol() ) { + $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition ); + } + + $changeTypeGroup = $this->getFilterGroup( 'changeType' ); + + if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) { + $transformedHideCategorizationDef = $this->transformFilterDefinition( + $this->hideCategorizationFilterDefinition + ); + + $transformedHideCategorizationDef['group'] = $changeTypeGroup; + + $hideCategorization = new ChangesListBooleanFilter( + $transformedHideCategorizationDef + ); + } + Hooks::run( 'ChangesListSpecialPageStructuredFilters', [ $this ] ); $unstructuredGroupDefinition = @@ -532,7 +740,6 @@ abstract class ChangesListSpecialPage extends SpecialPage { 'rcfilters-filter-unregistered-conflicts-user-experience-level' ); - $changeTypeGroup = $this->getFilterGroup( 'changeType' ); $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' ); $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' ); $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' ); @@ -540,12 +747,15 @@ abstract class ChangesListSpecialPage extends SpecialPage { $significanceTypeGroup = $this->getFilterGroup( 'significance' ); $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' ); - $hideMinorFilter->conflictsWith( - $categoryFilter, - 'rcfilters-hideminor-conflicts-typeofchange-global', - 'rcfilters-hideminor-conflicts-typeofchange', - 'rcfilters-typeofchange-conflicts-hideminor' - ); + // categoryFilter is conditional; see registerFilters + if ( $categoryFilter !== null ) { + $hideMinorFilter->conflictsWith( + $categoryFilter, + 'rcfilters-hideminor-conflicts-typeofchange-global', + 'rcfilters-hideminor-conflicts-typeofchange', + 'rcfilters-typeofchange-conflicts-hideminor' + ); + } $hideMinorFilter->conflictsWith( $logactionsFilter, 'rcfilters-hideminor-conflicts-typeofchange-global', @@ -558,6 +768,24 @@ abstract class ChangesListSpecialPage extends SpecialPage { 'rcfilters-hideminor-conflicts-typeofchange', 'rcfilters-typeofchange-conflicts-hideminor' ); + + $watchlistGroup = $this->getFilterGroup( 'watchlist' ); + $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf( + $watchlistGroup->getFilter( 'watchednew' ) + ); + } + + /** + * Transforms filter definition to prepare it for constructor. + * + * See overrides of this method as well. + * + * @param array $filterDefinition Original filter definition + * + * @return array Transformed definition + */ + protected function transformFilterDefinition( array $filterDefinition ) { + return $filterDefinition; } /** @@ -566,17 +794,27 @@ abstract class ChangesListSpecialPage extends SpecialPage { * Array specifying groups and their filters; see Filter and * ChangesListFilterGroup constructors. * - * There is light processing to simplify core maintenance. See overrides - * of this method as well. + * There is light processing to simplify core maintenance. */ protected function registerFiltersFromDefinitions( array $definition ) { - $priority = -1; + $autoFillPriority = -1; foreach ( $definition as $groupDefinition ) { - $groupDefinition['priority'] = $priority; - $priority--; + if ( !isset( $groupDefinition['priority'] ) ) { + $groupDefinition['priority'] = $autoFillPriority; + } else { + // If it's explicitly specified, start over the auto-fill + $autoFillPriority = $groupDefinition['priority']; + } + + $autoFillPriority--; $className = $groupDefinition['class']; unset( $groupDefinition['class'] ); + + foreach ( $groupDefinition['filters'] as &$filterDefinition ) { + $filterDefinition = $this->transformFilterDefinition( $filterDefinition ); + } + $this->registerFilterGroup( new $className( $groupDefinition ) ); } } @@ -644,6 +882,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { public function getDefaultOptions() { $config = $this->getConfig(); $opts = new FormOptions(); + $structuredUI = $this->getUser()->getOption( 'rcenhancedfilters' ); // Add all filters foreach ( $this->filterGroups as $filterGroup ) { @@ -653,12 +892,12 @@ abstract class ChangesListSpecialPage extends SpecialPage { $opts->add( $filterGroup->getName(), $filterGroup->getDefault() ); } else { foreach ( $filterGroup->getFilters() as $filter ) { - $opts->add( $filter->getName(), $filter->getDefault() ); + $opts->add( $filter->getName(), $filter->getDefault( $structuredUI ) ); } } } - $opts->add( 'namespace', '', FormOptions::INTNULL ); + $opts->add( 'namespace', '', FormOptions::STRING ); $opts->add( 'invert', false ); $opts->add( 'associated', false ); @@ -842,7 +1081,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { $query_options, $join_conds, $opts[$filterGroup->getName()] ); } else { foreach ( $filterGroup->getFilters() as $filter ) { - if ( $opts[$filter->getName()] && $filter->isAllowed( $this ) ) { + if ( $opts[$filter->getName()] ) { $filter->modifyQuery( $dbr, $this, $tables, $fields, $conds, $query_options, $join_conds ); } @@ -851,25 +1090,28 @@ abstract class ChangesListSpecialPage extends SpecialPage { } // 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"; } } @@ -1103,12 +1345,12 @@ abstract class ChangesListSpecialPage extends SpecialPage { * @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, - $wgLearnerMemberSince, - $wgExperiencedUserMemberSince; + $wgExperiencedUserEdits, + $wgLearnerMemberSince, + $wgExperiencedUserMemberSince; $LEVEL_COUNT = 3; @@ -1122,7 +1364,9 @@ abstract class ChangesListSpecialPage extends SpecialPage { $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; @@ -1144,7 +1388,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { ); if ( $selectedExpLevels === [ 'newcomer' ] ) { - $conds[] = "NOT ( $aboveNewcomer )"; + $conds[] = "NOT ( $aboveNewcomer )"; } elseif ( $selectedExpLevels === [ 'learner' ] ) { $conds[] = $dbr->makeList( [ $aboveNewcomer, "NOT ( $aboveLearner )" ],