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

Reply via email to