Bsitu has uploaded a new change for review.
https://gerrit.wikimedia.org/r/153651
Change subject: Add TargetPage model and mapper to notifications
......................................................................
Add TargetPage model and mapper to notifications
This will be used for marking a notificaiton as read when
a user visits a target page. The new table should keep the
volume as low as possible for fast data loopup. records
should be removed from the table once it's marked as read.
Change-Id: I605cbc79adfc12d22bd889c5bb513d05c479fe6e
---
M Echo.php
M Hooks.php
A db_patches/echo_target_page.sql
M echo.sql
A includes/mapper/TargetPageMapper.php
A model/TargetPage.php
A tests/includes/mapper/TargetPageMapperTest.php
A tests/model/TargetPageTest.php
8 files changed, 610 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Echo
refs/changes/51/153651/1
diff --git a/Echo.php b/Echo.php
index ac8f765..30ac6b4 100644
--- a/Echo.php
+++ b/Echo.php
@@ -52,6 +52,7 @@
$wgAutoloadClasses['EchoAbstractEntity'] = $dir . 'model/AbstractEntity.php';
$wgAutoloadClasses['EchoEvent'] = $dir . 'model/Event.php';
$wgAutoloadClasses['EchoNotification'] = $dir . 'model/Notification.php';
+$wgAutoloadClasses['EchoTargetPage'] = $dir . 'model/TargetPage.php';
$wgAutoloadClasses['MWEchoEmailBatch'] = $dir . 'includes/EmailBatch.php';
$wgAutoloadClasses['MWDbEchoEmailBatch'] = $dir . 'includes/DbEmailBatch.php';
$wgAutoloadClasses['MWEchoEmailBundler'] = $dir . 'includes/EmailBundler.php';
@@ -63,6 +64,7 @@
$wgAutoloadClasses['EchoAbstractMapper'] = $dir .
'includes/mapper/AbstractMapper.php';
$wgAutoloadClasses['EchoEventMapper'] = $dir .
'includes/mapper/EventMapper.php';
$wgAutoloadClasses['EchoNotificationMapper'] = $dir .
'includes/mapper/NotificationMapper.php';
+$wgAutoloadClasses['EchoTargetPageMapper'] = $dir .
'includes/mapper/TargetPageMapper.php';
$wgAutoloadClasses['EchoUserNotificationGateway'] = $dir .
'includes/gateway/UserNotificationGateway.php';
// Output formatters
diff --git a/Hooks.php b/Hooks.php
index 8eb741b..8e12509 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -110,6 +110,7 @@
$baseSQLFile = "$dir/echo.sql";
$updater->addExtensionTable( 'echo_event', $baseSQLFile );
$updater->addExtensionTable( 'echo_email_batch',
"$dir/db_patches/echo_email_batch.sql" );
+ $updater->addExtensionTable( 'echo_target_page',
"$dir/db_patches/echo_target_page.sql" );
if ( $updater->getDB()->getType() === 'sqlite' ) {
$updater->modifyExtensionField( 'echo_event',
'event_agent', "$dir/db_patches/patch-event_agent-split.sqlite.sql" );
diff --git a/db_patches/echo_target_page.sql b/db_patches/echo_target_page.sql
new file mode 100644
index 0000000..f6e5c4a
--- /dev/null
+++ b/db_patches/echo_target_page.sql
@@ -0,0 +1,8 @@
+CREATE TABLE /*_*/echo_target_page (
+ etp_user int unsigned not null default 0,
+ etp_page int unsigned not null default 0,
+ etp_event int unsigned not null default 0
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/echo_target_page_user_event ON /*_*/echo_target_page
(etp_user, etp_event);
+CREATE INDEX /*i*/echo_target_page_user_page_event ON /*_*/echo_target_page
(etp_user, etp_page, etp_event);
diff --git a/echo.sql b/echo.sql
index 7b3f388..02871a6 100644
--- a/echo.sql
+++ b/echo.sql
@@ -41,3 +41,12 @@
CREATE UNIQUE INDEX /*i*/echo_email_batch_user_event ON /*_*/echo_email_batch
(eeb_user_id,eeb_event_id);
CREATE INDEX /*i*/echo_email_batch_user_hash_priority ON /*_*/echo_email_batch
(eeb_user_id, eeb_event_hash, eeb_event_priority);
+
+CREATE TABLE /*_*/echo_target_page (
+ etp_user int unsigned not null default 0,
+ etp_page int unsigned not null default 0,
+ etp_event int unsigned not null default 0
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/echo_target_page_user_event ON /*_*/echo_target_page
(etp_user, etp_event);
+CREATE INDEX /*i*/echo_target_page_user_page_event ON /*_*/echo_target_page
(etp_user, etp_page, etp_event);
diff --git a/includes/mapper/TargetPageMapper.php
b/includes/mapper/TargetPageMapper.php
new file mode 100644
index 0000000..9083e44
--- /dev/null
+++ b/includes/mapper/TargetPageMapper.php
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * Database mapper for EchoTargetPage model
+ */
+class EchoTargetPageMapper extends EchoAbstractMapper {
+
+ /**
+ * List of db fields used to construct an EchoTargetPage model
+ * @var string[]
+ */
+ protected static $fields = array(
+ 'etp_user',
+ 'etp_page',
+ 'etp_event'
+ );
+
+ /**
+ * Fetch an EchoTargetPage instance by user & page_id
+ *
+ * @param User $user
+ * @param int $pageId
+ * @return EchoTargetPage[]|boolean
+ */
+ public function fetchByUserPageId( User $user, $pageId ) {
+ $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+ $res = $dbr->select(
+ array( 'echo_target_page' ),
+ self::$fields,
+ array(
+ 'etp_user' => $user->getId(),
+ 'etp_page' => $pageId
+ ),
+ __METHOD__
+ );
+ if ( $res ) {
+ $targetPages = array();
+ foreach ( $res as $row ) {
+ $targetPages[] = EchoTargetPage::newFromRow(
$row );
+ }
+ return $targetPages;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Fetch EchoTargetPage records by user and set of event_id
+ *
+ * @param User $user
+ * @param int[] $eventIds
+ * @return EchoTargetPage[]|boolean
+ */
+ public function fetchByUserEventIds( User $user, array $eventIds ) {
+ if ( !$eventIds ) {
+ return array();
+ }
+ $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+ $res = $dbr->select(
+ array( 'echo_target_page' ),
+ self::$fields,
+ array(
+ 'etp_user' => $user->getId(),
+ 'etp_event' => $eventIds
+ ),
+ __METHOD__
+ );
+ if ( $res ) {
+ $targetPages = array();
+ foreach ( $res as $row ) {
+ $targetPages[] = EchoTargetPage::newFromRow(
$row );
+ }
+ return $targetPages;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Insert an EchoTargetPage instance into the database
+ *
+ * @param EchoTargetPage $targetPage
+ * @return boolean
+ */
+ public function insert( EchoTargetPage $targetPage ) {
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ $row = $targetPage->toDbArray();
+
+ $res = $dbw->insert( 'echo_target_page', $row, __METHOD__ );
+
+ return $res;
+ }
+
+ /**
+ * Delete an EchoTargetPage instance from the database
+ *
+ * @param EchoTargetPage
+ * @return boolean
+ */
+ public function delete( EchoTargetPage $targetPage ) {
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ $res = $dbw->delete(
+ 'echo_target_page',
+ array(
+ 'etp_user' => $targetPage->getUser()->getId(),
+ 'etp_page' => $targetPage->getPageId(),
+ 'etp_event' => $targetPage->getEventId()
+ ),
+ __METHOD__
+ );
+ return $res;
+ }
+
+ /**
+ * Delete multiple EchoTargetPage records by user & set of event_id
+ *
+ * @param User $user
+ * @param int[] $eventIds
+ * @return boolean
+ */
+ public function deleteByUserEvents( User $user, array $eventIds ) {
+ if ( !$eventIds ) {
+ return true;
+ }
+
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ $res = $dbw->delete(
+ 'echo_target_page',
+ array(
+ 'etp_user' => $user->getId(),
+ 'etp_event' => $eventIds
+ ),
+ __METHOD__
+ );
+ return $res;
+ }
+
+ /**
+ * Delete multiple EchoTargetPage records by user
+ *
+ * @param User $user
+ * @return boolean
+ */
+ public function deleteByUser( User $user ) {
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ $res = $dbw->delete(
+ 'echo_target_page',
+ array(
+ 'etp_user' => $user->getId()
+ ),
+ __METHOD__
+ );
+ return $res;
+ }
+
+}
diff --git a/model/TargetPage.php b/model/TargetPage.php
new file mode 100644
index 0000000..cc84660
--- /dev/null
+++ b/model/TargetPage.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * Map a title to an echo notification so that we can mark a notification as
read
+ * when visiting the page. This only supports titles with ids because majority
+ * of notifications have page_id and searching by namespace and title is slow
+ */
+class EchoTargetPage extends EchoAbstractEntity {
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * @var Title|null
+ */
+ protected $title;
+
+ /**
+ * @var int
+ */
+ protected $pageId;
+
+ /**
+ * @var EchoEvent|null
+ */
+ protected $event;
+
+ /**
+ * @var int
+ */
+ protected $eventId;
+
+ /**
+ * Only allow creating instance internally
+ */
+ protected function __construct() {}
+
+ /**
+ * Create a EchoTargetPage instance from User, Title and EchoEvent
+ *
+ * @param User $user
+ * @param Title $title
+ * @param EchoEvent $event
+ * @return TargetPage|null
+ */
+ public static function create( User $user, Title $title, EchoEvent
$event ) {
+ // This only support title with a page_id
+ if ( $user->isAnon() || !$title->getArticleID() ) {
+ return null;
+ }
+ $obj = new self();
+ $obj->user = $user;
+ $obj->event = $event;
+ $obj->eventId = $event->getId();
+ $obj->title = $title;
+ $obj->pageId = $title->getArticleID();
+ return $obj;
+ }
+
+ /**
+ * Create a EchoTargetPage instance from stdClass object
+ *
+ * @param stdClass $row
+ * @return EchoTargetPage
+ * @throws MWException
+ */
+ public static function newFromRow( $row ) {
+ $requiredFields = array (
+ 'etp_user',
+ 'etp_page',
+ 'etp_event'
+ );
+ foreach ( $requiredFields as $field ) {
+ if ( !isset( $row->$field ) || !$row->$field ) {
+ throw new MWException( $field . ' is not set in
the row!' );
+ }
+ }
+ $obj = new self();
+ $obj->user = User::newFromId( $row->etp_user );
+ $obj->pageId = $row->etp_page;
+ $obj->eventId = $row->etp_event;
+ return $obj;
+ }
+
+ /**
+ * @return User
+ */
+ public function getUser() {
+ return $this->user;
+ }
+
+ /**
+ * @return Title|null
+ */
+ public function getTitle() {
+ if ( !$this->title ) {
+ $this->title = Title::newFromId( $this->pageId );
+ }
+ return $this->title;
+ }
+
+ /**
+ * @return int
+ */
+ public function getPageId() {
+ return $this->pageId;
+ }
+
+ /**
+ * @return EchoEvent
+ */
+ public function getEvent() {
+ if ( !$this->event ) {
+ $this->event = EchoEvent::newFromID( $this->eventId );
+ }
+ return $this->event;
+ }
+
+ /**
+ * @return int
+ */
+ public function getEventId() {
+ return $this->eventId;
+ }
+
+ /**
+ * Convert the properties to a database row
+ * @return array
+ */
+ public function toDbArray() {
+ return array (
+ 'etp_user' => $this->user->getId(),
+ 'etp_page' => $this->pageId,
+ 'etp_event' => $this->eventId
+ );
+ }
+}
diff --git a/tests/includes/mapper/TargetPageMapperTest.php
b/tests/includes/mapper/TargetPageMapperTest.php
new file mode 100644
index 0000000..dcf9b35
--- /dev/null
+++ b/tests/includes/mapper/TargetPageMapperTest.php
@@ -0,0 +1,190 @@
+<?php
+
+class EchoTargetPageMapperTest extends MediaWikiTestCase {
+
+ public function testFetchByUserPageId() {
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( array ( 'select' => false ) ) );
+ $res = $targetMapper->fetchByUserPageId( User::newFromId( 1 ),
1 );
+ $this->assertFalse( $res );
+
+ $dbResult = array (
+ (object)array (
+ 'etp_user' => 1,
+ 'etp_page' => 2,
+ 'etp_event' => 3
+ ),
+ (object)array (
+ 'etp_user' => 1,
+ 'etp_page' => 2,
+ 'etp_event' => 7,
+ )
+ );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( array ( 'select' => $dbResult ) ) );
+ $res = $targetMapper->fetchByUserPageId( User::newFromId( 1 ),
2 );
+ $this->assertTrue( is_array( $res ) );
+ foreach ( $res as $row ) {
+ $this->assertInstanceOf( 'EchoTargetPage', $row );
+ }
+ }
+
+ public function testFetchByUserEventIds() {
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( array ( 'select' => false ) ) );
+ $res = $targetMapper->fetchByUserEventIds( User::newFromId( 1
), array( 1, 2, 3 ) );
+ $this->assertFalse( $res );
+
+ $dbResult = array (
+ (object)array (
+ 'etp_user' => 1,
+ 'etp_page' => 2,
+ 'etp_event' => 2
+ ),
+ (object)array (
+ 'etp_user' => 1,
+ 'etp_page' => 2,
+ 'etp_event' => 3,
+ )
+ );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( array ( 'select' => $dbResult ) ) );
+ $res = $targetMapper->fetchByUserEventIds( User::newFromId( 1
), array( 1, 2, 3 ) );
+ $this->assertTrue( is_array( $res ) );
+ $this->assertCount( 2, $res );
+ foreach ( $res as $row ) {
+ $this->assertInstanceOf( 'EchoTargetPage', $row );
+ }
+ }
+
+ public function provideDataTestInsert() {
+ return array (
+ array (
+ 'successful insert with next sequence = 1',
+ array ( 'nextSequenceValue' => 1, 'insert' =>
true, 'insertId' => 2 ),
+ 1
+ ),
+ array (
+ 'successful insert with insert id = 2',
+ array ( 'nextSequenceValue' => null, 'insert'
=> true, 'insertId' => 2 ),
+ 2
+ ),
+ array (
+ 'unsuccessful insert',
+ array ( 'nextSequenceValue' => null, 'insert'
=> false, 'insertId' => 2 ),
+ false
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDataTestInsert
+ */
+ public function testInsert( $message, $dbResult, $result ) {
+ $target = $this->mockEchoTargetPage();
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( $dbResult ) );
+ $this->assertEquals( $result, $targetMapper->insert( $target ),
$message );
+ }
+
+ public function testDelete() {
+ $dbResult = array( 'delete' => true );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( $dbResult ) );
+ $this->assertTrue( $targetMapper->delete(
$this->mockEchoTargetPage() ) );
+
+ $dbResult = array( 'delete' => false );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( $dbResult ) );
+ $this->assertFalse( $targetMapper->delete(
$this->mockEchoTargetPage() ) );
+ }
+
+ public function provideDataTestDeleteByUserEvents() {
+ return array(
+ array( true, array( 1 ), true ),
+ array( false, array( 1 ), false ),
+ array( true, array(), true ),
+ array( false, array(), true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDataTestDeleteByUserEvents
+ */
+ public function testDeleteByUserEvents( $deleteResult, $eventIds,
$result ) {
+ $dbResult = array( 'delete' => $deleteResult );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( $dbResult ) );
+ $this->assertSame( $targetMapper->deleteByUserEvents(
User::newFromId( 1 ), $eventIds ), $result );
+ }
+
+ public function testDeleteByUser() {
+ $dbResult = array( 'delete' => true );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( $dbResult ) );
+ $this->assertSame( $targetMapper->deleteByUser(
User::newFromId( 1 ) ), true );
+
+ $dbResult = array( 'delete' => false );
+ $targetMapper = new EchoTargetPageMapper(
$this->mockMWEchoDbFactory( $dbResult ) );
+ $this->assertSame( $targetMapper->deleteByUser(
User::newFromId( 1 ) ), false );
+ }
+
+ /**
+ * Mock object of EchoTargetPage
+ */
+ protected function mockEchoTargetPage() {
+ $target = $this->getMockBuilder( 'EchoTargetPage' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $target->expects( $this->any() )
+ ->method( 'toDbArray' )
+ ->will( $this->returnValue( array() ) );
+ $target->expects( $this->any() )
+ ->method( 'getUser' )
+ ->will( $this->returnValue( User::newFromId( 1 ) ) );
+ $target->expects( $this->any() )
+ ->method( 'getPageId' )
+ ->will( $this->returnValue( 2 ) );
+ $target->expects( $this->any() )
+ ->method( 'getEventId' )
+ ->will( $this->returnValue( 3 ) );
+ return $target;
+ }
+
+ /**
+ * Mock object of MWEchoDbFactory
+ */
+ protected function mockMWEchoDbFactory( $dbResult ) {
+ $dbFactory = $this->getMockBuilder( 'MWEchoDbFactory' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $dbFactory->expects( $this->any() )
+ ->method( 'getEchoDb' )
+ ->will( $this->returnValue( $this->mockDb( $dbResult )
) );
+ return $dbFactory;
+ }
+
+ /**
+ * Mock object of DatabaseMysql ( DatabaseBase )
+ */
+ protected function mockDb( array $dbResult ) {
+ $dbResult += array(
+ 'nextSequenceValue' => '',
+ 'insert' => '',
+ 'insertId' => '',
+ 'select' => '',
+ 'delete' => ''
+ );
+ $db = $this->getMockBuilder( 'DatabaseMysql' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $db->expects( $this->any() )
+ ->method( 'nextSequenceValue' )
+ ->will( $this->returnValue(
$dbResult['nextSequenceValue'] ) );
+ $db->expects( $this->any() )
+ ->method( 'insert' )
+ ->will( $this->returnValue( $dbResult['insert'] ) );
+ $db->expects( $this->any() )
+ ->method( 'insertId' )
+ ->will( $this->returnValue( $dbResult['insertId'] ) );
+ $db->expects( $this->any() )
+ ->method( 'select' )
+ ->will( $this->returnValue( $dbResult['select'] ) );
+ $db->expects( $this->any() )
+ ->method( 'delete' )
+ ->will( $this->returnValue( $dbResult['delete'] ) );
+ return $db;
+ }
+
+}
diff --git a/tests/model/TargetPageTest.php b/tests/model/TargetPageTest.php
new file mode 100644
index 0000000..29670ff
--- /dev/null
+++ b/tests/model/TargetPageTest.php
@@ -0,0 +1,99 @@
+<?php
+
+class EchoTargetPageTest extends MediaWikiTestCase {
+
+ public function testCreate() {
+ $this->assertNull(
+ EchoTargetPage::create(
+ User::newFromId( 0 ),
+ $this->mockTitle( 1 ),
+ $this->mockEchoEvent()
+ )
+ );
+
+ $this->assertNull(
+ EchoTargetPage::create(
+ User::newFromId( 1 ),
+ $this->mockTitle( 0 ),
+ $this->mockEchoEvent()
+ )
+ );
+
+ $this->assertNull(
+ EchoTargetPage::create(
+ User::newFromId( 0 ),
+ $this->mockTitle( 0 ),
+ $this->mockEchoEvent()
+ )
+ );
+
+ $this->assertInstanceOf(
+ 'EchoTargetPage',
+ EchoTargetPage::create(
+ User::newFromId( 1 ),
+ $this->mockTitle( 1 ),
+ $this->mockEchoEvent()
+ )
+ );
+ }
+
+ public function testNewFromRow() {
+ $row = (object) array (
+ 'etp_user' => 1,
+ 'etp_page' => 2,
+ 'etp_event' => 3
+ );
+ $obj = EchoTargetPage::newFromRow( $row );
+ $this->assertInstanceOf( 'EchoTargetPage', $obj );
+ return $obj;
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testNewFromRowWithException() {
+ $row = (object) array (
+ 'etp_page' => 2,
+ 'etp_event' => 3
+ );
+ $this->assertInstanceOf( 'EchoTargetPage',
EchoTargetPage::newFromRow( $row ) );
+ }
+
+ /**
+ * @depends testNewFromRow
+ */
+ public function testToDbArray( $obj ) {
+ $row = $obj->toDbArray();
+ $this->assertTrue( is_array( $row ) );
+ $this->assertArrayHasKey( 'etp_user', $row );
+ $this->assertArrayHasKey( 'etp_page', $row );
+ $this->assertArrayHasKey( 'etp_event', $row );
+ }
+
+ /**
+ * Mock object of Title
+ */
+ protected function mockTitle( $pageId ) {
+ $event = $this->getMockBuilder( 'Title' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $event->expects( $this->any() )
+ ->method( 'getArticleID' )
+ ->will( $this->returnValue( $pageId ) );
+ return $event;
+ }
+
+ /**
+ * Mock object of EchoEvent
+ */
+ protected function mockEchoEvent( $eventId = 1 ) {
+ $event = $this->getMockBuilder( 'EchoEvent' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $event->expects( $this->any() )
+ ->method( 'getId' )
+ ->will( $this->returnValue( $eventId ) );
+ return $event;
+ }
+
+}
--
To view, visit https://gerrit.wikimedia.org/r/153651
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I605cbc79adfc12d22bd889c5bb513d05c479fe6e
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: two_tabs
Gerrit-Owner: Bsitu <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits