Merge "Clean up and refactor formatting system"

This commit is contained in:
jenkins-bot 2015-10-23 23:20:00 +00:00 committed by Gerrit Code Review
commit 0beefa78c3
11 changed files with 466 additions and 3 deletions

View file

@ -381,6 +381,7 @@ $wgEchoNotifications = array(
'category' => 'mention',
'group' => 'interactive',
'section' => 'alert',
'presentation-model' => 'EchoMentionPresentationModel',
'formatter-class' => 'EchoMentionFormatter',
'title-message' => 'notification-mention',
'title-params' => array( 'agent', 'subject-anchor', 'title', 'section-title', 'main-title-text' ),
@ -400,6 +401,8 @@ $wgEchoNotifications = array(
'category' => 'user-rights',
'group' => 'neutral',
'section' => 'alert',
'presentation-model' => 'EchoUserRightsPresentationModel',
// Legacy formatting system
'formatter-class' => 'EchoUserRightsFormatter',
'title-message' => 'notification-user-rights',
'title-params' => array( 'agent', 'user-rights-list' ),

View file

@ -45,16 +45,20 @@ $wgAutoloadClasses += array(
'EchoEmailMode' => __DIR__ . '/includes/EmailFormatter.php',
'EchoEmailSingle' => __DIR__ . '/includes/EmailFormatter.php',
'EchoEvent' => __DIR__ . '/includes/model/Event.php',
'EchoEventFormatter' => __DIR__ . '/includes/formatters/EchoEventFormatter.php',
'EchoEventMapper' => __DIR__ . '/includes/mapper/EventMapper.php',
'EchoEventMapperTest' => __DIR__ . '/tests/phpunit/mapper/EventMapperTest.php',
'EchoEventPresentationModel' => __DIR__ . '/includes/formatters/EventPresentationModel.php',
'EchoExecuteFirstArgumentStub' => __DIR__ . '/tests/phpunit/mapper/NotificationMapperTest.php',
'EchoFilteredSequentialIterator' => __DIR__ . '/includes/iterator/FilteredSequentialIterator.php',
'EchoFlyoutFormatter' => __DIR__ . '/includes/formatters/EchoFlyoutFormatter.php',
'EchoHTMLEmailDecorator' => __DIR__ . '/includes/EmailFormatter.php',
'EchoHTMLEmailFormatter' => __DIR__ . '/includes/EmailFormatter.php',
'EchoHooks' => __DIR__ . '/Hooks.php',
'EchoIteratorDecorator' => __DIR__ . '/includes/iterator/IteratorDecorator.php',
'EchoLocalCache' => __DIR__ . '/includes/cache/LocalCache.php',
'EchoMentionFormatter' => __DIR__ . '/includes/formatters/MentionFormatter.php',
'EchoMentionPresentationModel' => __DIR__ . '/includes/formatters/MentionPresentationModel.php',
'EchoMultipleIterator' => __DIR__ . '/includes/iterator/MultipleIterator.php',
'EchoNotRecursiveIterator' => __DIR__ . '/includes/iterator/NotRecursiveIterator.php',
'EchoNotification' => __DIR__ . '/includes/model/Notification.php',
@ -87,6 +91,7 @@ $wgAutoloadClasses += array(
'EchoUserNotificationGateway' => __DIR__ . '/includes/gateway/UserNotificationGateway.php',
'EchoUserNotificationGatewayTest' => __DIR__ . '/tests/phpunit/gateway/UserNotificationGatewayTest.php',
'EchoUserRightsFormatter' => __DIR__ . '/includes/formatters/UserRightsFormatter.php',
'EchoUserRightsPresentationModel' => __DIR__ . '/includes/formatters/UserRightsPresentationModel.php',
'FilteredSequentialIteratorTest' => __DIR__ . '/tests/phpunit/iterator/FilteredSequentialIteratorTest.php',
'MWEchoDbFactory' => __DIR__ . '/includes/EchoDbFactory.php',
'MWEchoDbFactoryTest' => __DIR__ . '/tests/phpunit/EchoDbFactoryTest.php',

View file

@ -69,10 +69,12 @@
"notification-add-comment-yours2": "[[User:$1|$1]] {{GENDER:$1|commented}} on \"[[$3#$2|$2]]\" on your talk page.",
"notification-mention": "[[User:$1|$1]] {{GENDER:$1|mentioned}} you on the $5 talk page in \"[[:$3#$2|$4]]\".",
"notification-mention-flyout": "$1 {{GENDER:$1|mentioned}} you on the $5 talk page in \"[[:$3#$2|$4]]\".",
"notification-header-mention": "$1 {{GENDER:$2|mentioned}} you on the $3 talk page in \"$4\".",
"notification-mention-nosection": "[[User:$1|$1]] {{GENDER:$1|mentioned}} you on the [[:$3|$2 talk page]].",
"notification-mention-nosection-flyout": "$1 {{GENDER:$1|mentioned}} you on the [[:$3|$2 talk page]].",
"notification-header-mention-nosection": "$1 {{GENDER:$2|mentioned}} you on the [[:$4|$3 talk page]].",
"notification-user-rights": "Your user rights [[Special:Log/rights/$1|were {{GENDER:$1|changed}}]] by [[User:$1|$1]]. $2. [[Special:ListGroupRights|Learn more]]",
"notification-user-rights-flyout": "Your user rights were {{GENDER:$1|changed}} by $1. $2. [[Special:ListGroupRights|Learn more]]",
"notification-header-user-rights": "Your user rights were {{GENDER:$2|changed}} by $1. $3. [[Special:ListGroupRights|Learn more]]",
"notification-user-rights-add": "You are now a member of {{PLURAL:$2|this group|these groups}}: $1",
"notification-user-rights-remove": "You are no longer a member of {{PLURAL:$2|this group|these groups}}: $1",
"notification-new-user": "Welcome to {{SITENAME}}, $1! We're glad you're here.",

View file

@ -90,10 +90,12 @@
"notification-add-comment-yours2": "Parameters:\n* $1 - a username, plain text; can be used for GENDER\n* $2 - discussion name\n* $3 - link to user talk page\nSee also:\n* {{msg-mw|Notification-add-comment2}}",
"notification-mention": "Format for displaying notifications of a comment in a specific section including a link to another user's user page.\n\nParameters:\n* $1 - the username of the person who edited, plain text. Can be used for GENDER\n* $2 - the section title of the discussion\n* $3 - the page title of the discussion\n* $4 - the raw section title text\n* $5 - the title text without namespace (a page title in any namespace)",
"notification-mention-flyout": "Flyout-specific format for displaying notifications of a comment in a specific section.\nParameters:\n* $1 - the username of the person who mentioned you, plain text. Can be used for GENDER.\n* $2 - the section title of the discussion\n* $3 - the page title of the discussion\n* $4 - the raw section title text\n* $5 - the title text without namespace (a page title in any namespace)",
"notification-header-mention": "Header text for a notification when you are mentioned by another user. $1 is that user's name (not suitable for GENDER). $2 is the user's name for use in GENDER. $3 is the name of the page without namespace they were mentioned in. $4 is a link to the section they were mentioned in.",
"notification-header-mention-nosection": "Header text for a notification when you are mentioned by another user, but not in a section of a page. $1 is that user's name (not suitable for GENDER). $2 is the user's name for use in GENDER. $3 is the name of the page without namespace they were mentioned in. $4 is the full page name, for use in a link.",
"notification-mention-nosection": "Format for displaying notifications of a comment including a link to another user's user page. Parameters:\n* $1 - the username of the person who edited, plain text. Can be used for GENDER\n* $2 - the title text without namespace (a page title in any namespace)\n* $3 - the page title of the discussion",
"notification-mention-nosection-flyout": "Flyout-specific format for displaying notifications of a comment.\nParameters:\n* $1 - the username of the person who edited, plain text. Can be used for GENDER\n* $2 - the title text without namespace (a page title in any namespace)\n* $3 - the page title of the discussion",
"notification-user-rights": "Format for displaying notifications of a user right change in notification page.\n\nParameters:\n* $1 - the username of the person who made the user right change. Can be used for GENDER support.\n* $2 - a semicolon separated list of {{msg-mw|Notification-user-rights-add}}, {{msg-mw|Notification-user-rights-remove}}",
"notification-user-rights-flyout": "Format for displaying notifications of a user right change in notification flyout. Parameters:\n* $1 - the username of the person who made the user right change. Can be used for GENDER support\n* $2 - a semicolon separated list of {{msg-mw|notification-user-rights-add}}, {{msg-mw|notification-user-rights-remove}}",
"notification-header-user-rights": "Format for displaying notifications of a user right change in notification flyout. Parameters:\n* $1 - the username of the person who made the user right change, formatted for display. Cannot be used for GENDER\n* $2 - the raw username of the person who made the user rights change, can be used for GENDER support\n* $3 - a semicolon separated list of {{msg-mw|notification-user-rights-add}}, {{msg-mw|notification-user-rights-remove}}",
"notification-user-rights-add": "Message indicating that a user was added to a user group. Parameters:\n* $1 - a comma separated list of user group names\n* $2 - the number of user groups, this is used for PLURAL support\nSee also:\n* {{msg-mw|Notification-user-rights-remove}}",
"notification-user-rights-remove": "Message indicating that a user was removed from a user group. Parameters:\n* $1 - a comma separated list of user group names\n* $2 - the number of user groups, this is used for PLURAL support\nSee also:\n* {{msg-mw|Notification-user-rights-add}}",
"notification-new-user": "Text of the welcome notification. Parameters:\n* $1 - the name of the new user\nSee also:\n* {{msg-mw|Guidedtour-tour-gettingstarted-start-title}}",

View file

@ -5,6 +5,13 @@
*/
class EchoDataOutputFormatter {
/**
* @var array type => class
*/
static $formatters = array(
'flyout' => 'EchoFlyoutFormatter'
);
/**
* Format a notification for a user in the format specified
*
@ -106,12 +113,28 @@ class EchoDataOutputFormatter {
}
if ( $format ) {
$output['*'] = EchoNotificationController::formatNotification( $event, $user, $format );
$output['*'] = self::formatNotification( $event, $user, $format );
}
return $output;
}
protected static function formatNotification( EchoEvent $event, User $user, $format ) {
global $wgLang;
if ( isset( self::$formatters[$format] )
&& EchoEventPresentationModel::supportsPresentationModel( $event->getType() )
) {
// FIXME don't use $wgLang. It's ok because this is only used for the API or Special page, and not
// emails yet.
/** @var EchoEventFormatter $formatter */
$formatter = new self::$formatters[$format]( $user, $wgLang );
return $formatter->format( $event );
} else {
// Legacy b/c
return EchoNotificationController::formatNotification( $event, $user, $format );
}
}
/**
* Get the date header in user's format, 'May 10' or '10 May', depending
* on user's date format preference

View file

@ -0,0 +1,25 @@
<?php
/**
* Abstract class that each "formatter" should implement.
*
* A formatter is an output type, example formatters would be:
* * Special:Notifications
* * HTML email
* * plaintext email
*
* The formatter does not maintain any state except for the
* arguments passed in the constructor (user and language)
*/
abstract class EchoEventFormatter {
public function __construct( User $user, Language $language ) {
$this->user = $user;
$this->language = $language;
}
/**
* @param EchoEvent $event
* @return string HTML
*/
abstract public function format( EchoEvent $event );
}

View file

@ -0,0 +1,69 @@
<?php
/**
* A formatter for the notification flyout popup
*
* Ideally we wouldn't need this and we'd just pass the
* presentation model to the client, but we need to continue
* sending HTML for backwards compatibility.
*/
class EchoFlyoutFormatter extends EchoEventFormatter {
public function format( EchoEvent $event ) {
$model = EchoEventPresentationModel::factory( $event, $this->language, $this->user );
$icon = Html::element(
'img',
array(
'class' => 'mw-echo-icon',
'src' => $this->getIconURL( $model ),
)
);
$html = Xml::tags(
'div',
array( 'class' => 'mw-echo-title' ),
$model->getHeaderMessage()->parse()
) . "\n";
//@todo body text
$ts = $this->language->getHumanTimestamp(
new MWTimestamp( $event->getTimestamp() ),
null,
$this->user
);
$footerItems = array( $ts );
foreach ( $model->getSecondaryLinks() as $target => $text ) {
$footerItems[] = Html::element( 'a', array( 'href' => $target ), $text );
}
$html .= Xml::tags(
'div',
array( 'class' => 'mw-echo-notification-footer' ),
$this->language->pipeList( $footerItems )
) . "\n";
// Add the primary link afterwards???
list( $primaryUrl, $primaryText ) = $model->getPrimaryLink();
$html .= Html::element(
'a',
array( 'class' => 'mw-echo-notification-primary-link', 'href' => $primaryUrl ),
$primaryText
) . "\n";
// Wrap everything in mw-echo-content class
$html = Xml::tags( 'div', array( 'class' => 'mw-echo-content' ), $html );
// And then add the icon in front and wrap with mw-echo-state class.
$html = Xml::tags( 'div', array( 'class' => 'mw-echo-state' ), $icon . $html );
return $html;
}
private function getIconURL( EchoEventPresentationModel $model ) {
return EchoNotificationFormatter::getIconUrl(
$model->getIconType(),
$this->language->getDir()
);
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Class that returns structured data based
* on the provided event.
*/
abstract class EchoEventPresentationModel {
/**
* @var EchoEvent
*/
protected $event;
/**
* @var Language
*/
protected $language;
/**
* @var string
*/
protected $type;
/**
* @var User for permissions checking
*/
private $user;
/**
* @param EchoEvent $event
* @param Language|string $language
* @param User $user Only used for permissions checking
*/
protected function __construct( EchoEvent $event, $language, User $user ) {
$this->event = $event;
$this->type = $event->getType();
$this->language = wfGetLangObj( $language );
$this->user = $user;
}
/**
* Convenience function to detect whether the event type
* has been updated to use the presentation model system
*
* @param string $type event type
* @return bool
*/
public static function supportsPresentationModel( $type ) {
global $wgEchoNotifications;
return isset( $wgEchoNotifications[$type]['presentation-model'] );
}
/**
* @param EchoEvent $event
* @param Language|string $language
* @param User $user
* @return EchoEventPresentationModel
*/
public static function factory( EchoEvent $event, $language, User $user ) {
global $wgEchoNotifications;
// @todo don't depend upon globals
$class = $wgEchoNotifications[$event->getType()]['presentation-model'];
return new $class( $event, $language, $user );
}
/**
* Equivalent to IContextSource::msg for the current
* language
*
* @return Message
*/
protected function msg( /* ,,, */ ) {
/**
* @var Message $msg
*/
$msg = call_user_func_array( 'wfMessage', func_get_args() );
$msg->inLanguage( $this->language );
return $msg;
}
/**
* @return string The symbolic icon name as defined in $wgEchoNotificationIcons
*/
abstract public function getIconType();
/**
* Helper for EchoEvent::userCan
*
* @param int $type Revision::DELETED_* constant
* @return bool
*/
final protected function userCan( $type ) {
return $this->event->userCan( $type, $this->user );
}
/**
* @return array|bool ['wikitext to display', 'username for GENDER'], false if no agent
*
* We have to display wikitext so we can add CSS classes for revision deleted user.
* The goal of this function is for callers not to worry about whether
* the user is visible or not.
* @par Example:
* @code
* list( $formattedName, $genderName ) = $this->getAgentForOutput();
* $msg->params( $formattedName, $genderName );
* @endcode
*/
final protected function getAgentForOutput() {
$agent = $this->event->getAgent();
if ( !$agent ) {
return false;
}
if ( $this->userCan( Revision::DELETED_USER ) ) {
// Not deleted
return array( $agent->getName(), $agent->getName() );
} else {
// Deleted/hidden
$msg = $this->msg( 'rev-deleted-user' )->plain();
// HACK: Pass an invalid username to GENDER to force the default
return array( '<span class="history-deleted">' . $msg . '</span>', '[]' );
}
}
/**
* @return string Message key that will be used in getHeaderMessage
*/
protected function getHeaderMessageKey() {
return "notification-header-{$this->type}";
}
/**
* Get a message object and add the performer's name as
* a parameter. It is expected that subclasses will override
* this.
*
* @return Message
*/
public function getHeaderMessage() {
$msg = $this->msg( $this->getHeaderMessageKey() );
list( $formattedName, $genderName ) = $this->getAgentForOutput();
$msg->params( $formattedName, $genderName );
return $msg;
}
/**
* Get the body text, false if notification has no body
*
* @return bool|Message
*/
public function getBodyText() {
return false;
}
/**
* Possibly-relative URL to the primary link for this
*
* @return [array URL, link text (non-escaped)]
*/
abstract public function getPrimaryLink();
/**
* Possibly-relative URLs to the secondary links
*
* @return array URL => link text
*/
public function getSecondaryLinks() {
return array();
}
}

View file

@ -0,0 +1,106 @@
<?php
class EchoMentionPresentationModel extends EchoEventPresentationModel {
private $sectionTitle = null;
public function getIconType() {
return 'chat';
}
private function getSection() {
if ( $this->sectionTitle !== null ) {
return $this->sectionTitle;
}
$sectionTitle = $this->event->getExtraParam( 'section-title' );
if ( !$sectionTitle ) {
$this->sectionTitle = false;
return false;
}
// Check permissions
if ( !$this->userCan( Revision::DELETED_TEXT ) ) {
$this->sectionTitle = false;
return false;
}
$this->sectionTitle = $sectionTitle;
return $this->sectionTitle;
}
/**
* Override to switch the message key to -nosection
* if no section title was detected
*
* @return string
*/
protected function getHeaderMessageKey() {
// Messages used:
// notification-header-mention
// notification-header-mention-nosection
$key = parent::getHeaderMessageKey();
if ( !$this->getSection() ) {
$key .= '-nosection';
}
return $key;
}
public function getHeaderMessage() {
$msg = parent::getHeaderMessage();
// @fixme this message should not say "xx talk page"
$msg->params( $this->event->getTitle()->getText() );
$section = $this->getSection();
$sectionTitle = $this->getTitleWithSection();
if ( $section ) {
$msg->rawParams(
Linker::link(
$sectionTitle,
htmlspecialchars( EchoDiscussionParser::getTextSnippet(
$section,
$this->language,
30
) )
)
);
} else {
// For the -nosection message
$msg->params( $sectionTitle->getPrefixedText() );
}
return $msg;
}
/**
* @return Title
*/
private function getTitleWithSection() {
$title = $this->event->getTitle();
$section = $this->getSection();
if ( $section ) {
$title = Title::makeTitle(
$title->getNamespace(),
$title->getDBkey(),
$section
);
}
return $title;
}
public function getPrimaryLink() {
return array(
$this->getTitleWithSection()->getLocalURL(),
$this->msg( 'notification-link-text-view-mention' )->text()
);
}
public function getSecondaryLinks() {
$url = $this->event->getTitle()->getLocalURL( array(
'oldid' => 'prev',
'diff' => $this->event->getExtraParam( 'revid' )
) );
return array(
$url => $this->msg( 'notification-link-text-view-changes' )->text()
);
}
}

View file

@ -70,3 +70,4 @@ class EchoUserRightsFormatter extends EchoBasicFormatter {
return array( $target, $query );
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* Formatter for 'user-rights' notifications
*/
class EchoUserRightsPresentationModel extends EchoEventPresentationModel {
public function getIconType() {
return 'site';
}
public function getHeaderMessage() {
$msg = parent::getHeaderMessage();
// @todo fix lego message
$msg->params( $this->getChangedGroups() );
return $msg;
}
/**
* @return string
*/
private function getChangedGroups() {
$list = array();
$extra = $this->event->getExtra();
foreach ( array( 'add', 'remove' ) as $action ) {
if ( isset( $extra[$action] ) && $extra[$action] ) {
// Get the localized group names, bug 55338
$groups = array();
foreach( $extra[$action] as $group ) {
$msg = $this->msg( 'group-' . $group );
$groups[] = $msg->isBlank() ? $group : $msg->text();
}
// Messages that can be used here:
// * notification-user-rights-add
// * notification-user-rights-remove
$list[] = $this->msg( 'notification-user-rights-' . $action )
->params( $this->language->commaList( $groups ), count( $groups ) )
->text();
}
}
return $this->language->semicolonList( $list );
}
public function getPrimaryLink() {
return array(
SpecialPage::getTitleFor( 'Listgrouprights' )->getLocalURL(),
$this->msg( 'echo-learn-more' )->text()
);
}
}