jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/362215 )
Change subject: Use language fallback when showing descriptions in search
results
......................................................................
Use language fallback when showing descriptions in search results
At the beginning, there was just a todo comment in the code.
A new hook handler has been created to make it testable.
Change-Id: I45039f579054c0130162c9e14c87c94dfe3cb1c3
---
M repo/Wikibase.hooks.php
M repo/Wikibase.php
A repo/includes/Hooks/ShowSearchHitHandler.php
A repo/tests/phpunit/includes/Hooks/ShowSearchHitHandlerTest.php
4 files changed, 404 insertions(+), 49 deletions(-)
Approvals:
jenkins-bot: Verified
Thiemo Mättig (WMDE): Looks good to me, approved
diff --git a/repo/Wikibase.hooks.php b/repo/Wikibase.hooks.php
index ced38e0..d6563a1 100644
--- a/repo/Wikibase.hooks.php
+++ b/repo/Wikibase.hooks.php
@@ -29,7 +29,6 @@
use StubUserLang;
use Title;
use User;
-use Wikibase\DataModel\Term\DescriptionsProvider;
use Wikibase\Lib\AutoCommentFormatter;
use Wikibase\Lib\Store\Sql\EntityChangeLookup;
use Wikibase\Repo\Content\EntityHandler;
@@ -540,53 +539,6 @@
}
return true;
- }
-
- /**
- * Format the output when the search result contains entities
- *
- * @param SpecialSearch $searchPage
- * @param SearchResult $result
- * @param array $terms
- * @param string &$link
- * @param string &$redirect
- * @param string &$section
- * @param string &$extract
- * @param string &$score
- * @param string &$size
- * @param string &$date
- * @param string &$related
- * @param string &$html
- */
- public static function onShowSearchHit( SpecialSearch $searchPage,
SearchResult $result, $terms,
- &$link, &$redirect, &$section, &$extract, &$score, &$size,
&$date, &$related, &$html
- ) {
- $entityContentFactory =
WikibaseRepo::getDefaultInstance()->getEntityContentFactory();
-
- $title = $result->getTitle();
- $contentModel = $title->getContentModel();
-
- if ( $entityContentFactory->isEntityContentModel( $contentModel
) ) {
- /** @var EntityContent $content */
- $page = WikiPage::factory( $title );
- $content = $page->getContent();
-
- if ( $content && !$content->isRedirect() ) {
- $entity = $content->getEntity();
- $languageCode =
$searchPage->getLanguage()->getCode(); // TODO: language fallback!
-
- if ( $entity instanceof DescriptionsProvider &&
-
$entity->getDescriptions()->hasTermForLanguage( $languageCode )
- ) {
- $description =
$entity->getDescriptions()->getByLanguage( $languageCode )->getText();
- $attr = [ 'class' =>
'wb-itemlink-description' ];
- $link .= $searchPage->msg(
'colon-separator' )->text();
- $link .= Html::element( 'span', $attr,
$description );
- }
- }
-
- $extract = ''; // TODO: set this to something useful.
- }
}
/**
diff --git a/repo/Wikibase.php b/repo/Wikibase.php
index ca66bbd..76a74c9 100644
--- a/repo/Wikibase.php
+++ b/repo/Wikibase.php
@@ -987,7 +987,7 @@
$wgHooks['PageHistoryLineEnding'][] =
'Wikibase\RepoHooks::onPageHistoryLineEnding';
$wgHooks['ApiCheckCanExecute'][] =
'Wikibase\RepoHooks::onApiCheckCanExecute';
$wgHooks['SetupAfterCache'][] = 'Wikibase\RepoHooks::onSetupAfterCache';
- $wgHooks['ShowSearchHit'][] = 'Wikibase\RepoHooks::onShowSearchHit';
+ $wgHooks['ShowSearchHit'][] =
'Wikibase\Repo\Hooks\ShowSearchHitHandler::onShowSearchHit';
$wgHooks['ShowSearchHitTitle'][] =
'Wikibase\RepoHooks::onShowSearchHitTitle';
$wgHooks['TitleGetRestrictionTypes'][] =
'Wikibase\RepoHooks::onTitleGetRestrictionTypes';
$wgHooks['AbuseFilter-contentToString'][] =
'Wikibase\RepoHooks::onAbuseFilterContentToString';
diff --git a/repo/includes/Hooks/ShowSearchHitHandler.php
b/repo/includes/Hooks/ShowSearchHitHandler.php
new file mode 100644
index 0000000..a60b8f0
--- /dev/null
+++ b/repo/includes/Hooks/ShowSearchHitHandler.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Wikibase\Repo\Hooks;
+
+use Html;
+use IContextSource;
+use SearchResult;
+use SpecialSearch;
+use Title;
+use Wikibase\DataModel\Entity\EntityDocument;
+use Wikibase\DataModel\Services\Lookup\EntityLookup;
+use Wikibase\DataModel\Term\DescriptionsProvider;
+use Wikibase\LanguageFallbackChain;
+use Wikibase\Repo\Content\EntityContentFactory;
+use Wikibase\Repo\WikibaseRepo;
+use Wikibase\Store\EntityIdLookup;
+
+/**
+ * Handler to format entities in the search results
+ *
+ * @license GPL-2.0+
+ * @author Matěj Suchánek
+ * @author Daniel Kinzler
+ */
+class ShowSearchHitHandler {
+
+ /**
+ * @var EntityContentFactory
+ */
+ private $entityContentFactory;
+
+ /**
+ * @var LanguageFallbackChain
+ */
+ private $languageFallbackChain;
+
+ /**
+ * @var EntityIdLookup
+ */
+ private $entityIdLookup;
+
+ /**
+ * @var EntityLookup
+ */
+ private $entityLookup;
+
+ public function __construct(
+ EntityContentFactory $entityContentFactory,
+ LanguageFallbackChain $languageFallbackChain,
+ EntityIdLookup $entityIdLookup,
+ EntityLookup $entityLookup
+ ) {
+ $this->entityContentFactory = $entityContentFactory;
+ $this->languageFallbackChain = $languageFallbackChain;
+ $this->entityIdLookup = $entityIdLookup;
+ $this->entityLookup = $entityLookup;
+ }
+
+ /**
+ * @param IContextSource $context
+ * @return self
+ */
+ private static function newFromGlobalState( IContextSource $context ) {
+ $wikibaseRepo = WikibaseRepo::getDefaultInstance();
+ $languageFallbackChainFactory =
$wikibaseRepo->getLanguageFallbackChainFactory();
+
+ return new self(
+ $wikibaseRepo->getEntityContentFactory(),
+ $languageFallbackChainFactory->newFromContext( $context
),
+ $wikibaseRepo->getEntityIdLookup(),
+ $wikibaseRepo->getEntityLookup()
+ );
+ }
+
+ /**
+ * Format the output when the search result contains entities
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ShowSearchHit
+ * @see doShowSearchHit
+ *
+ * @param SpecialSearch $searchPage
+ * @param SearchResult $result
+ * @param array $terms
+ * @param string &$link
+ * @param string &$redirect
+ * @param string &$section
+ * @param string &$extract
+ * @param string &$score
+ * @param string &$size
+ * @param string &$date
+ * @param string &$related
+ * @param string &$html
+ */
+ public static function onShowSearchHit( SpecialSearch $searchPage,
SearchResult $result, array $terms,
+ &$link, &$redirect, &$section, &$extract, &$score, &$size,
&$date, &$related, &$html
+ ) {
+ $self = self::newFromGlobalState( $searchPage->getContext() );
+ $self->doShowSearchHit( $searchPage, $result, $terms, $link,
$redirect, $section, $extract,
+ $score, $size, $date, $related, $html );
+ }
+
+ public function doShowSearchHit( SpecialSearch $searchPage,
SearchResult $result, array $terms,
+ &$link, &$redirect, &$section, &$extract, &$score, &$size,
&$date, &$related, &$html
+ ) {
+ $title = $result->getTitle();
+ $contentModel = $title->getContentModel();
+
+ if ( !$this->entityContentFactory->isEntityContentModel(
$contentModel ) ) {
+ return;
+ }
+
+ $extract = ''; // TODO: set this to something useful.
+
+ $entity = $this->getEntity( $title );
+ if ( !( $entity instanceof DescriptionsProvider ) ) {
+ return;
+ }
+
+ $terms = $entity->getDescriptions()->toTextArray();
+ $termData =
$this->languageFallbackChain->extractPreferredValue( $terms );
+ if ( $termData !== null ) {
+ $this->addDescription( $link, $termData, $searchPage );
+ }
+ }
+
+ private function addDescription( &$link, array $termData, SpecialSearch
$searchPage ) {
+ $description = $termData['value'];
+ $attr = [ 'class' => 'wb-itemlink-description' ];
+ if ( $termData['language'] !==
$searchPage->getLanguage()->getCode() ) {
+ $attr += [ 'dir' => 'auto', 'lang' => wfBCP47(
$termData['language'] ) ];
+ }
+ $link .= $searchPage->msg( 'colon-separator' )->escaped();
+ $link .= Html::element( 'span', $attr, $description );
+ }
+
+ /**
+ * @param Title $title
+ * @return EntityDocument|null
+ */
+ private function getEntity( Title $title ) {
+ $entityId = $this->entityIdLookup->getEntityIdForTitle( $title
);
+ if ( $entityId ) {
+ return $this->entityLookup->getEntity( $entityId );
+ }
+ return null;
+ }
+
+}
diff --git a/repo/tests/phpunit/includes/Hooks/ShowSearchHitHandlerTest.php
b/repo/tests/phpunit/includes/Hooks/ShowSearchHitHandlerTest.php
new file mode 100644
index 0000000..4682852
--- /dev/null
+++ b/repo/tests/phpunit/includes/Hooks/ShowSearchHitHandlerTest.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace Wikibase\Repo\Tests\Hooks;
+
+use Language;
+use MediaWikiTestCase;
+use Message;
+use SearchResult;
+use SpecialSearch;
+use Title;
+use Wikibase\DataModel\Entity\Item;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Services\Lookup\EntityLookup;
+use Wikibase\DataModel\Term\TermList;
+use Wikibase\LanguageFallbackChain;
+use Wikibase\Repo\Content\EntityContentFactory;
+use Wikibase\Repo\Hooks\ShowSearchHitHandler;
+use Wikibase\Store\EntityIdLookup;
+
+/**
+ * @covers Wikibase\Repo\Hooks\ShowSearchHitHandler
+ *
+ * @group Wikibase
+ *
+ * @license GPL-2.0+
+ * @author Matěj Suchánek
+ */
+class ShowSearchHitHandlerTest extends MediaWikiTestCase {
+
+ const NON_ENTITY_TITLE = 'foo';
+ const DE_DESCRIPTION_ITEM_ID = 'Q1';
+ const FALLBACK_DESCRIPTION_ITEM_ID = 'Q2';
+ const NO_FALLBACK_DESCRIPTION_ITEM_ID = 'Q3';
+ const EMPTY_ITEM_ID = 'Q4';
+
+ /**
+ * @return SpecialSearch
+ */
+ private function getSearchPage() {
+ $searchPage = $this->getMockBuilder( SpecialSearch::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $searchPage->method( 'msg' )
+ ->with( $this->equalTo( 'colon-separator' ) )
+ ->will( $this->returnCallback( function ( $key ) {
+ $msg = $this->getMockBuilder( Message::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $msg->method( 'escaped' )->willReturn( ': ' );
+
+ return $msg;
+ } ) );
+ $searchPage->method( 'getLanguage' )
+ ->willReturn( Language::factory( 'de' ) );
+
+ return $searchPage;
+ }
+
+ /**
+ * @param string $title
+ * @return SearchResult
+ */
+ private function getSearchResult( $title ) {
+ $mockTitle = $this->getMock( Title::class );
+ $mockTitle->method( 'getText' )->willReturn( $title );
+ // hack: content model equals title/id
+ $mockTitle->method( 'getContentModel' )->willReturn( $title );
+
+ $searchResult = $this->getMockBuilder( SearchResult::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $searchResult->method( 'getTitle' )->willReturn( $mockTitle );
+
+ return $searchResult;
+ }
+
+ /**
+ * @see LanguageFallbackChain::extractPreferredValue
+ * @param array $terms
+ * @return array|null
+ */
+ private function extractPreferredValue( array $terms ) {
+ if ( array_key_exists( 'de', $terms ) ) {
+ return [
+ 'value' => $terms['de'],
+ 'language' => 'de',
+ ];
+ } elseif ( array_key_exists( 'en', $terms ) ) {
+ return [
+ 'value' => $terms['en'],
+ 'language' => 'en',
+ ];
+ }
+
+ return null;
+ }
+
+ /**
+ * @return LanguageFallbackChain
+ */
+ private function getLanguageFallbackChain() {
+ $languageFallbackChain = $this->getMockBuilder(
LanguageFallbackChain::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $languageFallbackChain->method( 'extractPreferredValue' )
+ ->will( $this->returnCallback( function ( array $terms
) {
+ return $this->extractPreferredValue( $terms );
+ } ) );
+
+ return $languageFallbackChain;
+ }
+
+ /**
+ * @return EntityContentFactory
+ */
+ private function getEntityContentFactory() {
+ $entityContentFactory = $this->getMockBuilder(
EntityContentFactory::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entityContentFactory->method( 'isEntityContentModel' )
+ ->will( $this->returnCallback( function ( $contentModel
) {
+ // hack: content model equals title/id
+ return $contentModel !== self::NON_ENTITY_TITLE;
+ } ) );
+
+ return $entityContentFactory;
+ }
+
+ /**
+ * @return EntityIdLookup
+ */
+ private function getEntityIdLookup() {
+ $entityIdLookup = $this->getMock( EntityIdLookup::class );
+ $entityIdLookup->method( 'getEntityIdForTitle' )
+ ->will( $this->returnCallback( function ( Title $title
) {
+ return new ItemId( $title->getText() );
+ } ) );
+
+ return $entityIdLookup;
+ }
+
+ /**
+ * @param ItemId $itemId
+ * @return string[]
+ */
+ private function getDescriptionsArray( ItemId $itemId ) {
+ $terms = [];
+ switch ( $itemId->getSerialization() ) {
+ case self::DE_DESCRIPTION_ITEM_ID:
+ $terms['de'] = '<b>German description</b>';
+ case self::FALLBACK_DESCRIPTION_ITEM_ID:
+ $terms['en'] = 'fallback description';
+ case self::NO_FALLBACK_DESCRIPTION_ITEM_ID:
+ $terms['fr'] = 'unused description';
+ case self::EMPTY_ITEM_ID:
+ break;
+ }
+ return $terms;
+ }
+
+ /**
+ * @param ItemId $itemId
+ * @return Item $item
+ */
+ private function getEntity( ItemId $itemId ) {
+ $termList = $this->getMockBuilder( TermList::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $termList->method( 'toTextArray' )
+ ->willReturn( $this->getDescriptionsArray( $itemId ) );
+
+ $item = $this->getMockBuilder( Item::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $item->method( 'getDescriptions' )->willReturn( $termList );
+
+ return $item;
+ }
+
+ /**
+ * @return EntityLookup
+ */
+ private function getEntityLookup() {
+ $entityLookup = $this->getMock( EntityLookup::class );
+ $entityLookup->method( 'getEntity' )
+ ->will( $this->returnCallback( function ( ItemId
$itemId ) {
+ return $this->getEntity( $itemId );
+ } ) );
+
+ return $entityLookup;
+ }
+
+ /**
+ * @dataProvider showSearchHitProvider
+ */
+ public function testShowSearchHit( $title, $expected, $newExtract ) {
+ $searchPage = $this->getSearchPage();
+ $searchResult = $this->getSearchResult( $title );
+ $handler = new ShowSearchHitHandler(
+ $this->getEntityContentFactory(),
+ $this->getLanguageFallbackChain(),
+ $this->getEntityIdLookup(),
+ $this->getEntityLookup()
+ );
+ $link = '<a>link</a>';
+ $extract = '<span>extract</span>';
+ $redirect = $section = $score = $size = $date = $related =
$html = '';
+ $handler->doShowSearchHit(
+ $searchPage,
+ $searchResult,
+ [],
+ $link,
+ $redirect,
+ $section,
+ $extract,
+ $score,
+ $size,
+ $date,
+ $related,
+ $html
+ );
+ $this->assertEquals( $expected, $link );
+ $this->assertEquals( $newExtract, $extract );
+ }
+
+ public function showSearchHitProvider() {
+ return [
+ 'non-entity' => [
+ self::NON_ENTITY_TITLE,
+ '<a>link</a>',
+ '<span>extract</span>',
+ ],
+ 'German description' => [
+ self::DE_DESCRIPTION_ITEM_ID,
+ '<a>link</a>: <span
class="wb-itemlink-description"><b>German description</b></span>',
+ '',
+ ],
+ 'fallback description' => [
+ self::FALLBACK_DESCRIPTION_ITEM_ID,
+ '<a>link</a>: <span
class="wb-itemlink-description" dir="auto" lang="en">fallback
description</span>',
+ '',
+ ],
+ 'no available fallback description' => [
+ self::NO_FALLBACK_DESCRIPTION_ITEM_ID,
+ '<a>link</a>',
+ '',
+ ],
+ 'description-less item' => [
+ self::EMPTY_ITEM_ID,
+ '<a>link</a>',
+ '',
+ ],
+ ];
+ }
+
+}
--
To view, visit https://gerrit.wikimedia.org/r/362215
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I45039f579054c0130162c9e14c87c94dfe3cb1c3
Gerrit-PatchSet: 4
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Matěj Suchánek <[email protected]>
Gerrit-Reviewer: Fomafix <[email protected]>
Gerrit-Reviewer: Jonas Kress (WMDE) <[email protected]>
Gerrit-Reviewer: Matěj Suchánek <[email protected]>
Gerrit-Reviewer: Thiemo Mättig (WMDE) <[email protected]>
Gerrit-Reviewer: WMDE-leszek <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits