jenkins-bot has submitted this change and it was merged.
Change subject: Replace EchoBackend with mappers and gateway
......................................................................
Replace EchoBackend with mappers and gateway
* Get rid of EchoBackend by separating responsibilities into smaller objects
* Move main fetchNotification logic from API to a more appropriate place
* Add more unit testing coverage
Change-Id: I42f4d7566543332588431c21c220c0d64d026b70
---
M Echo.php
M Hooks.php
M api/ApiEchoNotifications.php
M formatters/BasicFormatter.php
M formatters/PageLinkFormatter.php
A includes/DataOutputFormatter.php
D includes/DbEchoBackend.php
D includes/EchoBackend.php
M includes/EchoDbFactory.php
M includes/NotifUser.php
A includes/gateway/UserNotificationGateway.php
A includes/mapper/EventMapper.php
A includes/mapper/NotificationMapper.php
M model/Event.php
M model/Notification.php
M special/SpecialNotifications.php
A tests/includes/EchoDbFactoryTest.php
A tests/includes/gateway/UserNotificationGatewayTest.php
A tests/includes/mapper/EventMapperTest.php
A tests/includes/mapper/NotificationMapperTest.php
20 files changed, 1,363 insertions(+), 663 deletions(-)
Approvals:
EBernhardson: Looks good to me, approved
jenkins-bot: Verified
diff --git a/Echo.php b/Echo.php
index 2568095..8fb2dad 100644
--- a/Echo.php
+++ b/Echo.php
@@ -47,6 +47,7 @@
$wgExtensionMessagesFiles['Echo'] = $dir . 'Echo.i18n.php';
$wgExtensionMessagesFiles['EchoAliases'] = $dir . 'Echo.alias.php';
+// Basic Echo classes
$wgAutoloadClasses['EchoHooks'] = $dir . 'Hooks.php';
$wgAutoloadClasses['EchoEvent'] = $dir . 'model/Event.php';
$wgAutoloadClasses['EchoNotification'] = $dir . 'model/Notification.php';
@@ -56,7 +57,15 @@
$wgAutoloadClasses['MWDbEchoEmailBundler'] = $dir .
'includes/DbEmailBundler.php';
$wgAutoloadClasses['MWEchoEventLogging'] = $dir . 'includes/EventLogging.php';
-// Formatters
+// Database mappers && gateways
+$wgAutoloadClasses['EchoEventMapper'] = $dir .
'includes/mapper/EventMapper.php';
+$wgAutoloadClasses['EchoNotificationMapper'] = $dir .
'includes/mapper/NotificationMapper.php';
+$wgAutoloadClasses['EchoUserNotificationGateway'] = $dir .
'includes/gateway/UserNotificationGateway.php';
+
+// Output formatters
+$wgAutoloadClasses['EchoDataOutputFormatter'] = $dir .
'includes/DataOutputFormatter.php';
+
+// Event formatters
$wgAutoloadClasses['EchoNotificationFormatter'] = $dir .
'formatters/NotificationFormatter.php';
$wgAutoloadClasses['EchoBasicFormatter'] = $dir .
'formatters/BasicFormatter.php';
$wgAutoloadClasses['EchoEditFormatter'] = $dir .
'formatters/EditFormatter.php';
@@ -101,8 +110,6 @@
$wgSpecialPageGroups['Notifications'] = 'users';
// Backend support
-$wgAutoloadClasses['MWEchoBackend'] = $dir . 'includes/EchoBackend.php';
-$wgAutoloadClasses['MWDbEchoBackend'] = $dir . 'includes/DbEchoBackend.php';
$wgAutoloadClasses['MWEchoDbFactory'] = $dir . 'includes/EchoDbFactory.php';
$wgAutoloadClasses['MWEchoNotifUser'] = $dir . 'includes/NotifUser.php';
@@ -248,14 +255,11 @@
// Configuration
-// The name of the backend to use for Echo, eg, Db, Redis, Zeromq
+// The name of the backend to use for Echo email bundling and digest, it should
+// be always Db
+// @deprecated
+// @todo remove it from code base
$wgEchoBackendName = 'Db';
-
-/**
- * The backend object
- * @var MWEchoBackend
- */
-$wgEchoBackend = null;
// Whether to turn on email batch function
$wgEchoEnableEmailBatch = true;
diff --git a/Hooks.php b/Hooks.php
index 68ab9e8..a4f703b 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -13,14 +13,11 @@
* from $wgExtensionFunctions
*/
public static function initEchoExtension() {
- global $wgEchoBackend, $wgEchoBackendName, $wgEchoNotifications,
- $wgEchoNotificationCategories,
$wgEchoNotificationIcons, $wgEchoConfig,
- $wgNotificationSenderName;
+ global $wgEchoNotifications, $wgEchoNotificationCategories,
$wgEchoNotificationIcons,
+ $wgEchoConfig, $wgNotificationSenderName;
// allow extensions to define their own event
wfRunHooks( 'BeforeCreateEchoEvent', array(
&$wgEchoNotifications, &$wgEchoNotificationCategories,
&$wgEchoNotificationIcons ) );
-
- $wgEchoBackend = MWEchoBackend::factory( $wgEchoBackendName );
// turn schema off if eventLogging is not enabled
if ( !function_exists( 'efLogServerSideEvent' ) ) {
diff --git a/api/ApiEchoNotifications.php b/api/ApiEchoNotifications.php
index 91f0171..9e85e86 100644
--- a/api/ApiEchoNotifications.php
+++ b/api/ApiEchoNotifications.php
@@ -23,7 +23,12 @@
$result = array();
if ( in_array( 'list', $prop ) ) {
- $result['list'] = self::getNotifications( $user,
$params['format'], $params['limit'] + 1, $params['continue'] );
+ $result['list'] = array();
+ $notifMapper = new EchoNotificationMapper(
MWEchoDbFactory::newFromDefault() );
+ $notifs = $notifMapper->fetchByUser( $user,
$params['limit'] + 1, $params['continue'], 'web' );
+ foreach ( $notifs as $notif ) {
+ $result['list'][$notif->getEvent()->getID()] =
EchoDataOutputFormatter::formatOutput( $notif, $params['format'], $user );
+ }
// check if there is more elements than we request
if ( count( $result['list'] ) > $params['limit'] ) {
@@ -54,116 +59,6 @@
$this->getResult()->setIndexedTagName( $result, 'notification'
);
$this->getResult()->addValue( 'query', $this->getModuleName(),
$result );
- }
-
- /**
- * Get a list of notifications based on the passed parameters
- *
- * @param $user User the user to get notifications for
- * @param $format string|bool false to not format any notifications,
string to a specific output format
- * @param $limit int The maximum number of notifications to return
- * @param $continue string Used for offset
- *
- * @return array
- */
- public static function getNotifications( $user, $format = false, $limit
= 20, $continue = null ) {
- global $wgEchoBackend;
-
- $output = array();
-
- // TODO: Make 'web' based on a new API param?
- $res = $wgEchoBackend->loadNotifications( $user, $limit,
$continue, 'web' );
-
- foreach ( $res as $row ) {
- $event = EchoEvent::newFromRow( $row );
- if ( $row->notification_bundle_base &&
$row->notification_bundle_display_hash ) {
- $event->setBundleHash(
$row->notification_bundle_display_hash );
- }
-
- $timestampMw = self::getUserLocalTime( $user,
$row->notification_timestamp );
-
- // Start creating date section header
- $now = wfTimestamp();
- $dateFormat = substr( $timestampMw, 0, 8 );
- if ( substr( self::getUserLocalTime( $user, $now ), 0,
8 ) === $dateFormat ) {
- // 'Today'
- $date = wfMessage( 'echo-date-today'
)->escaped();
- } elseif ( substr( self::getUserLocalTime( $user, $now
- 86400 ), 0, 8 ) === $dateFormat ) {
- // 'Yesterday'
- $date = wfMessage( 'echo-date-yesterday'
)->escaped();
- } else {
- // 'May 10' or '10 May' (depending on user's
date format preference)
- $lang =
RequestContext::getMain()->getLanguage();
- $dateFormat = $lang->getDateFormatString(
'pretty', $user->getDatePreference() ?: 'default' );
- $date = $lang->sprintfDate( $dateFormat,
$timestampMw );
- }
- // End creating date section header
-
- $thisEvent = array(
- 'id' => $event->getId(),
- 'type' => $event->getType(),
- 'category' => $event->getCategory(),
- 'timestamp' => array(
- // UTC timestamp in UNIX format used
for loading more notification
- 'utcunix' => wfTimestamp( TS_UNIX,
$row->notification_timestamp ),
- 'unix' => self::getUserLocalTime(
$user, $row->notification_timestamp, TS_UNIX ),
- 'mw' => $timestampMw,
- 'date' => $date
- ),
- );
-
- if ( $event->getVariant() ) {
- $thisEvent['variant'] = $event->getVariant();
- }
-
- if ( $event->getTitle() ) {
- $thisEvent['title'] = array(
- 'full' =>
$event->getTitle()->getPrefixedText(),
- 'namespace' =>
$event->getTitle()->getNSText(),
- 'namespace-key' =>
$event->getTitle()->getNamespace(),
- 'text' => $event->getTitle()->getText(),
- );
- }
-
- if ( $event->getAgent() ) {
- if ( $event->userCan( Revision::DELETED_USER,
$user ) ) {
- $thisEvent['agent'] = array(
- 'id' =>
$event->getAgent()->getId(),
- 'name' =>
$event->getAgent()->getName(),
- );
- } else {
- $thisEvent['agent'] = array(
'userhidden' => '' );
- }
- }
-
- if ( $row->notification_read_timestamp ) {
- $thisEvent['read'] =
$row->notification_read_timestamp;
- }
-
- if ( $format ) {
- $thisEvent['*'] =
EchoNotificationController::formatNotification(
- $event, $user, $format );
- }
-
- $output[$event->getID()] = $thisEvent;
- }
-
- return $output;
- }
-
- /**
- * Internal helper function for converting UTC timezone to a user's
timezone
- *
- * @param $user User
- * @param $ts string
- * @param $format int output format
- *
- * @return string
- */
- private static function getUserLocalTime( $user, $ts, $format = TS_MW )
{
- $timestamp = new MWTimestamp( $ts );
- $timestamp->offsetForUser( $user );
- return $timestamp->getTimestamp( $format );
}
public function getAllowedParams() {
diff --git a/formatters/BasicFormatter.php b/formatters/BasicFormatter.php
index d6a7f3c..53872e6 100644
--- a/formatters/BasicFormatter.php
+++ b/formatters/BasicFormatter.php
@@ -545,40 +545,44 @@
/**
* Get raw bundle data for an event so it can be manipulated
- * @param $event EchoEvent
- * @param $user User
- * @param $type string deprecated
- * @return ResultWrapper|bool
+ * @param EchoEvent
+ * @param User
+ * @param string deprecated
+ * @return EchoEvent[]|bool
*/
protected function getRawBundleData( $event, $user, $type ) {
- global $wgEchoBackend;
-
- // We should keep bundling for events as long as it has bundle
- // hash event for bundle-turned-off events as well, this is
- // mainly for historical data
+ // We should keep bundling for events as long as it has bundle
hash
+ // even for events with bundling switched to off, this is
mainly for
+ // historical data
if ( !$event->getBundleHash() ) {
return false;
}
- $data = $wgEchoBackend->getRawBundleData( $user,
$event->getBundleHash(), $this->distributionType, 'DESC',
self::$maxRawBundleData );
+ $eventMapper = new EchoEventMapper(
MWEchoDbFactory::newFromDefault() );
+ $events = $eventMapper->fetchByUserBundleHash(
+ $user, $event->getBundleHash(),
$this->distributionType, 'DESC', self::$maxRawBundleData
+ );
- if ( $data ) {
- $this->bundleData['raw-data-count'] += $data->numRows();
+ if ( $events ) {
+ $this->bundleData['raw-data-count'] += count( $events );
+ // Distribution types other than web include the base
event
+ // in the result already, decrement it by one
if ( $this->distributionType !== 'web' ) {
$this->bundleData['raw-data-count']--;
}
}
- return $data;
+ return $events;
}
/**
* Construct the bundle data for an event, by default, the group
iterator
* is agent, eg, by user A and x others. custom formatter can overwrite
* this function to use a differnt group iterator such as title,
namespace
- * @param $event EchoEvent
- * @param $user User
- * @param $type string deprecated
+ *
+ * @param EchoEvent
+ * @param User
+ * @param string deprecated
* @throws MWException
*/
protected function generateBundleData( $event, $user, $type ) {
@@ -606,13 +610,19 @@
// Initialize with 1 for the agent of current event
$count = 1;
- foreach ( $data as $row ) {
- $key = $row->event_agent_id ? 'event_agent_id' :
'event_agent_ip';
- if ( !isset( $agents[$row->$key] ) ) {
- $agents[$row->$key] = $row->$key;
- $count++;
+ foreach ( $data as $evt ) {
+ if ( $evt->getAgent() ) {
+ if ( $evt->getAgent()->isAnon() ) {
+ $key = $evt->getAgent()->getName();
+ } else {
+ $key = $evt->getAgent()->getId();
+ }
+ if ( !isset( $agents[$key] ) ) {
+ $agents[$key] = $key;
+ $count++;
+ }
}
- $this->bundleData['last-raw-data'] = $row;
+ $this->bundleData['last-raw-data'] = $evt;
}
$this->bundleData['agent-other-count'] = $count - 1;
@@ -622,7 +632,7 @@
// If there is more raw data than we requested, that means we
have not
// retrieved the very last raw record, set the key back to null
- if ( $data->numRows() >= self::$maxRawBundleData ) {
+ if ( count( $data ) >= self::$maxRawBundleData ) {
$this->bundleData['last-raw-data'] = null;
}
}
@@ -753,7 +763,7 @@
$data = $this->getBundleLastRawData(
$event, $user );
if ( $data ) {
- $extra = $data->event_extra;
+ $extra = $data->getExtra();
if ( isset( $extra['revid'] ) )
{
$oldId =
$target->getPreviousRevisionID( $extra['revid'] );
// The diff engine
doesn't provide a way to diff against a null revision.
@@ -773,31 +783,32 @@
}
/**
- * Get the last bundle data in raw stdObject format. When bundling
notifications,
+ * Get the last echo event in a set of bundling data. When bundling
notifications,
* we mostly only need the very first notification, which is the bundle
base.
* In some cases, like talk notification diff, Flow notificaiton first
unread post,
* we need data from the very last notification.
*
* @param EchoEvent
* @param User
- * @return stdObject|boolean false for none
+ * @return EchoEvent|boolean false for none
*/
- protected function getBundleLastRawData( $event, $user ) {
+ protected function getBundleLastRawData( EchoEvent $event, User $user )
{
if ( $event->getBundleHash() ) {
// First try cache data from preivous query
if ( isset( $this->bundleData['last-raw-data'] ) ) {
$data = $this->bundleData['last-raw-data'];
// Then try to query the storage
} else {
- global $wgEchoBackend;
- $data = $wgEchoBackend->getRawBundleData(
$user, $event->getBundleHash(), $this->distributionType, 'ASC', 1 );
+ $eventMapper = new EchoEventMapper(
MWEchoDbFactory::newFromDefault() );
+ $data = $eventMapper->fetchByUserBundleHash(
+ $user, $event->getBundleHash(),
$this->distributionType, 'ASC', 1
+ );
if ( $data ) {
- $data = $data->current();
+ $data = reset( $data );
}
}
if ( $data ) {
- $data->event_extra = $data->event_extra ?
unserialize( $data->event_extra ) : array();
return $data;
}
}
diff --git a/formatters/PageLinkFormatter.php b/formatters/PageLinkFormatter.php
index f87f920..80fbc45 100644
--- a/formatters/PageLinkFormatter.php
+++ b/formatters/PageLinkFormatter.php
@@ -35,6 +35,7 @@
* This method overwrite parent method and construct the bundle iterator
* based on link from, it will be used in a message like this: Page A
was
* link from Page B and X other pages
+ *
* @param $event EchoEvent
* @param $user User
* @param $type string deprecated
@@ -58,8 +59,8 @@
$linkFrom = array(
$extra['link-from-page-id'] => true
);
- foreach ( $data as $row ) {
- $extra = $row->event_extra ? unserialize(
$row->event_extra ) : null;
+ foreach ( $data as $bundledEvent ) {
+ $extra = $bundledEvent->getExtra();
if ( !$extra ) {
continue;
}
diff --git a/includes/DataOutputFormatter.php b/includes/DataOutputFormatter.php
new file mode 100644
index 0000000..5224737
--- /dev/null
+++ b/includes/DataOutputFormatter.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * Utility class that formats a notification in the format specified
+ */
+class EchoDataOutputFormatter {
+
+ /**
+ * Format a notification for a user in the format specified
+ *
+ * @param string|bool specifify output format, false to not format any
notifications
+ * @param User|null the target user viewing the notification
+ * @return array
+ */
+ public function formatOutput( EchoNotification $notification, $format =
false, User $user = null ) {
+ $event = $notification->getEvent();
+ // Default to notification user if user is not specified
+ if ( !$user ) {
+ $user = $notification->getUser();
+ }
+
+ if ( $notification->getBundleBase() &&
$notification->getBundleDisplayHash() ) {
+ $event->setBundleHash(
$notification->getBundleDisplayHash() );
+ }
+
+ $timestampMw = self::getUserLocalTime( $user,
$notification->getTimestamp() );
+
+ // Start creating date section header
+ $now = wfTimestamp();
+ $dateFormat = substr( $timestampMw, 0, 8 );
+ if ( substr( self::getUserLocalTime( $user, $now ), 0, 8 ) ===
$dateFormat ) {
+ // 'Today'
+ $date = wfMessage( 'echo-date-today' )->escaped();
+ } elseif ( substr( self::getUserLocalTime( $user, $now - 86400
), 0, 8 ) === $dateFormat ) {
+ // 'Yesterday'
+ $date = wfMessage( 'echo-date-yesterday' )->escaped();
+ } else {
+ // 'May 10' or '10 May' (depending on user's date
format preference)
+ $lang = RequestContext::getMain()->getLanguage();
+ $dateFormat = $lang->getDateFormatString( 'pretty',
$user->getDatePreference() ?: 'default' );
+ $date = $lang->sprintfDate( $dateFormat, $timestampMw );
+ }
+ // End creating date section header
+
+ $output = array(
+ 'id' => $event->getId(),
+ 'type' => $event->getType(),
+ 'category' => $event->getCategory(),
+ 'timestamp' => array(
+ // UTC timestamp in UNIX format used for
loading more notification
+ 'utcunix' => wfTimestamp( TS_UNIX,
$notification->getTimestamp() ),
+ 'unix' => self::getUserLocalTime( $user,
$notification->getTimestamp(), TS_UNIX ),
+ 'mw' => $timestampMw,
+ 'date' => $date
+ ),
+ );
+
+ if ( $event->getVariant() ) {
+ $output['variant'] = $event->getVariant();
+ }
+
+ if ( $event->getTitle() ) {
+ $output['title'] = array(
+ 'full' => $event->getTitle()->getPrefixedText(),
+ 'namespace' => $event->getTitle()->getNSText(),
+ 'namespace-key' =>
$event->getTitle()->getNamespace(),
+ 'text' => $event->getTitle()->getText(),
+ );
+ }
+
+ if ( $event->getAgent() ) {
+ if ( $event->userCan( Revision::DELETED_USER, $user ) )
{
+ $output['agent'] = array(
+ 'id' => $event->getAgent()->getId(),
+ 'name' => $event->getAgent()->getName(),
+ );
+ } else {
+ $output['agent'] = array( 'userhidden' => '' );
+ }
+ }
+
+ if ( $notification->getReadTimestamp() ) {
+ $output['read'] = $notification->getReadTimestamp();
+ }
+
+ if ( $format ) {
+ $output['*'] =
EchoNotificationController::formatNotification( $event, $user, $format );
+ }
+
+ return $output;
+ }
+
+ /**
+ * Helper function for converting UTC timezone to a user's timezone
+ *
+ * @param User
+ * @param string
+ * @param int output format
+ *
+ * @return string
+ */
+ public static function getUserLocalTime( User $user, $ts, $format =
TS_MW ) {
+ $timestamp = new MWTimestamp( $ts );
+ $timestamp->offsetForUser( $user );
+ return $timestamp->getTimestamp( $format );
+ }
+
+}
diff --git a/includes/DbEchoBackend.php b/includes/DbEchoBackend.php
deleted file mode 100644
index cf457e6..0000000
--- a/includes/DbEchoBackend.php
+++ /dev/null
@@ -1,312 +0,0 @@
-<?php
-
-/**
- * Database backend for echo notification
- */
-class MWDbEchoBackend extends MWEchoBackend {
-
- /**
- * @param $row array
- */
- public function createNotification( $row ) {
- $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
- $fname = __METHOD__;
- $dbw->onTransactionIdle(
- function() use ( $dbw, $row, $fname ) {
- $dbw->startAtomic( $fname );
- // reset the base if this notification has a
display hash
- if ( $row['notification_bundle_display_hash'] )
{
- $dbw->update(
- 'echo_notification',
- array(
'notification_bundle_base' => 0 ),
- array(
- 'notification_user' =>
$row['notification_user'],
-
'notification_bundle_display_hash' => $row['notification_bundle_display_hash'],
-
'notification_bundle_base' => 1
- ),
- $fname
- );
- }
-
- $row['notification_timestamp'] =
$dbw->timestamp( $row['notification_timestamp'] );
- $dbw->insert( 'echo_notification', $row, $fname
);
- $dbw->endAtomic( $fname );
-
- $user = User::newFromId(
$row['notification_user'] );
- MWEchoNotifUser::newFromUser( $user
)->resetNotificationCount( DB_MASTER );
- }
- );
- }
-
- /**
- * @param $user User the user to get notifications for
- * @param $limit int The maximum number of notifications to return
- * @param $continue string Used for offset
- * @param $outputFormat string The output format of the notifications
(web,
- * email, etc.)
- * @return array
- */
- public function loadNotifications( $user, $limit, $continue,
$outputFormat = 'web' ) {
- $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-
- $eventTypesToLoad =
EchoNotificationController::getUserEnabledEvents( $user, $outputFormat );
- if ( !$eventTypesToLoad ) {
- return array();
- }
-
- // Look for notifications with base = 1
- $conds = array(
- 'notification_user' => $user->getID(),
- 'event_type' => $eventTypesToLoad,
- 'notification_bundle_base' => 1
- );
-
- $offset = $this->extractQueryOffset( $continue );
-
- // Start points are specified
- if ( $offset['timestamp'] && $offset['offset'] ) {
- $ts = $dbr->addQuotes( $dbr->timestamp(
$offset['timestamp'] ) );
- // The offset and timestamp are those of the first
notification we want to return
- $conds[] = "notification_timestamp < $ts OR (
notification_timestamp = $ts AND notification_event <= " . $offset['offset'] .
" )";
- }
-
- $res = $dbr->select(
- array( 'echo_notification', 'echo_event' ),
- '*',
- $conds,
- __METHOD__,
- array(
- 'ORDER BY' => 'notification_timestamp DESC,
notification_event DESC',
- 'LIMIT' => $limit,
- ),
- array(
- 'echo_event' => array( 'LEFT JOIN',
'notification_event=event_id' ),
- )
- );
-
- return iterator_to_array( $res, false );
- }
-
- /**
- * @param $user User
- * @param $bundleHash string the bundle hash
- * @param $type string
- * @param $order string 'ASC'/'DESC'
- * @param $limit int
- * @return ResultWrapper|bool
- */
- public function getRawBundleData( $user, $bundleHash, $type = 'web',
$order = 'DESC', $limit = 250 ) {
- $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-
- // We only display 99+ if the number is over 100, we can do
limit 250, this should be sufficient
- // to return 99 distinct group iterators, avoid select count(
distinct ) for the following reason:
- // 1. it will not scale for large volume data
- // 2. notification may have random grouping iterator
- // 3. agent may be anonymous, can't do distinct over two
columns: event_agent_id and event_agent_ip
- if ( $type == 'web' ) {
- $res = $dbr->select(
- array( 'echo_notification', 'echo_event' ),
- array( 'event_agent_id', 'event_agent_ip',
'event_extra', 'event_page_id' ),
- array(
- 'notification_event=event_id',
- 'notification_user' => $user->getId(),
- 'notification_bundle_base' => 0,
- 'notification_bundle_display_hash' =>
$bundleHash
- ),
- __METHOD__,
- array( 'ORDER BY' => 'notification_timestamp '
. $order, 'LIMIT' => $limit )
- );
- // this would be email for now
- } else {
- $res = $dbr->select(
- array( 'echo_email_batch', 'echo_event' ),
- array( 'event_agent_id', 'event_agent_ip',
'event_extra', 'event_page_id' ),
- array(
- 'eeb_event_id=event_id',
- 'eeb_user_id' => $user->getId(),
- 'eeb_event_hash' => $bundleHash
- ),
- __METHOD__,
- array( 'ORDER BY' => 'eeb_event_id ' . $order,
'LIMIT' => $limit )
- );
- }
-
- return $res;
- }
-
- /**
- * Get the last bundle stat - read_timestamp & bundle_display_hash
- * @param $user User
- * @param $bundleHash string The hash used to identify a set of
bundle-able events
- * @return ResultWrapper|bool
- */
- public function getLastBundleStat( $user, $bundleHash ) {
- $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-
- $res = $dbr->selectRow(
- array( 'echo_notification' ),
- array( 'notification_read_timestamp',
'notification_bundle_display_hash' ),
- array(
- 'notification_user' => $user->getId(),
- 'notification_bundle_hash' => $bundleHash
- ),
- __METHOD__,
- array( 'ORDER BY' => 'notification_timestamp DESC',
'LIMIT' => 1 )
- );
- return $res;
- }
-
- /**
- * @param $row array
- * @return int
- */
- public function createEvent( $row ) {
- $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
- $id = $dbw->nextSequenceValue( 'echo_event_id' );
-
- if ( $id ) {
- $row['event_id'] = $id;
- }
-
- $dbw->insert( 'echo_event', $row, __METHOD__ );
-
- if ( !$id ) {
- $id = $dbw->insertId();
- }
-
- return $id;
- }
-
- /**
- * @param $id int
- * @param $fromMaster bool
- * @return ResultWrapper
- * @throws MWException
- */
- public function loadEvent( $id, $fromMaster = false ) {
- $db = $fromMaster ? MWEchoDbFactory::getDB( DB_MASTER ) :
MWEchoDbFactory::getDB( DB_SLAVE );
-
- $row = $db->selectRow( 'echo_event', '*', array( 'event_id' =>
$id ), __METHOD__ );
-
- if ( !$row && !$fromMaster ) {
- return $this->loadEvent( $id, true );
- } elseif ( !$row ) {
- throw new MWException( "No EchoEvent found with ID:
$id" );
- }
-
- return $row;
- }
-
- /**
- * @param $user User
- * @param $eventIDs array
- */
- public function markRead( $user, $eventIDs ) {
- if ( !$eventIDs ) {
- return;
- }
-
- $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
- $dbw->update(
- 'echo_notification',
- array( 'notification_read_timestamp' =>
$dbw->timestamp( wfTimestampNow() ) ),
- array(
- 'notification_user' => $user->getId(),
- 'notification_event' => $eventIDs,
- 'notification_read_timestamp' => null,
- ),
- __METHOD__
- );
- }
-
- /**
- * @param $user User
- */
- public function markAllRead( $user ) {
- $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
- $dbw->update(
- 'echo_notification',
- array( 'notification_read_timestamp' =>
$dbw->timestamp( wfTimestampNow() ) ),
- array(
- 'notification_user' => $user->getId(),
- 'notification_read_timestamp' => NULL,
- 'notification_bundle_base' => 1,
- ),
- __METHOD__
- );
- }
-
- /**
- * @param $user User object to check notifications for
- * @param $dbSource string use master or slave storage to pull count
- * @return int
- */
- public function getNotificationCount( $user, $dbSource ) {
- // double check
- if ( !in_array( $dbSource, array( DB_SLAVE, DB_MASTER ) ) ) {
- $dbSource = DB_SLAVE;
- }
-
- $eventTypesToLoad =
EchoNotificationController::getUserEnabledEvents( $user, 'web' );
-
- if ( !$eventTypesToLoad ) {
- return 0;
- }
-
- global $wgEchoMaxNotificationCount;
-
- $db = MWEchoDbFactory::getDB( $dbSource );
- $res = $db->select(
- array( 'echo_notification', 'echo_event' ),
- array( 'notification_event' ),
- array(
- 'notification_user' => $user->getId(),
- 'notification_bundle_base' => 1,
- 'notification_read_timestamp' => null,
- 'event_type' => $eventTypesToLoad,
- ),
- __METHOD__,
- array( 'LIMIT' => $wgEchoMaxNotificationCount + 1 ),
- array(
- 'echo_event' => array( 'LEFT JOIN',
'notification_event=event_id' ),
- )
- );
- return $db->numRows( $res );
- }
-
- /**
- * IMPORTANT: should only call this function if the number of unread
notification
- * is reasonable, for example, unread notification count is less than
the max
- * display defined in $wgEchoMaxNotificationCount
- * @param $user User
- * @param $type string
- * @return array
- */
- public function getUnreadNotifications( $user, $type ) {
- $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
- $res = $dbr->select(
- array( 'echo_notification', 'echo_event' ),
- array( 'notification_event' ),
- array(
- 'notification_user' => $user->getId(),
- 'notification_bundle_base' => 1,
- 'notification_read_timestamp' => null,
- 'event_type' => $type,
- 'notification_event = event_id'
- ),
- __METHOD__
- );
-
- $eventIds = array();
- foreach ( $res as $row ) {
- $eventIds[$row->notification_event] =
$row->notification_event;
- }
-
- return $eventIds;
- }
-
-}
diff --git a/includes/EchoBackend.php b/includes/EchoBackend.php
deleted file mode 100644
index cbfbbed..0000000
--- a/includes/EchoBackend.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-/**
- * Base backend class for accessing and saving echo notification data,
- * this class should only provide all the necessary interfaces and
- * implementation should be provided in each child class
- */
-abstract class MWEchoBackend {
-
- /**
- * Factory to initialize a backend class
- * @param $backend string
- * @return MWEchoBackend
- * @throws MWException
- */
- public static function factory( $backend ) {
- $backend = strval( $backend );
-
- $className = 'MW' . $backend . 'EchoBackend';
-
- if ( !class_exists( $className ) ) {
- throw new MWException( "$backend backend is not
supported" );
- }
-
- return new $className();
- }
-
- /**
- * Extract the offset used for notification list
- *
- * @param $continue String Used for offset
- *
- * @throws MWException
- * @return int[]
- */
- protected function extractQueryOffset( $continue ) {
- $offset = array (
- 'timestamp' => 0,
- 'offset' => 0,
- );
- if ( $continue ) {
- $values = explode( '|', $continue, 3 );
- if ( count( $values ) !== 2 ) {
- throw new MWException( 'Invalid continue param:
' . $continue );
- }
- $offset['timestamp'] = (int)$values[0];
- $offset['offset'] = (int)$values[1];
- }
-
- return $offset;
- }
-
- /**
- * Create a new notification
- * @param $row array
- */
- abstract public function createNotification( $row );
-
- /**
- * Load notifications based on the parameters
- * @param $user User the user to get notifications for
- * @param $limit int The maximum number of notifications to return
- * @param $continue string Used for offset
- * @param $outputFormat string The output format of the notifications
(web, email, etc.)
- * @return array
- */
- abstract public function loadNotifications( $user, $limit, $continue,
$outputFormat = 'web' );
-
- /**
- * Get the bundle data for user/hash
- * @param $user User
- * @param $bundleHash string The hash used to identify a set of
bundle-able events
- * @param $type string 'web'/'email'
- * @param $order 'ASC'/'DESC' Sort the result in ascending/descending
order
- * @param $limit int the number of records to retrieve
- * @return ResultWrapper|bool
- */
- abstract public function getRawBundleData( $user, $bundleHash, $type =
'web', $order = 'DESC', $limit = 250 );
-
- /**
- * Get the last bundle stat - read_timestamp & bundle_display_hash
- * @param $user User
- * @param $bundleHash string The hash used to identify a set of
bundle-able events
- * @return ResultWrapper|bool
- */
- abstract public function getLastBundleStat( $user, $bundleHash );
-
- /**
- * Create an Echo event
- * @param $row array
- * @return int
- */
- abstract public function createEvent( $row );
-
- /**
- * Load an Echo event
- * @param $id int
- * @param $fromMaster bool
- */
- abstract public function loadEvent( $id, $fromMaster );
-
- /**
- * Mark notifications as read for a user
- * @param $user User
- * @param $eventIDs array
- */
- abstract public function markRead( $user, $eventIDs );
-
- /**
- * Mark all unread notifications as read for a user
- * @param $user User
- */
- abstract public function markAllRead( $user );
-
- /**
- * Retrieves number of unread notifications that a user has.
- * @param $user User object to check notifications for
- * @param $dbSource string use master or slave storage to pull count
- * @return int
- */
- abstract public function getNotificationCount( $user, $dbSource );
-
- /**
- * Get the event ids for corresponding unread notifications for an
- * event type
- * @param $user User object to check notification for
- * @param $type string event type
- * @return array
- */
- abstract public function getUnreadNotifications( $user, $type );
-
-}
diff --git a/includes/EchoDbFactory.php b/includes/EchoDbFactory.php
index 24bac62..41372c4 100644
--- a/includes/EchoDbFactory.php
+++ b/includes/EchoDbFactory.php
@@ -7,8 +7,64 @@
class MWEchoDbFactory {
/**
+ * The wiki to access the database for
+ * @var string|bool
+ */
+ protected $wiki;
+
+ /**
+ * The cluster for the database
+ * @var string|bool
+ */
+ protected $cluster;
+
+ /**
+ * @param string|bool
+ * @param string|bool
+ */
+ public function __construct( $cluster = false, $wiki = false ) {
+ $this->cluster = $cluster;
+ $this->wiki = $wiki;
+ }
+
+ /**
+ * Create a db factory instance from default Echo configuration
+ * @return MWEchoDbFactory
+ */
+ public static function newFromDefault() {
+ global $wgEchoCluster;
+ return new self( $wgEchoCluster );
+ }
+
+ /**
+ * Get the database load balancer
+ * @param $wiki string|bool The wiki ID, or false for the current wiki
+ * @return LoadBalancer
+ */
+ protected function getLB() {
+ // Use the external db defined for Echo
+ if ( $this->cluster ) {
+ $lb = wfGetLBFactory()->getExternalLB( $this->cluster,
$this->wiki );
+ } else {
+ $lb = wfGetLB( $this->wiki );
+ }
+
+ return $lb;
+ }
+
+ /**
+ * Get the database connection for Echo
+ * @param $db int Index of the connection to get
+ * @param $groups mixed Query groups.
+ * @return DatabaseBase
+ */
+ public function getEchoDb( $db, $groups = array() ) {
+ return $this->getLB()->getConnection( $db, $groups, $this->wiki
);
+ }
+
+ /**
* Wrapper function for wfGetDB
- *
+ * @deprecated Use newFromDefault() instead to create a db factory
* @param $db int Index of the connection to get
* @param $groups mixed Query groups.
* @param $wiki string|bool The wiki ID, or false for the current wiki
diff --git a/includes/NotifUser.php b/includes/NotifUser.php
index 38bc388..670ed85 100644
--- a/includes/NotifUser.php
+++ b/includes/NotifUser.php
@@ -18,20 +18,21 @@
private $cache;
/**
- * Echo backend storage
- * @var MWEchoBackend
+ * Database access gateway
+ * @var EchoUserNotificationGateway
*/
- private $storage;
+ private $userNotifGateway;
/**
* Constructor for initialization
- * @param $user User
+ * @param User
+ * @param BagOStuff
+ * @param EchoUserNotificationGateway
*/
- private function __construct( User $user ) {
- global $wgMemc, $wgEchoBackend;
+ private function __construct( User $user, BagOStuff $cache,
EchoUserNotificationGateway $userNotifGateway ) {
$this->mUser = $user;
- $this->storage = $wgEchoBackend;
- $this->cache = $wgMemc;
+ $this->userNotifGateway = $userNotifGateway;
+ $this->cache = $cache;
}
/**
@@ -44,7 +45,11 @@
if ( $user->isAnon() ) {
throw new MWException( 'User must be logged in to view
notification!' );
}
- return new MWEchoNotifUser( $user );
+ global $wgMemc;
+ return new MWEchoNotifUser(
+ $user, $wgMemc,
+ new EchoUserNotificationGateway( $user,
MWEchoDbFactory::newFromDefault() )
+ );
}
/**
@@ -66,8 +71,7 @@
// Mark the talk page notification as read
$this->markRead(
- $this->storage->getUnreadNotifications(
- $this->mUser,
+ $this->userNotifGateway->getUnreadNotifications(
'edit-user-talk'
)
);
@@ -133,7 +137,7 @@
return (int)$this->cache->get( $memcKey );
}
- $count = $this->storage->getNotificationCount( $this->mUser,
$dbSource );
+ $count = $this->userNotifGateway->getNotificationCount(
$dbSource );
$this->cache->set( $memcKey, $count, 86400 );
@@ -150,7 +154,7 @@
return;
}
- $this->storage->markRead( $this->mUser, $eventIds );
+ $this->userNotifGateway->markRead( $eventIds );
$this->resetNotificationCount( DB_MASTER );
}
@@ -165,7 +169,7 @@
// Only update all the unread notifications if it isn't a huge
number.
// TODO: Implement batched jobs it's over the maximum.
- $this->storage->markAllRead( $this->mUser );
+ $this->userNotifGateway->markAllRead();
$this->resetNotificationCount( DB_MASTER );
$this->flagCacheWithNoTalkNotification();
return true;
diff --git a/includes/gateway/UserNotificationGateway.php
b/includes/gateway/UserNotificationGateway.php
new file mode 100644
index 0000000..b138a6c
--- /dev/null
+++ b/includes/gateway/UserNotificationGateway.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * Database gateway which handles direct database interaction with the
+ * echo_notification & echo_event for a user, that wouldn't require
+ * loading data into models
+ */
+class EchoUserNotificationGateway {
+
+ /**
+ * @var MWEchoDbFactory
+ */
+ protected $dbFactory;
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * The tables for this gateway
+ */
+ protected static $eventTable = 'echo_event';
+ protected static $notificationTable = 'echo_notification';
+
+ /**
+ * @param User
+ * @param MWEchoDbFactory
+ */
+ public function __construct( User $user, MWEchoDbFactory $dbFactory ) {
+ $this->user = $user;
+ $this->dbFactory = $dbFactory;
+ }
+
+ /**
+ * Mark notifications as read
+ * @param $eventIDs array
+ */
+ public function markRead( array $eventIDs ) {
+ if ( !$eventIDs ) {
+ return;
+ }
+
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ return $dbw->update(
+ self::$notificationTable,
+ array( 'notification_read_timestamp' =>
$dbw->timestamp( wfTimestampNow() ) ),
+ array(
+ 'notification_user' => $this->user->getId(),
+ 'notification_event' => $eventIDs,
+ 'notification_read_timestamp' => null,
+ ),
+ __METHOD__
+ );
+ }
+
+ /**
+ * Mark all notification as read
+ */
+ public function markAllRead() {
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ return $dbw->update(
+ self::$notificationTable,
+ array( 'notification_read_timestamp' =>
$dbw->timestamp( wfTimestampNow() ) ),
+ array(
+ 'notification_user' => $this->user->getId(),
+ 'notification_read_timestamp' => NULL,
+ 'notification_bundle_base' => 1,
+ ),
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param $dbSource string use master or slave storage to pull count
+ * @return int
+ */
+ public function getNotificationCount( $dbSource ) {
+ // double check
+ if ( !in_array( $dbSource, array( DB_SLAVE, DB_MASTER ) ) ) {
+ $dbSource = DB_SLAVE;
+ }
+
+ $eventTypesToLoad =
EchoNotificationController::getUserEnabledEvents( $this->user, 'web' );
+
+ if ( !$eventTypesToLoad ) {
+ return 0;
+ }
+
+ global $wgEchoMaxNotificationCount;
+
+ $db = $this->dbFactory->getEchoDb( $dbSource );
+ $res = $db->select(
+ array(
+ self::$notificationTable,
+ self::$eventTable
+ ),
+ array( 'notification_event' ),
+ array(
+ 'notification_user' => $this->user->getId(),
+ 'notification_bundle_base' => 1,
+ 'notification_read_timestamp' => null,
+ 'event_type' => $eventTypesToLoad,
+ ),
+ __METHOD__,
+ array( 'LIMIT' => $wgEchoMaxNotificationCount + 1 ),
+ array(
+ 'echo_event' => array( 'LEFT JOIN',
'notification_event=event_id' ),
+ )
+ );
+ if ( $res ) {
+ return $db->numRows( $res );
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * IMPORTANT: should only call this function if the number of unread
notification
+ * is reasonable, for example, unread notification count is less than
the max
+ * display defined in $wgEchoMaxNotificationCount
+ * @param string
+ * @return int[]
+ */
+ public function getUnreadNotifications( $type ) {
+ $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+ $res = $dbr->select(
+ array(
+ self::$notificationTable,
+ self::$eventTable
+ ),
+ array( 'notification_event' ),
+ array(
+ 'notification_user' => $this->user->getId(),
+ 'notification_bundle_base' => 1,
+ 'notification_read_timestamp' => null,
+ 'event_type' => $type,
+ 'notification_event = event_id'
+ ),
+ __METHOD__
+ );
+
+ $eventIds = array();
+ if ( $res ) {
+ foreach ( $res as $row ) {
+ $eventIds[$row->notification_event] =
$row->notification_event;
+ }
+ }
+
+ return $eventIds;
+ }
+
+}
diff --git a/includes/mapper/EventMapper.php b/includes/mapper/EventMapper.php
new file mode 100644
index 0000000..a888a5d
--- /dev/null
+++ b/includes/mapper/EventMapper.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * Database mapper for EchoEvent model, which is an immutable class, there
should
+ * not be any update to it
+ */
+class EchoEventMapper {
+
+ /**
+ * Echo database factory
+ * @param MWEchoDbFactory
+ */
+ protected $dbFactory;
+
+ /**
+ * @param MWEchoDbFactory
+ */
+ public function __construct( MWEchoDbFactory $dbFactory ) {
+ $this->dbFactory = $dbFactory;
+ }
+
+ /**
+ * Insert an event record
+ *
+ * @param EchoEvent
+ * @return int|bool
+ */
+ public function insert( EchoEvent $event ) {
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ $id = $dbw->nextSequenceValue( 'echo_event_id' );
+
+ $row = $event->toDbArray();
+ if ( $id ) {
+ $row['event_id'] = $id;
+ }
+
+ $res = $dbw->insert( 'echo_event', $row, __METHOD__ );
+
+ if ( $res ) {
+ if ( !$id ) {
+ $id = $dbw->insertId();
+ }
+ return $id;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Create an EchoEvent by id
+ *
+ * @param int
+ * @param boolean
+ * @return EchoEvent
+ * @throws MWException
+ */
+ public function fetchById( $id, $fromMaster = false ) {
+ $db = $fromMaster ? $this->dbFactory->getEchoDb( DB_MASTER ) :
$this->dbFactory->getEchoDb( DB_SLAVE );
+
+ $row = $db->selectRow( 'echo_event', '*', array( 'event_id' =>
$id ), __METHOD__ );
+
+ if ( !$row && !$fromMaster ) {
+ return $this->fetchById( $id, true );
+ } elseif ( !$row ) {
+ throw new MWException( "No EchoEvent found with ID:
$id" );
+ }
+
+ return EchoEvent::newFromRow( $row );
+ }
+
+ /**
+ * Get a list of echo events identified by user and bundle hash
+ *
+ * @param $user User
+ * @param $bundleHash string the bundle hash
+ * @param $type string distribution type
+ * @param $order string 'ASC'/'DESC'
+ * @param $limit int
+ * @return EchoEvent[]|bool
+ */
+ public function fetchByUserBundleHash( User $user, $bundleHash, $type =
'web', $order = 'DESC', $limit = 250 ) {
+ $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+ // We only display 99+ if the number is over 100, we can do
limit 250, this should
+ // be sufficient to return 99 distinct group iterators, avoid
select count( distinct )
+ // for the following reason:
+ // 1. it will not scale for large volume data
+ // 2. notification may have random grouping iterator
+ // 3. agent may be anonymous, can't do distinct over two
columns: event_agent_id and event_agent_ip
+ if ( $type == 'web' ) {
+ $res = $dbr->select(
+ array( 'echo_notification', 'echo_event' ),
+ array( 'echo_event.*' ),
+ array(
+ 'notification_event=event_id',
+ 'notification_user' => $user->getId(),
+ 'notification_bundle_base' => 0,
+ 'notification_bundle_display_hash' =>
$bundleHash
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'notification_timestamp '
. $order, 'LIMIT' => $limit )
+ );
+ // this would be email for now
+ } else {
+ $res = $dbr->select(
+ array( 'echo_email_batch', 'echo_event' ),
+ array( 'echo_event.*' ),
+ array(
+ 'eeb_event_id=event_id',
+ 'eeb_user_id' => $user->getId(),
+ 'eeb_event_hash' => $bundleHash
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'eeb_event_id ' . $order,
'LIMIT' => $limit )
+ );
+ }
+
+ if ( $res ) {
+ $data = array();
+ foreach ( $res as $row ) {
+ $data[] = EchoEvent::newFromRow( $row );
+ }
+ return $data;
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/includes/mapper/NotificationMapper.php
b/includes/mapper/NotificationMapper.php
new file mode 100644
index 0000000..de91fbf
--- /dev/null
+++ b/includes/mapper/NotificationMapper.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * Database mapper for EchoNotification model
+ */
+class EchoNotificationMapper {
+
+ /**
+ * Echo database factory
+ * @param MWEchoDbFactory
+ */
+ protected $dbFactory;
+
+ /**
+ * @param MWEchoDbFactory
+ */
+ public function __construct( MWEchoDbFactory $dbFactory ) {
+ $this->dbFactory = $dbFactory;
+ }
+
+ /**
+ * Insert a notification record
+ * @param EchoNotification
+ * @return null
+ */
+ public function insert( EchoNotification $notification ) {
+ $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+ $fname = __METHOD__;
+ $row = $notification->toDbArray();
+
+ $dbw->onTransactionIdle( function() use ( $dbw, $row, $fname ) {
+ $dbw->startAtomic( $fname );
+ // reset the bundle base if this notification has a
display hash
+ // the result of this operation is that all previous
notifications
+ // with the same display hash are set to non-base
because new record
+ // is becoming the bundle base
+ if ( $row['notification_bundle_display_hash'] ) {
+ $dbw->update(
+ 'echo_notification',
+ array( 'notification_bundle_base' => 0
),
+ array(
+ 'notification_user' =>
$row['notification_user'],
+
'notification_bundle_display_hash' => $row['notification_bundle_display_hash'],
+ 'notification_bundle_base' => 1
+ ),
+ $fname
+ );
+ }
+
+ $row['notification_timestamp'] = $dbw->timestamp(
$row['notification_timestamp'] );
+ $res = $dbw->insert( 'echo_notification', $row, $fname
);
+ $dbw->endAtomic( $fname );
+
+ // @todo - This is simple and easy but the proper way
is to build a notification
+ // observer to notify all listeners on creating a new
notification
+ if ( $res ) {
+ $user = User::newFromId(
$row['notification_user'] );
+ MWEchoNotifUser::newFromUser( $user
)->resetNotificationCount( DB_MASTER );
+ }
+ } );
+ }
+
+ /**
+ * Extract the offset used for notification list
+ * @param $continue String Used for offset
+ * @throws MWException
+ * @return int[]
+ */
+ protected function extractQueryOffset( $continue ) {
+ $offset = array (
+ 'timestamp' => 0,
+ 'offset' => 0,
+ );
+ if ( $continue ) {
+ $values = explode( '|', $continue, 3 );
+ if ( count( $values ) !== 2 ) {
+ throw new MWException( 'Invalid continue param:
' . $continue );
+ }
+ $offset['timestamp'] = (int)$values[0];
+ $offset['offset'] = (int)$values[1];
+ }
+
+ return $offset;
+ }
+
+ /**
+ * Get Notification by user in batch along with limit, offset etc
+ * @param User the user to get notifications for
+ * @param int The maximum number of notifications to return
+ * @param string Used for offset
+ * @param string Notification distribution type ( web, email, etc.)
+ * @return EchoNotification[]
+ */
+ public function fetchByUser( User $user, $limit, $continue,
$distributionType = 'web' ) {
+ $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+ $eventTypesToLoad =
EchoNotificationController::getUserEnabledEvents( $user, $distributionType );
+ if ( !$eventTypesToLoad ) {
+ return array();
+ }
+
+ // Look for notifications with base = 1
+ $conds = array(
+ 'notification_user' => $user->getID(),
+ 'event_type' => $eventTypesToLoad,
+ 'notification_bundle_base' => 1
+ );
+
+ $offset = $this->extractQueryOffset( $continue );
+
+ // Start points are specified
+ if ( $offset['timestamp'] && $offset['offset'] ) {
+ $ts = $dbr->addQuotes( $dbr->timestamp(
$offset['timestamp'] ) );
+ // The offset and timestamp are those of the first
notification we want to return
+ $conds[] = "notification_timestamp < $ts OR (
notification_timestamp = $ts AND notification_event <= " . $offset['offset'] .
" )";
+ }
+
+ $res = $dbr->select(
+ array( 'echo_notification', 'echo_event' ),
+ '*',
+ $conds,
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'notification_timestamp DESC,
notification_event DESC',
+ 'LIMIT' => $limit,
+ ),
+ array(
+ 'echo_event' => array( 'LEFT JOIN',
'notification_event=event_id' ),
+ )
+ );
+
+ $data = array();
+
+ if ( $res ) {
+ foreach ( $res as $row ) {
+ $data[] = EchoNotification::newFromRow( $row );
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Get the last notification in a set of bundle-able notifications by a
bundle hash
+ * @param User
+ * @param string The hash used to identify a set of bundle-able
notifications
+ * @return EchoNotification|bool
+ */
+ public function fetchNewestByUserBundleHash( User $user, $bundleHash ) {
+ $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+ $row = $dbr->selectRow(
+ array( 'echo_notification', 'echo_event' ),
+ array( '*' ),
+ array(
+ 'notification_user' => $user->getId(),
+ 'notification_bundle_hash' => $bundleHash
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'notification_timestamp DESC',
'LIMIT' => 1 ),
+ array(
+ 'echo_event' => array( 'LEFT JOIN',
'notification_event=event_id' ),
+ )
+ );
+ if ( $row ) {
+ return EchoNotification::newFromRow( $row );
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/model/Event.php b/model/Event.php
index 34b9e46..6fae194 100644
--- a/model/Event.php
+++ b/model/Event.php
@@ -147,6 +147,37 @@
}
/**
+ * Convert the object's database property to array
+ * @return array
+ */
+ public function toDbArray() {
+ $data = array (
+ 'event_type' => $this->type,
+ 'event_variant' => $this->variant,
+ 'event_extra' => $this->serializeExtra()
+ );
+ if ( $this->id ) {
+ $data['event_id'] = $this->id;
+ }
+ if ( $this->agent ) {
+ if ( $this->agent->isAnon() ) {
+ $data['event_agent_ip'] =
$this->agent->getName();
+ } else {
+ $data['event_agent_id'] = $this->agent->getId();
+ }
+ }
+ if ( $this->title ) {
+ $pageId = $this->title->getArticleId();
+ // Don't need any special handling for title with no id
+ // as they are already stored in extra data array
+ if ( $pageId ) {
+ $data['event_page_id'] = $pageId;
+ }
+ }
+ return $data;
+ }
+
+ /**
* Check whether the echo event is an enabled event
* @return bool
*/
@@ -163,32 +194,8 @@
* Inserts the object into the database.
*/
protected function insert() {
- global $wgEchoBackend;
-
- if ( $this->id ) {
- throw new MWException( "Attempt to insert() an existing
event" );
- }
-
- $row = array(
- 'event_type' => $this->type,
- 'event_variant' => $this->variant,
- );
-
- if ( $this->agent ) {
- if ( $this->agent->isAnon() ) {
- $row['event_agent_ip'] =
$this->agent->getName();
- } else {
- $row['event_agent_id'] = $this->agent->getId();
- }
- }
-
- if ( $this->pageId ) {
- $row['event_page_id'] = $this->pageId;
- }
-
- $row['event_extra'] = $this->serializeExtra();
-
- $this->id = $wgEchoBackend->createEvent( $row );
+ $eventMapper = new EchoEventMapper(
MWEchoDbFactory::newFromDefault() );
+ $this->id = $eventMapper->insert( $this );
}
/**
@@ -235,9 +242,21 @@
* @param $fromMaster bool
*/
public function loadFromID( $id, $fromMaster = false ) {
- global $wgEchoBackend;
+ $eventMapper = new EchoEventMapper(
MWEchoDbFactory::newFromDefault() );
+ $event = $eventMapper->fetchById( $id, $fromMaster );
- $this->loadFromRow( $wgEchoBackend->loadEvent( $id, $fromMaster
) );
+ // Copy over the attribute
+ $this->id = $event->id;
+ $this->type = $event->type;
+ $this->variant = $event->variant;
+ $this->extra = $event->extra;
+ $this->pageId = $event->pageId;
+ $this->agent = $event->agent;
+ $this->title = $event->title;
+ // Don't overwrite timestamp if it exists already
+ if ( !$this->timestamp ) {
+ $this->timestamp = $event->timestamp;
+ }
}
/**
diff --git a/model/Notification.php b/model/Notification.php
index d0a6abc..8f93ad2 100644
--- a/model/Notification.php
+++ b/model/Notification.php
@@ -23,6 +23,25 @@
protected $readTimestamp;
/**
+ * Determine whether this is a bundle base. Default is 1,
+ * which means it's a bundle base
+ * @var int
+ */
+ protected $bundleBase = 1;
+
+ /**
+ * The hash used to determine if a set of event could be bundled
+ * @var string
+ */
+ protected $bundleHash = '';
+
+ /**
+ * The hash used to bundle events to display
+ * @var string
+ */
+ protected $bundleDisplayHash = '';
+
+ /**
* Do not use this constructor.
*/
protected function __construct() {}
@@ -71,16 +90,9 @@
* Adds this new notification object to the backend storage.
*/
protected function insert() {
- global $wgEchoBackend, $wgEchoNotifications;
+ global $wgEchoNotifications;
- $row = array(
- 'notification_event' => $this->event->getId(),
- 'notification_user' => $this->user->getId(),
- 'notification_timestamp' => $this->timestamp,
- 'notification_read_timestamp' => $this->readTimestamp,
- 'notification_bundle_hash' => '',
- 'notification_bundle_display_hash' => ''
- );
+ $notifMapper = new EchoNotificationMapper(
MWEchoDbFactory::newFromDefault() );
// Get the bundle key for this event if web bundling is enabled
$bundleKey = '';
@@ -89,23 +101,60 @@
}
if ( $bundleKey ) {
$hash = md5( $bundleKey );
- $row['notification_bundle_hash'] = $hash;
-
- $lastStat = $wgEchoBackend->getLastBundleStat(
$this->user, $hash );
+ $this->bundleHash = $hash;
+ $lastNotif = $notifMapper->fetchNewestByUserBundleHash(
$this->user, $hash );
// Use a new display hash if:
// 1. there was no last bundle notification
// 2. last bundle notification with the same hash was
read
- if ( $lastStat &&
!$lastStat->notification_read_timestamp ) {
- $row['notification_bundle_display_hash'] =
$lastStat->notification_bundle_display_hash;
+ if ( $lastNotif && !$lastNotif->getReadTimestamp() ) {
+ $this->bundleDisplayHash =
$lastNotif->getBundleDisplayHash();
} else {
- $row['notification_bundle_display_hash'] = md5(
$bundleKey . '-display-hash-' . wfTimestampNow() );
+ $this->bundleDisplayHash = md5( $bundleKey .
'-display-hash-' . wfTimestampNow() );
}
}
- $wgEchoBackend->createNotification( $row );
+ $notifMapper->insert( $this );
wfRunHooks( 'EchoCreateNotificationComplete', array( $this ) );
+ }
+
+ /**
+ * Load a notification record from std class
+ * @param stdClass
+ * @return EchoNotification
+ */
+ public static function newFromRow( $row ) {
+ $notification = new EchoNotification();
+
+ if ( property_exists( $row, 'event_type' ) ) {
+ $notification->event = EchoEvent::newFromRow( $row );
+ } else {
+ $notification->event = EchoEvent::newFromID(
$row->notification_event );
+ }
+ $notification->user = User::newFromId( $row->notification_user
);
+ $notification->timestamp = $row->notification_timestamp;
+ $notification->readTimestamp =
$row->notification_read_timestamp;
+ $notification->bundleBase = $row->notification_bundle_base;
+ $notification->bundleHash = $row->notification_bundle_hash;
+ $notification->bundleDisplayHash =
$row->notification_bundle_display_hash;
+ return $notification;
+ }
+
+ /**
+ * Convert object property to database row array
+ * @return array
+ */
+ public function toDbArray() {
+ return array(
+ 'notification_event' => $this->event->getId(),
+ 'notification_user' => $this->user->getId(),
+ 'notification_timestamp' => $this->timestamp,
+ 'notification_read_timestamp' => $this->readTimestamp,
+ 'notification_bundle_base' => $this->bundleBase,
+ 'notification_bundle_hash' => $this->bundleHash,
+ 'notification_bundle_display_hash' =>
$this->bundleDisplayHash
+ );
}
/**
@@ -139,4 +188,28 @@
public function getReadTimestamp() {
return $this->readTimestamp;
}
+
+ /**
+ * Getter method
+ * @return int Notification bundle base
+ */
+ public function getBundleBase() {
+ return $this->bundleBase;
+ }
+
+ /**
+ * Getter method
+ * @return string|null Notification bundle hash
+ */
+ public function getBundleHash() {
+ return $this->bundleHash;
+ }
+
+ /**
+ * Getter method
+ * @return string|null Notification bundle display hash
+ */
+ public function getBundleDisplayHash() {
+ return $this->bundleDisplayHash;
+ }
}
diff --git a/special/SpecialNotifications.php b/special/SpecialNotifications.php
index 067d090..4ca78bd 100644
--- a/special/SpecialNotifications.php
+++ b/special/SpecialNotifications.php
@@ -38,7 +38,12 @@
$continue = $this->getRequest()->getVal( 'continue', null );
// Pull the notifications
- $notif = ApiEchoNotifications::getNotifications( $user, 'html',
self::$displayNum + 1, $continue );
+ $notif = array();
+ $notificationMapper = new EchoNotificationMapper(
MWEchoDbFactory::newFromDefault() );
+ $notifications = $notificationMapper->fetchByUser( $user,
self::$displayNum + 1, $continue, 'web' );
+ foreach ( $notifications as $notification ) {
+ $notif[] = EchoDataOutputFormatter::formatOutput(
$notification, 'html', $user );
+ }
// If there are no notifications, display a message saying so
if ( !$notif ) {
diff --git a/tests/includes/EchoDbFactoryTest.php
b/tests/includes/EchoDbFactoryTest.php
new file mode 100644
index 0000000..61cffab
--- /dev/null
+++ b/tests/includes/EchoDbFactoryTest.php
@@ -0,0 +1,29 @@
+<?php
+
+class MWEchoDbFactoryTest extends MediaWikiTestCase {
+
+ public function testNewFromDefault() {
+ $db = MWEchoDbFactory::newFromDefault();
+ $this->assertInstanceOf( 'MWEchoDbFactory', $db );
+ return $db;
+ }
+
+ /**
+ * @depends testNewFromDefault
+ */
+ public function testGetEchoDb( MWEchoDbFactory $db ) {
+ $this->assertInstanceOf( 'DatabaseBase', $db->getEchoDb(
DB_MASTER ) );
+ $this->assertInstanceOf( 'DatabaseBase', $db->getEchoDb(
DB_SLAVE ) );
+ }
+
+ /**
+ * @depends testNewFromDefault
+ */
+ public function testGetLB( MWEchoDbFactory $db ) {
+ $reflection = new ReflectionClass( 'MWEchoDbFactory' );
+ $method = $reflection->getMethod( 'getLB' );
+ $method->setAccessible( true );
+ $this->assertInstanceOf( 'LoadBalancer', $method->invoke( $db )
);
+ }
+
+}
diff --git a/tests/includes/gateway/UserNotificationGatewayTest.php
b/tests/includes/gateway/UserNotificationGatewayTest.php
new file mode 100644
index 0000000..5f27a9c
--- /dev/null
+++ b/tests/includes/gateway/UserNotificationGatewayTest.php
@@ -0,0 +1,134 @@
+<?php
+
+class EchoUserNotificationGatewayTest extends MediaWikiTestCase {
+
+ public function testMarkRead() {
+ // no event ids to mark
+ $gateway = new EchoUserNotificationGateway( User::newFromId( 1
), $this->mockMWEchoDbFactory() );
+ $this->assertNull( $gateway->markRead( array() ) );
+
+ // successful update
+ $gateway = new EchoUserNotificationGateway( User::newFromId( 1
), $this->mockMWEchoDbFactory( array( 'update' => true ) ) );
+ $this->assertTrue( $gateway->markRead( array( 2 ) ) );
+
+ // unsuccessful update
+ $gateway = new EchoUserNotificationGateway( User::newFromId( 1
), $this->mockMWEchoDbFactory( array( 'update' => false ) ) );
+ $this->assertFalse( $gateway->markRead( array( 2 ) ) );
+ }
+
+ public function testMarkAllRead() {
+ // successful update
+ $gateway = new EchoUserNotificationGateway( User::newFromId( 1
), $this->mockMWEchoDbFactory( array( 'update' => true ) ) );
+ $this->assertTrue( $gateway->markAllRead( array( 2 ) ) );
+
+ // unsuccessful update
+ $gateway = new EchoUserNotificationGateway( User::newFromId( 1
), $this->mockMWEchoDbFactory( array( 'update' => false ) ) );
+ $this->assertFalse( $gateway->markAllRead( array( 2 ) ) );
+ }
+
+ public function testGetNotificationCount() {
+ global $wgEchoNotificationCategories;
+ $previous = $wgEchoNotificationCategories;
+
+ // Alter the category group so the user is always elegible to
+ // view some notification types.
+ foreach ( $wgEchoNotificationCategories as &$value ) {
+ $value['usergroups'] = array( 'echo_group' );
+ }
+ unset( $value );
+
+ // successful select
+ $gateway = new EchoUserNotificationGateway( $this->mockUser(),
$this->mockMWEchoDbFactory( array( 'select' => false ) ) );
+ $this->assertEquals( 0, $gateway->getNotificationCount(
DB_SLAVE ) );
+
+ // successful select
+ $gateway = new EchoUserNotificationGateway( $this->mockUser(),
$this->mockMWEchoDbFactory( array( 'select' => array( 1, 2, 3 ) ) ) );
+ $this->assertEquals( 3, $gateway->getNotificationCount(
DB_SLAVE ) );
+
+ // Alter the category group so the user is not elegible to
+ // view any notification types.
+ foreach ( $wgEchoNotificationCategories as &$value ) {
+ $value['usergroups'] = array( 'sysop' );
+ }
+ unset( $value );
+
+ $gateway = new EchoUserNotificationGateway( $this->mockUser(),
$this->mockMWEchoDbFactory( array( 'select' => array( 1, 2, 3 ) ) ) );
+ $this->assertEquals( 0, $gateway->getNotificationCount(
DB_SLAVE ) );
+
+ $wgEchoNotificationCategories = $previous;
+ }
+
+ public function testGetUnreadNotifications() {
+ $gateway = new EchoUserNotificationGateway( $this->mockUser(),
$this->mockMWEchoDbFactory( array( 'select' => false ) ) );
+ $this->assertEmpty( $gateway->getUnreadNotifications(
'user_talk' ) );
+
+ $dbResult = array(
+ (object)array( 'notification_event' => 1 ),
+ (object)array( 'notification_event' => 2 ),
+ (object)array( 'notification_event' => 3 ),
+ );
+ $gateway = new EchoUserNotificationGateway( $this->mockUser(),
$this->mockMWEchoDbFactory( array( 'select' => $dbResult ) ) );
+ $res = $gateway->getUnreadNotifications( 'user_talk' );
+ $this->assertEquals( $res, array( 1 => 1, 2 => 2, 3 => 3 ) );
+ }
+
+ /**
+ * Mock object of User
+ */
+ protected function mockUser() {
+ $user = $this->getMockBuilder( 'User' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $user->expects( $this->any() )
+ ->method( 'getID' )
+ ->will( $this->returnValue( 1 ) );
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->will( $this->returnValue( true ) );
+ $user->expects( $this->any() )
+ ->method( 'getGroups' )
+ ->will( $this->returnValue( array( 'echo_group' ) ) );
+ return $user;
+ }
+
+ /**
+ * Mock object of MWEchoDbFactory
+ */
+ protected function mockMWEchoDbFactory( array $dbResult = array() ) {
+ $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 = array() ) {
+ $dbResult += array(
+ 'update' => '',
+ 'select' => '',
+ 'selectRow' => '',
+ );
+ $db = $this->getMockBuilder( 'DatabaseMysql' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $db->expects( $this->any() )
+ ->method( 'update' )
+ ->will( $this->returnValue( $dbResult['update'] ) );
+ $db->expects( $this->any() )
+ ->method( 'select' )
+ ->will( $this->returnValue( $dbResult['select'] ) );
+ $db->expects( $this->any() )
+ ->method( 'selectRow' )
+ ->will( $this->returnValue( $dbResult['selectRow'] ) );
+ $db->expects( $this->any() )
+ ->method( 'numRows' )
+ ->will( $this->returnValue( count( $dbResult['select']
) ) );
+ return $db;
+ }
+
+}
diff --git a/tests/includes/mapper/EventMapperTest.php
b/tests/includes/mapper/EventMapperTest.php
new file mode 100644
index 0000000..834df68
--- /dev/null
+++ b/tests/includes/mapper/EventMapperTest.php
@@ -0,0 +1,167 @@
+<?php
+
+class EchoEventMapperTest extends MediaWikiTestCase {
+
+ 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 ) {
+ $event = $this->mockEchoEvent();
+ $eventMapper = new EchoEventMapper( $this->mockMWEchoDbFactory(
$dbResult ) );
+ $this->assertEquals( $result, $eventMapper->insert( $event ),
$message );
+ }
+
+ /**
+ * Successful fetchById()
+ */
+ public function testSuccessfulFetchById() {
+ $eventMapper = new EchoEventMapper(
+ $this->mockMWEchoDbFactory(
+ array(
+ 'selectRow' => (object)array (
+ 'event_id' => 1,
+ 'event_type' => 'test',
+ 'event_variant' => '',
+ 'event_extra' => '',
+ 'event_page_id' => '',
+ 'event_agent_id' => '',
+ 'event_agent_ip' => ''
+ )
+ )
+ )
+ );
+ $res = $eventMapper->fetchById( 1 );
+ $this->assertInstanceOf( 'EchoEvent', $res );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testUnsuccessfulFetchById() {
+ $eventMapper = new EchoEventMapper(
+ $this->mockMWEchoDbFactory(
+ array(
+ 'selectRow' => false
+ )
+ )
+ );
+ $res = $eventMapper->fetchById( 1 );
+ $this->assertInstanceOf( 'EchoEvent', $res );
+ }
+
+ public function testFetchByUserBundleHash() {
+ // Unsuccessful select
+ $event = $this->mockEchoEvent();
+ $eventMapper = new EchoEventMapper( $this->mockMWEchoDbFactory(
array ( 'select' => false ) ) );
+ $res = $eventMapper->fetchByUserBundleHash( User::newFromId( 1
), 'testhash', 'web', 'DESC', 250 );
+ $this->assertFalse( $res );
+
+ // Successful select
+ $event = $this->mockEchoEvent();
+ $dbResult = array (
+ (object)array (
+ 'event_id' => 1,
+ 'event_type' => 'test',
+ 'event_variant' => '',
+ 'event_extra' => '',
+ 'event_page_id' => '',
+ 'event_agent_id' => '',
+ 'event_agent_ip' => ''
+ ),
+ (object)array (
+ 'event_id' => 2,
+ 'event_type' => 'test2',
+ 'event_variant' => '',
+ 'event_extra' => '',
+ 'event_page_id' => '',
+ 'event_agent_id' => '',
+ 'event_agent_ip' => ''
+ )
+ );
+ $eventMapper = new EchoEventMapper( $this->mockMWEchoDbFactory(
array ( 'select' => $dbResult ) ) );
+ $res = $eventMapper->fetchByUserBundleHash( User::newFromId( 1
), 'testhash', 'web', 'DESC', 250 );
+ $this->assertTrue( is_array( $res ) );
+ foreach ( $res as $row ) {
+ $this->assertInstanceOf( 'EchoEvent', $row );
+ }
+ }
+
+ /**
+ * Mock object of EchoEvent
+ */
+ protected function mockEchoEvent() {
+ $event = $this->getMockBuilder( 'EchoEvent' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $event->expects( $this->any() )
+ ->method( 'toDbArray' )
+ ->will( $this->returnValue( array() ) );
+ return $event;
+ }
+
+ /**
+ * 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' => '',
+ 'selectRow' => ''
+ );
+ $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( 'selectRow' )
+ ->will( $this->returnValue( $dbResult['selectRow'] ) );
+ return $db;
+ }
+
+}
diff --git a/tests/includes/mapper/NotificationMapperTest.php
b/tests/includes/mapper/NotificationMapperTest.php
new file mode 100644
index 0000000..2de2583
--- /dev/null
+++ b/tests/includes/mapper/NotificationMapperTest.php
@@ -0,0 +1,184 @@
+<?php
+
+class EchoNotificationMapperTest extends MediaWikiTestCase {
+
+ /**
+ * @todo write this test
+ */
+ public function testInsert() {
+ $this->assertTrue( true );
+ }
+
+ public function testFetchByUser() {
+ global $wgEchoNotificationCategories;
+ $previous = $wgEchoNotificationCategories;
+
+ // Alter the category group so the user is always elegible to
+ // view some notification types.
+ foreach ( $wgEchoNotificationCategories as &$value ) {
+ $value['usergroups'] = array( 'echo_group' );
+ }
+ unset( $value );
+
+ // Unsuccessful select
+ $notifMapper = new EchoNotificationMapper(
$this->mockMWEchoDbFactory( array ( 'select' => false ) ) );
+ $res = $notifMapper->fetchByUser( $this->mockUser(), 10, '' );
+ $this->assertEmpty( $res );
+
+ // Successful select
+ $dbResult = array(
+ (object)array (
+ 'event_id' => 1,
+ 'event_type' => 'test',
+ 'event_variant' => '',
+ 'event_extra' => '',
+ 'event_page_id' => '',
+ 'event_agent_id' => '',
+ 'event_agent_ip' => '',
+ 'notification_user' => 1,
+ 'notification_timestamp' => '20140615101010',
+ 'notification_read_timestamp' =>
'20140616101010',
+ 'notification_bundle_base' => 1,
+ 'notification_bundle_hash' => 'testhash',
+ 'notification_bundle_display_hash' =>
'testdisplayhash'
+ )
+ );
+ $notifMapper = new EchoNotificationMapper(
$this->mockMWEchoDbFactory( array ( 'select' => $dbResult ) ) );
+ $res = $notifMapper->fetchByUser( $this->mockUser(), 10, '' );
+ $this->assertTrue( is_array( $res ) );
+ foreach ( $res as $row ) {
+ $this->assertInstanceOf( 'EchoNotification', $row );
+ }
+
+ // Alter the category group so the user is not elegible to
+ // view any notification types.
+ foreach ( $wgEchoNotificationCategories as &$value ) {
+ $value['usergroups'] = array( 'sysop' );
+ }
+ unset( $value );
+
+ $notifMapper = new EchoNotificationMapper(
$this->mockMWEchoDbFactory( array() ) );
+ $res = $notifMapper->fetchByUser( $this->mockUser(), 10, '' );
+ $this->assertEmpty( $res );
+
+ // Restore the default setting
+ $wgEchoNotificationCategories = $previous;
+ }
+
+ public function testFetchNewestByUserBundleHash() {
+ // Unsuccessful select
+ $notifMapper = new EchoNotificationMapper(
$this->mockMWEchoDbFactory( array ( 'selectRow' => false ) ) );
+ $res = $notifMapper->fetchNewestByUserBundleHash(
User::newFromId( 1 ), 'testhash' );
+ $this->assertFalse( $res );
+
+ // Successful select
+ $dbResult = (object)array (
+ 'event_id' => 1,
+ 'event_type' => 'test',
+ 'event_variant' => '',
+ 'event_extra' => '',
+ 'event_page_id' => '',
+ 'event_agent_id' => '',
+ 'event_agent_ip' => '',
+ 'notification_user' => 1,
+ 'notification_timestamp' => '20140615101010',
+ 'notification_read_timestamp' => '20140616101010',
+ 'notification_bundle_base' => 1,
+ 'notification_bundle_hash' => 'testhash',
+ 'notification_bundle_display_hash' => 'testdisplayhash'
+ );
+ $notifMapper = new EchoNotificationMapper(
$this->mockMWEchoDbFactory( array ( 'selectRow' => $dbResult ) ) );
+ $row = $notifMapper->fetchNewestByUserBundleHash(
User::newFromId( 1 ), 'testdisplayhash' );
+ $this->assertInstanceOf( 'EchoNotification', $row );
+ }
+
+ /**
+ * Mock object of User
+ */
+ protected function mockUser() {
+ $user = $this->getMockBuilder( 'User' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $user->expects( $this->any() )
+ ->method( 'getID' )
+ ->will( $this->returnValue( 1 ) );
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->will( $this->returnValue( true ) );
+ $user->expects( $this->any() )
+ ->method( 'getGroups' )
+ ->will( $this->returnValue( array( 'echo_group' ) ) );
+ return $user;
+ }
+
+ /**
+ * Mock object of EchoNotification
+ */
+ protected function mockEchoNotification() {
+ $event = $this->getMockBuilder( 'EchoNotification' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $event->expects( $this->any() )
+ ->method( 'toDbArray' )
+ ->will( $this->returnValue( array() ) );
+ return $event;
+ }
+
+ /**
+ * 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(
+ 'insert' => '',
+ 'select' => '',
+ 'selectRow' => ''
+ );
+ $db = $this->getMockBuilder( 'DatabaseMysql' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $db->expects( $this->any() )
+ ->method( 'insert' )
+ ->will( $this->returnValue( $dbResult['insert'] ) );
+ $db->expects( $this->any() )
+ ->method( 'select' )
+ ->will( $this->returnValue( $dbResult['select'] ) );
+ $db->expects( $this->any() )
+ ->method( 'selectRow' )
+ ->will( $this->returnValue( $dbResult['selectRow'] ) );
+ $db->expects ( $this->any() )
+ ->method( 'onTransactionIdle' )
+ ->will( new EchoExecuteFirstArgumentStub );
+
+ return $db;
+ }
+
+}
+
+class EchoExecuteFirstArgumentStub implements
PHPUnit_Framework_MockObject_Stub {
+ public function invoke( PHPUnit_Framework_MockObject_Invocation
$invocation ) {
+ if ( !$invocation instanceof
PHPUnit_Framework_MockObject_Invocation_Static ) {
+ throw new PHPUnit_Framework_Exception( 'wrong
invocation type' );
+ }
+ if ( !$invocation->arguments ) {
+ throw new PHPUnit_Framework_Exception( 'Method call
must have an argument' );
+ }
+ return call_user_func( reset( $invocation->arguments ) );
+ }
+
+ public function toString() {
+ return 'return result of call_user_func on first invocation
argument';
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/147369
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I42f4d7566543332588431c21c220c0d64d026b70
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: master
Gerrit-Owner: Bsitu <[email protected]>
Gerrit-Reviewer: Bsitu <[email protected]>
Gerrit-Reviewer: EBernhardson <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits