From: Leszek Manicki Date: Fri, 18 Mar 2016 13:43:26 +0000 (+0100) Subject: Use WatchedItemStore in ApiQueryInfo::getWatchedInfo X-Git-Tag: 1.31.0-rc.0~7553^2 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=b92ae1501e0a3a7e3eaa0cf513f9b76de2cebb38;p=lhc%2Fweb%2Fwiklou.git Use WatchedItemStore in ApiQueryInfo::getWatchedInfo Adds a method for getting watchlist's notification timestamps for a batch of LinkTargets. Bug: T129482 Change-Id: I1f84212e7879a84b34bb3b53859069fcea282bba --- diff --git a/includes/WatchedItemStore.php b/includes/WatchedItemStore.php index 4e2dfa5d99..8a3f205103 100644 --- a/includes/WatchedItemStore.php +++ b/includes/WatchedItemStore.php @@ -502,6 +502,61 @@ class WatchedItemStore { return (bool)$this->getWatchedItem( $user, $target ); } + /** + * @param User $user + * @param LinkTarget[] $targets + * + * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp, + * where $timestamp is: + * - string|null value of wl_notificationtimestamp, + * - false if $target is not watched by $user. + */ + public function getNotificationTimestampsBatch( User $user, array $targets ) { + $timestamps = []; + foreach ( $targets as $target ) { + $timestamps[$target->getNamespace()][$target->getDBkey()] = false; + } + + if ( $user->isAnon() ) { + return $timestamps; + } + + $targetsToLoad = []; + foreach ( $targets as $target ) { + $cachedItem = $this->getCached( $user, $target ); + if ( $cachedItem ) { + $timestamps[$target->getNamespace()][$target->getDBkey()] = + $cachedItem->getNotificationTimestamp(); + } else { + $targetsToLoad[] = $target; + } + } + + if ( !$targetsToLoad ) { + return $timestamps; + } + + $dbr = $this->getConnection( DB_SLAVE ); + + $lb = new LinkBatch( $targetsToLoad ); + $res = $dbr->select( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + $lb->constructSet( 'wl', $dbr ), + 'wl_user' => $user->getId(), + ], + __METHOD__ + ); + $this->reuseConnection( $dbr ); + + foreach ( $res as $row ) { + $timestamps[(int)$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; + } + + return $timestamps; + } + /** * Must be called separately for Subject & Talk namespaces * diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 2d382dd54e..ea1b94e42f 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -448,8 +448,8 @@ class ApiQueryInfo extends ApiQueryBase { ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' ); } - if ( $this->fld_watched ) { - $pageInfo['watched'] = isset( $this->watched[$ns][$dbkey] ); + if ( $this->fld_watched && $this->watched !== null ) { + $pageInfo['watched'] = $this->watched[$ns][$dbkey]; } if ( $this->fld_watchers ) { @@ -470,7 +470,7 @@ class ApiQueryInfo extends ApiQueryBase { if ( $this->fld_notificationtimestamp ) { $pageInfo['notificationtimestamp'] = ''; - if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) { + if ( $this->notificationtimestamps[$ns][$dbkey] ) { $pageInfo['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] ); } @@ -758,30 +758,23 @@ class ApiQueryInfo extends ApiQueryBase { $this->watched = []; $this->notificationtimestamps = []; - $db = $this->getDB(); - - $lb = new LinkBatch( $this->everything ); - $this->resetQueryParams(); - $this->addTables( [ 'watchlist' ] ); - $this->addFields( [ 'wl_title', 'wl_namespace' ] ); - $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp ); - $this->addWhere( [ - $lb->constructSet( 'wl', $db ), - 'wl_user' => $user->getId() - ] ); - - $res = $this->select( __METHOD__ ); + $store = WatchedItemStore::getDefaultInstance(); + $timestamps = $store->getNotificationTimestampsBatch( $user, $this->everything ); - foreach ( $res as $row ) { - if ( $this->fld_watched ) { - $this->watched[$row->wl_namespace][$row->wl_title] = true; - } - if ( $this->fld_notificationtimestamp ) { - $this->notificationtimestamps[$row->wl_namespace][$row->wl_title] = - $row->wl_notificationtimestamp; + if ( $this->fld_watched ) { + foreach ( $timestamps as $namespaceId => $dbKeys ) { + $this->watched[$namespaceId] = array_map( + function( $x ) { + return $x !== false; + }, + $dbKeys + ); } } + if ( $this->fld_notificationtimestamp ) { + $this->notificationtimestamps = $timestamps; + } } /** diff --git a/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php b/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php index 0dea461974..ad5041c956 100644 --- a/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php +++ b/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php @@ -52,6 +52,10 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase { [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => 0 ] ], $store->countWatchersMultiple( [ $title ], [ 'minimumWatchers' => $initialWatchers + 2 ] ) ); + $this->assertEquals( + [ $title->getNamespace() => [ $title->getDBkey() => null ] ], + $store->getNotificationTimestampsBatch( $user, [ $title ] ) + ); $store->removeWatch( $user, $title ); $this->assertFalse( @@ -64,6 +68,10 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase { $initialWatchers, $store->countWatchersMultiple( [ $title ] )[$title->getNamespace()][$title->getDBkey()] ); + $this->assertEquals( + [ $title->getNamespace() => [ $title->getDBkey() => false ] ], + $store->getNotificationTimestampsBatch( $user, [ $title ] ) + ); } public function testUpdateAndResetNotificationTimestamp() { @@ -81,6 +89,10 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase { '20150202010101', $store->loadWatchedItem( $user, $title )->getNotificationTimestamp() ); + $this->assertEquals( + [ $title->getNamespace() => [ $title->getDBkey() => '20150202010101' ] ], + $store->getNotificationTimestampsBatch( $user, [ $title ] ) + ); $this->assertEquals( $initialVisitingWatchers - 1, $store->countVisitingWatchers( $title, '20150202020202' ) @@ -102,6 +114,10 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase { $this->assertTrue( $store->resetNotificationTimestamp( $user, $title ) ); $this->assertNull( $store->getWatchedItem( $user, $title )->getNotificationTimestamp() ); + $this->assertEquals( + [ $title->getNamespace() => [ $title->getDBkey() => null ] ], + $store->getNotificationTimestampsBatch( $user, [ $title ] ) + ); $this->assertEquals( $initialVisitingWatchers, $store->countVisitingWatchers( $title, '20150202020202' ) diff --git a/tests/phpunit/includes/WatchedItemStoreUnitTest.php b/tests/phpunit/includes/WatchedItemStoreUnitTest.php index 869b0d2db0..be5172e7ac 100644 --- a/tests/phpunit/includes/WatchedItemStoreUnitTest.php +++ b/tests/phpunit/includes/WatchedItemStoreUnitTest.php @@ -1481,6 +1481,251 @@ class WatchedItemStoreUnitTest extends PHPUnit_Framework_TestCase { ); } + public function testGetNotificationTimestampsBatch() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + $dbResult = [ + $this->getFakeRow( [ + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + 'wl_notificationtimestamp' => '20151212010101', + ] ), + $this->getFakeRow( + [ + 'wl_namespace' => 1, + 'wl_title' => 'AnotherDbKey', + 'wl_notificationtimestamp' => null, + ] + ), + ]; + + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + 'makeWhereFrom2d return value', + 'wl_user' => 1 + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( $dbResult ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->exactly( 2 ) ) + ->method( 'get' ) + ->withConsecutive( + [ '0:SomeDbKey:1' ], + [ '1:AnotherDbKey:1' ] + ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = new WatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => '20151212010101', ], + 1 => [ 'AnotherDbKey' => null, ], + ], + $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_notWatchedTarget() { + $targets = [ + new TitleValue( 0, 'OtherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'OtherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + 'makeWhereFrom2d return value', + 'wl_user' => 1 + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( $this->getFakeRow( [] ) ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( '0:OtherDbKey:1' ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = new WatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache + ); + + $this->assertEquals( + [ + 0 => [ 'OtherDbKey' => false, ], + ], + $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_cachedItem() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $user = $this->getMockNonAnonUserWithId( 1 ); + $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' ); + + $mockDb = $this->getMockDb(); + + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ 1 => [ 'AnotherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + 'makeWhereFrom2d return value', + 'wl_user' => 1 + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( [ + $this->getFakeRow( + [ 'wl_namespace' => 1, 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ] + ) + ] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->at( 1 ) ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( $cachedItem ) ); + $mockCache->expects( $this->at( 3 ) ) + ->method( 'get' ) + ->with( '1:AnotherDbKey:1' ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = new WatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => '20151212010101', ], + 1 => [ 'AnotherDbKey' => null, ], + ], + $store->getNotificationTimestampsBatch( $user, $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_allItemsCached() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $user = $this->getMockNonAnonUserWithId( 1 ); + $cachedItems = [ + new WatchedItem( $user, $targets[0], '20151212010101' ), + new WatchedItem( $user, $targets[1], null ), + ]; + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() )->method( $this->anything() ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->at( 1 ) ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( $cachedItems[0] ) ); + $mockCache->expects( $this->at( 3 ) ) + ->method( 'get' ) + ->with( '1:AnotherDbKey:1' ) + ->will( $this->returnValue( $cachedItems[1] ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = new WatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => '20151212010101', ], + 1 => [ 'AnotherDbKey' => null, ], + ], + $store->getNotificationTimestampsBatch( $user, $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_anonymousUser() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() )->method( $this->anything() ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( $this->anything() ); + + $store = new WatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => false, ], + 1 => [ 'AnotherDbKey' => false, ], + ], + $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets ) + ); + } + public function testResetNotificationTimestamp_anonymousUser() { $mockDb = $this->getMockDb(); $mockDb->expects( $this->never() )