],
],
],
+
+ [
+ '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;
[
'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() {