jenkins-bot has submitted this change and it was merged. Change subject: Add EditActionHookHandler in client to inject entity usage data ......................................................................
Add EditActionHookHandler in client to inject entity usage data Bug: T144921 Change-Id: I1f1fad785545be475f1db8c12d1647ec449a29d7 --- M client/WikibaseClient.hooks.php M client/WikibaseClient.php M client/i18n/qqq.json A client/includes/Hooks/EditActionHookHandler.php M client/resources/Resources.php A client/resources/wikibase.client.action.edit.collapsibleFooter.js A client/tests/phpunit/includes/Hooks/EditActionHookHandlerTest.php 7 files changed, 538 insertions(+), 2 deletions(-) Approvals: Hoo man: Looks good to me, approved jenkins-bot: Verified diff --git a/client/WikibaseClient.hooks.php b/client/WikibaseClient.hooks.php index 1a5ee70..14a013a 100644 --- a/client/WikibaseClient.hooks.php +++ b/client/WikibaseClient.hooks.php @@ -6,6 +6,7 @@ use BaseTemplate; use ChangesList; use EchoEvent; +use EditPage; use IContextSource; use Message; use OutputPage; @@ -22,6 +23,7 @@ use Wikibase\Client\Hooks\BeforePageDisplayHandler; use Wikibase\Client\Hooks\DeletePageNoticeCreator; use Wikibase\Client\Hooks\EchoNotificationsHandlers; +use Wikibase\Client\Hooks\EditActionHookHandler; use Wikibase\Client\Hooks\InfoActionHookHandler; use Wikibase\Client\RecentChanges\ChangeLineFormatter; use Wikibase\Client\RecentChanges\ExternalChangeFactory; @@ -438,6 +440,28 @@ } /** + * Adds the Entity usage data in ActionEdit + * + * @param EditPage $editor + * @param string[] $checkboxes + * @param int $tabindex + */ + public static function onEditAction( EditPage &$editor, array &$checkboxes, &$tabindex ) { + if ( $editor->preview || $editor->section ) { + // Shorten out, like template transclusion in core + return; + } + + $editActionHookHandler = EditActionHookHandler::newFromGlobalState( + $editor->getContext() + ); + $editActionHookHandler->handle( $editor ); + + $out = $editor->getContext()->getOutput(); + $out->addModules( 'wikibase.client.action.edit.collapsibleFooter' ); + } + + /** * Notify the user that we have automatically updated the repo or that they * need to do that per hand. * diff --git a/client/WikibaseClient.php b/client/WikibaseClient.php index 7b34a44..6df21ae 100644 --- a/client/WikibaseClient.php +++ b/client/WikibaseClient.php @@ -122,6 +122,7 @@ $wgHooks['BeforePageDisplay'][] = '\Wikibase\ClientHooks::onBeforePageDisplayAddJsConfig'; $wgHooks['ScribuntoExternalLibraries'][] = '\Wikibase\ClientHooks::onScribuntoExternalLibraries'; $wgHooks['InfoAction'][] = '\Wikibase\ClientHooks::onInfoAction'; + $wgHooks['EditPageBeforeEditChecks'][] = '\Wikibase\ClientHooks::onEditAction'; $wgHooks['BaseTemplateAfterPortlet'][] = '\Wikibase\ClientHooks::onBaseTemplateAfterPortlet'; $wgHooks['GetBetaFeaturePreferences'][] = '\Wikibase\ClientHooks::onGetBetaFeaturePreferences'; $wgHooks['ArticleDeleteAfterSuccess'][] = '\Wikibase\ClientHooks::onArticleDeleteAfterSuccess'; diff --git a/client/i18n/qqq.json b/client/i18n/qqq.json index 012b245..6bb1a02 100644 --- a/client/i18n/qqq.json +++ b/client/i18n/qqq.json @@ -110,7 +110,7 @@ "wikibase-entityusage-submit": "Label for the button that activates the action", "wikibase-pageinfo-entity-id": "A link to the corresponding Wikibase Item", "wikibase-pageinfo-entity-id-none": "The page is not linked with a wikibase item.\n{{Identical|None}}", - "wikibase-pageinfo-entity-usage": "Desciption in action=info about entities used in the page", + "wikibase-pageinfo-entity-usage": "Desciption in action=info and action=edit about entities used in the page", "wikibase-pageinfo-entity-usage-S": "Name for ''sitelink'' entity usage", "wikibase-pageinfo-entity-usage-L": "Name for ''label'' entity usage\n{{Identical|Label}}", "wikibase-pageinfo-entity-usage-T": "Name for ''title'' entity usage\n{{Identical|Title}}", diff --git a/client/includes/Hooks/EditActionHookHandler.php b/client/includes/Hooks/EditActionHookHandler.php new file mode 100644 index 0000000..c25ad2e --- /dev/null +++ b/client/includes/Hooks/EditActionHookHandler.php @@ -0,0 +1,179 @@ +<?php + +namespace Wikibase\Client\Hooks; + +use EditPage; +use Html; +use IContextSource; +use Wikibase\Client\RepoLinker; +use Wikibase\Client\Usage\EntityUsage; +use Wikibase\Client\Usage\UsageLookup; +use Wikibase\Client\WikibaseClient; +use Wikibase\DataModel\Entity\EntityIdParser; +use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookupFactory; + +/** + * @since 0.5 + * + * @license GPL-2.0+ + * @author Amir Sarabadani < ladsgr...@gmail.com > + */ +class EditActionHookHandler { + + /** + * @var RepoLinker + */ + private $repoLinker; + + /** + * @var UsageLookup + */ + private $usageLookup; + + /** + * @var LanguageFallbackLabelDescriptionLookupFactory + */ + private $labelDescriptionLookupFactory; + + /** + * @var EntityIdParser + */ + private $idParser; + + /** + * @var IContextSource + */ + private $context; + + public function __construct( + RepoLinker $repoLinker, + UsageLookup $usageLookup, + LanguageFallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory, + EntityIdParser $idParser, + IContextSource $context + ) { + $this->repoLinker = $repoLinker; + $this->usageLookup = $usageLookup; + $this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory; + $this->idParser = $idParser; + $this->context = $context; + } + + /** + * @param IContextSource $context + * @return EditActionHookHandler + */ + public static function newFromGlobalState( IContextSource $context ) { + $wikibaseClient = WikibaseClient::getDefaultInstance(); + + $usageLookup = $wikibaseClient->getStore()->getUsageLookup(); + $labelDescriptionLookupFactory = new LanguageFallbackLabelDescriptionLookupFactory( + $wikibaseClient->getLanguageFallbackChainFactory(), + $wikibaseClient->getTermLookup(), + $wikibaseClient->getTermBuffer() + ); + $idParser = $wikibaseClient->getEntityIdParser(); + + return new self( + $wikibaseClient->newRepoLinker(), + $usageLookup, + $labelDescriptionLookupFactory, + $idParser, + $context + ); + } + + /** + * @param EditPage $editor + */ + public function handle( EditPage $editor ) { + // Check if there are usages to show + $title = $editor->getTitle(); + $usages = $this->usageLookup->getUsagesForPage( $title->getArticleID() ); + + if ( $usages ) { + $header = $this->getHeader(); + $usageOutput = $this->formatEntityUsage( $usages ); + $output = Html::rawElement( + 'div', + [ 'class' => 'wikibase-entity-usage' ], + $header . "\n" . $usageOutput + ); + $editor->editFormTextAfterTools .= $output; + } + } + + /** + * @param string[] $rowAspects + * + * @return string HTML + */ + private function formatAspects( array $rowAspects ) { + $aspects = []; + + foreach ( $rowAspects as $aspect ) { + $aspects[] = $this->context->msg( + 'wikibase-pageinfo-entity-usage-' . $aspect[0], $aspect[1] + )->parse(); + } + + return $this->context->getLanguage()->commaList( $aspects ); + } + + /** + * @param EntityUsage[] $usages + * @return string HTML + */ + private function formatEntityUsage( array $usages ) { + $usageAspectsByEntity = []; + $entityIds = []; + + foreach ( $usages as $key => $entityUsage ) { + $entityId = $entityUsage->getEntityId()->getSerialization(); + $entityIds[$entityId] = $entityUsage->getEntityId(); + if ( !isset( $usageAspectsByEntity[$entityId] ) ) { + $usageAspectsByEntity[$entityId] = []; + } + $usageAspectsByEntity[$entityId][] = [ + $entityUsage->getAspect(), + $entityUsage->getModifier() + ]; + } + + $output = ''; + $labelLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup( + $this->context->getLanguage(), + array_values( $entityIds ) + ); + + foreach ( $usageAspectsByEntity as $entityId => $aspects ) { + $label = $labelLookup->getLabel( $entityIds[$entityId] ); + $text = $label === null ? $entityId : $label->getText(); + + $aspectContent = $this->formatAspects( $aspects ); + $colon = $this->context->msg( 'colon-separator' )->plain(); + $output .= Html::rawElement( + 'li', + [], + $this->repoLinker->buildEntityLink( + $entityIds[$entityId], + [ 'external' ], + $text + ) . $colon . $aspectContent + ); + } + return Html::rawElement( 'ul', [], $output ); + } + + /** + * @return string HTML + */ + private function getHeader() { + return Html::rawElement( + 'div', + [ 'class' => 'wikibase-entityusage-explanation' ], + $this->context->msg( 'wikibase-pageinfo-entity-usage' )->parseAsBlock() + ); + } + +} diff --git a/client/resources/Resources.php b/client/resources/Resources.php index 666fafa..deb1daf 100644 --- a/client/resources/Resources.php +++ b/client/resources/Resources.php @@ -105,7 +105,15 @@ 'wikibase-sitelinks-sitename-columnheading', 'wikibase-sitelinks-link-columnheading' ), - ) + ), + 'wikibase.client.action.edit.collapsibleFooter' => $moduleTemplate + [ + 'scripts' => 'wikibase.client.action.edit.collapsibleFooter.js', + 'dependencies' => [ + 'jquery.makeCollapsible', + 'mediawiki.cookie', + 'mediawiki.icon', + ], + ] ); } ); diff --git a/client/resources/wikibase.client.action.edit.collapsibleFooter.js b/client/resources/wikibase.client.action.edit.collapsibleFooter.js new file mode 100644 index 0000000..fe25cd5 --- /dev/null +++ b/client/resources/wikibase.client.action.edit.collapsibleFooter.js @@ -0,0 +1,55 @@ +// Copied from mediawiki.action.edit.collapsibleFooter +( function ( mw ) { + 'use strict'; + + var collapsibleLists, handleOne; + + // Collapsible lists of categories and templates + collapsibleLists = [ + { + listSel: '.wikibase-entity-usage ul', + togglerSel: '.wikibase-entityusage-explanation', + cookieName: 'wikibase-entity-usage-list' + } + ]; + + handleOne = function ( $list, $toggler, cookieName ) { + // Collapsed by default + var isCollapsed = mw.cookie.get( cookieName ) !== 'expanded'; + + // Style the toggler with an arrow icon and add a tabIndex and a role for accessibility + $toggler.addClass( 'mw-editfooter-toggler' ).prop( 'tabIndex', 0 ).attr( 'role', 'button' ); + $list.addClass( 'mw-editfooter-list' ); + + $list.makeCollapsible( { + $customTogglers: $toggler, + linksPassthru: true, + plainMode: true, + collapsed: isCollapsed + } ); + + $toggler.addClass( isCollapsed ? 'mw-icon-arrow-collapsed' : 'mw-icon-arrow-expanded' ); + + $list.on( 'beforeExpand.mw-collapsible', function () { + $toggler.removeClass( 'mw-icon-arrow-collapsed' ).addClass( 'mw-icon-arrow-expanded' ); + mw.cookie.set( cookieName, 'expanded' ); + } ); + + $list.on( 'beforeCollapse.mw-collapsible', function () { + $toggler.removeClass( 'mw-icon-arrow-expanded' ).addClass( 'mw-icon-arrow-collapsed' ); + mw.cookie.set( cookieName, 'collapsed' ); + } ); + }; + + mw.hook( 'wikipage.editform' ).add( function ( $editForm ) { + var i; + for ( i = 0; i < collapsibleLists.length; i++ ) { + // Pass to a function for iteration-local variables + handleOne( + $editForm.find( collapsibleLists[ i ].listSel ), + $editForm.find( collapsibleLists[ i ].togglerSel ), + collapsibleLists[ i ].cookieName + ); + } + } ); +}( mediaWiki ) ); diff --git a/client/tests/phpunit/includes/Hooks/EditActionHookHandlerTest.php b/client/tests/phpunit/includes/Hooks/EditActionHookHandlerTest.php new file mode 100644 index 0000000..42cb906 --- /dev/null +++ b/client/tests/phpunit/includes/Hooks/EditActionHookHandlerTest.php @@ -0,0 +1,269 @@ +<?php + +namespace Wikibase\Client\Tests\Hooks; + +use EditPage; +use Html; +use IContextSource; +use RequestContext; +use Title; +use Wikibase\Client\Hooks\EditActionHookHandler; +use Wikibase\Client\RepoLinker; +use Wikibase\Client\Usage\Sql\SqlUsageTracker; +use Wikibase\DataModel\Entity\EntityId; +use Wikibase\DataModel\Entity\EntityIdParser; +use Wikibase\DataModel\Entity\ItemId; +use Wikibase\DataModel\Services\Lookup\LabelDescriptionLookup; +use Wikibase\DataModel\Term\Term; +use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookupFactory; +use Wikibase\Lib\Store\SiteLinkLookup; +use Wikibase\Client\Usage\EntityUsage; + +/** + * @covers Wikibase\Client\Hooks\EditActionHookHandler + * + * @group WikibaseClient + * @group EditActionHookHandler + * @group Wikibase + * + * @license GPL-2.0+ + * @author Amir Sarabadani <ladsgr...@gmail.com> + */ +class EditActionHookHandlerTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider handleProvider + * @param string HTML $expected + * @param IContextSource $context + * @param EntityId|bool $entityId + * @param string $message + */ + public function testHandle( $expected, IContextSource $context, $entityId, $message ) { + $hookHandler = $this->newHookHandler( $entityId, $context ); + $editor = $this->getEditPage(); + $hookHandler->handle( $editor ); + + $this->assertSame( $expected, $editor->editFormTextAfterTools, $message ); + } + + public function testNewFromGlobalState() { + $context = $this->getContext(); + + $handler = EditActionHookHandler::newFromGlobalState( $context ); + $this->assertInstanceOf( EditActionHookHandler::class, $handler ); + + } + + public function handleProvider() { + $context = $this->getContext(); + $labeledLink = '<a href="https://www.wikidata.org/wiki/Q4" class="external">Berlin</a>'; + $q5Link = '<a href="https://www.wikidata.org/wiki/Q5" class="external">Q5</a>'; + $explanation = $context->msg( 'wikibase-pageinfo-entity-usage' )->escaped(); + $header = '<div class="wikibase-entity-usage"><div class="wikibase-entityusage-explanation">'; + $header .= "<p>$explanation\n</p></div>"; + $cases = []; + + $cases[] = [ + "$header\n<ul><li>$labeledLink: Sitelink</li></ul></div>", + $context, + new ItemId( 'Q4' ), + 'item id link' + ]; + + $cases[] = [ + '', + $context, + false, + 'page is not connected to an item' + ]; + + $cases[] = [ + "$header\n<ul><li>$q5Link: Sitelink</li></ul></div>", + $context, + new ItemId( 'Q5' ), + 'No label for Q5' + ]; + + return $cases; + } + + /** + * @param ItemId|bool $entityId + * @param IContextSource $context + * + * @return EditActionHookHandler + */ + private function newHookHandler( $entityId, IContextSource $context ) { + + $repoLinker = $this->getMockBuilder( RepoLinker::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'buildEntityLink' ] ) + ->getMock(); + + $repoLinker->expects( $this->any() ) + ->method( 'buildEntityLink' ) + ->will( $this->returnCallback( [ $this, 'buildEntityLink' ] ) ); + + $siteLinkLookup = $this->getMockBuilder( SiteLinkLookup::class ) + ->disableOriginalConstructor() + ->getMock(); + + $siteLinkLookup->expects( $this->any() ) + ->method( 'getItemIdForLink' ) + ->will( $this->returnValue( $entityId ) ); + + $sqlUsageTracker = $this->getMockBuilder( SqlUsageTracker::class ) + ->disableOriginalConstructor() + ->getMock(); + + $entityUsage = $entityId ? [ new EntityUsage( $entityId, 'S' ) ] : null; + $sqlUsageTracker->expects( $this->once() ) + ->method( 'getUsagesForPage' ) + ->will( $this->returnValue( $entityUsage ) ); + + $labelDescriptionLookupFactory = $this->getMockBuilder( + LanguageFallbackLabelDescriptionLookupFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $labelDescriptionLookupFactory->expects( $this->any() ) + ->method( 'newLabelDescriptionLookup' ) + ->will( $this->returnCallback( [ $this, 'newLabelDescriptionLookup' ] ) ); + + $idParser = $this->getMockBuilder( EntityIdParser::class ) + ->disableOriginalConstructor() + ->getMock(); + + $idParser->expects( $this->any() ) + ->method( 'parse' ) + ->will( $this->returnCallback( [ $this, 'parse' ] ) ); + + $hookHandler = new EditActionHookHandler( + $repoLinker, + $sqlUsageTracker, + $labelDescriptionLookupFactory, + $idParser, + $context + ); + + return $hookHandler; + } + + /** + * @return IContextSource + */ + private function getContext() { + $title = $this->getTitle(); + + $context = new RequestContext(); + $context->setTitle( $title ); + + $context->setLanguage( 'en' ); + + return $context; + } + + /** + * @return LabelDescriptionLookup + */ + public function newLabelDescriptionLookup() { + $lookup = $this->getMockBuilder( LabelDescriptionLookup::class ) + ->disableOriginalConstructor() + ->getMock(); + + $lookup->expects( $this->any() ) + ->method( 'getLabel' ) + ->will( $this->returnCallback( [ $this, 'getLabel' ] ) ); + + return $lookup; + } + + /** + * @param EntityId $entity + * + * @return Term|null + */ + public function getLabel( EntityId $entity ) { + $labelMap = [ 'Q4' => 'Berlin' ]; + $idSerialization = $entity->getSerialization(); + if ( !isset( $labelMap[$idSerialization] ) ) { + return null; + } + $term = new Term( 'en', $labelMap[$idSerialization] ); + return $term; + } + + /** + * @param string $entity + * + * @return ItemId + */ + public function parse( $entity ) { + // TODO: Let properties be tested too + return new ItemId( $entity ); + } + + /** + * @param string $entityId + * @param string[] $classes + * @param string|null $text + * + * @return string HTML + */ + public function buildEntityLink( $entityId, array $classes, $text = null ) { + if ( $text === null ) { + $text = $entityId; + } + + $attr = [ + 'href' => 'https://www.wikidata.org/wiki/' . $entityId, + 'class' => implode( ' ', $classes ) + ]; + + return Html::rawElement( 'a', $attr, $text ); + } + + /** + * @return EditPage + */ + private function getEditPage() { + $title = $this->getTitle(); + + $editor = $this->getMockBuilder( EditPage::class ) + ->disableOriginalConstructor() + ->getMock(); + + $editor->expects( $this->any() ) + ->method( 'getTitle' ) + ->will( $this->returnValue( $title ) ); + + $editor->editFormTextAfterTools = ''; + + return $editor; + } + + /** + * @return Title + */ + private function getTitle() { + $title = $this->getMockBuilder( Title::class ) + ->disableOriginalConstructor() + ->getMock(); + + $title->expects( $this->any() ) + ->method( 'exists' ) + ->will( $this->returnValue( true ) ); + + $title->expects( $this->any() ) + ->method( 'getNamespace' ) + ->will( $this->returnValue( NS_MAIN ) ); + + $title->expects( $this->any() ) + ->method( 'getPrefixedText' ) + ->will( $this->returnValue( 'Cat' ) ); + + return $title; + } + +} -- To view, visit https://gerrit.wikimedia.org/r/313629 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I1f1fad785545be475f1db8c12d1647ec449a29d7 Gerrit-PatchSet: 18 Gerrit-Project: mediawiki/extensions/Wikibase Gerrit-Branch: master Gerrit-Owner: Ladsgroup <ladsgr...@gmail.com> Gerrit-Reviewer: Daniel Kinzler <daniel.kinz...@wikimedia.de> Gerrit-Reviewer: Hoo man <h...@online.de> Gerrit-Reviewer: Jakob <jakob.warkot...@wikimedia.de> Gerrit-Reviewer: Ladsgroup <ladsgr...@gmail.com> Gerrit-Reviewer: Matěj Suchánek <matejsuchane...@gmail.com> Gerrit-Reviewer: Siebrand <siebr...@kitano.nl> Gerrit-Reviewer: WMDE-leszek <leszek.mani...@wikimedia.de> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits