Fall back to CirrusSearch's morelike: feature

When no related articles have been specified by an editor we instead
hit request pages similar to the current page using the CirrusSearch
extension's "morelike:" feature [0].

Changes:
* Config variable introduced RelatedArticlesUseCirrusSearch which allows
you to turn on use of the CirrusSearch API.
* Introduce a RelatedPagesGateway for dealing with making the API call
  and returning consistent results
* Move the "simple" API call for hydrating related pages fetched from
  the wgRelatedArticles configuration variable into RelatedPagesGateway
* Reduce the bootstrap module to just a bootstrap module!

Bug: T116707
Change-Id: Ia0ced1d7ae57c0939d1f5af275aa9d393f1420b1
This commit is contained in:
jdlrobson 2015-11-03 15:39:48 -08:00
parent b7a579ca64
commit e86fc3b159
7 changed files with 251 additions and 77 deletions

View file

@ -16,6 +16,7 @@
"globals": {
"mw": false,
"$": false,
"jQuery": true
"jQuery": true,
"OO": true
}
}

View file

@ -19,6 +19,9 @@
"RelatedArticlesMagic": "RelatedArticles.i18n.magic.php"
},
"Hooks": {
"ResourceLoaderTestModules": [
"RelatedArticles\\ReadMoreHooks::onResourceLoaderTestModules"
],
"ParserFirstCallInit": [
"RelatedArticles\\Hooks::onParserFirstCallInit"
],
@ -51,12 +54,25 @@
},
"manifest_version": 1,
"ResourceModules": {
"ext.relatedArticles.readMore": {
"scripts": [
"resources/ext.relatedArticles.readMore/RelatedPagesGateway.js"
],
"dependencies": [
"oojs"
],
"targets": [
"mobile",
"desktop"
]
},
"ext.relatedArticles.readMore.bootstrap": {
"scripts": [
"resources/ext.relatedArticles.readMore.bootstrap/index.js"
],
"dependencies": [
"mediawiki.api"
"mediawiki.api",
"ext.relatedArticles.readMore"
],
"targets": [
"mobile",
@ -78,6 +94,12 @@
]
}
},
"config": {
"RelatedArticlesUseCirrusSearch": false
},
"ConfigRegistry": {
"relatedarticles": "GlobalVarConfig::newInstance"
},
"ResourceFileModulePaths": {
"localBasePath": "",
"remoteExtPath": "RelatedArticles"

View file

@ -4,8 +4,34 @@ namespace RelatedArticles;
use OutputPage;
use Skin;
use ConfigFactory;
class ReadMoreHooks {
/**
* Register QUnit tests.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderTestModules
*
* @param array $modules
* @param ResourceLoader $rl
* @return bool
*/
public static function onResourceLoaderTestModules( &$modules, &$rl ) {
$boilerplate = array(
'localBasePath' => __DIR__ . '/../tests/qunit/',
'remoteExtPath' => 'RelatedArticles/tests/qunit',
'targets' => array( 'desktop', 'mobile' ),
);
$modules['qunit']['ext.relatedArticles.readMore.tests'] = $boilerplate + array(
'scripts' => array(
'ext.relatedArticles.readMore/test_RelatedPagesGateway.js',
),
'dependencies' => array(
'ext.relatedArticles.readMore',
),
);
return true;
}
/**
* Handler for the <code>MakeGlobalVariablesScript</code> hook.
@ -18,7 +44,10 @@ class ReadMoreHooks {
* @return boolean Always <code>true</code>
*/
public static function onMakeGlobalVariablesScript( &$vars, OutputPage $out ) {
$config = ConfigFactory::getDefaultInstance()->makeConfig( 'relatedarticles' );
$vars['wgRelatedArticles'] = $out->getProperty( 'RelatedArticles' );
$vars['wgRelatedArticlesUseCirrusSearch'] = $config->get( 'RelatedArticlesUseCirrusSearch' );
return true;
}

View file

@ -1,80 +1,28 @@
( function ( $ ) {
var relatedArticles = mw.config.get( 'wgRelatedArticles' ) || [],
config = mw.config.get( [ 'skin', 'wgNamespaceNumber', 'wgMFMode', 'wgIsMainPage' ] ),
module;
var config = mw.config.get( [ 'skin', 'wgNamespaceNumber', 'wgMFMode', 'wgIsMainPage' ] ),
relatedPages = new mw.relatedArticles.RelatedPagesGateway( new mw.Api(),
mw.config.get( 'wgPageName' ), mw.config.get( 'wgRelatedArticles' ),
mw.config.get( 'wgRelatedArticlesUseCirrusSearch' ) ),
// Limit number of related articles to 4 (more of them increases likelihood of reader ignoring).
relatedArticles = relatedArticles.slice( 0, 4 );
/**
* Retrieves the data required to render a card.
*
* Given a title, the following additional information is retrieved
* from the API:
*
* * The ID of the page corresponding to the title
* * The thumbnail, if any
* * The Wikidata description, if any
*
* @private
*
* @param {string[]} titles
* @return {jQuery.Promise}
*/
function getData( titles ) {
var api = new mw.Api();
return api.get( {
action: 'query',
prop: 'pageimages|pageterms',
wbptterms: 'description',
pilimit: titles.length,
'continue': '',
titles: titles
} ).then( function ( data ) {
if ( !data.query || !data.query.pages ) {
return [];
}
return $.map( data.query.pages, function ( page ) {
var result = {
id: page.pageid,
title: page.title,
thumbnail: page.thumbnail,
description: undefined
};
if (
page.terms &&
page.terms.description &&
page.terms.description.length > 0
) {
result.description = page.terms.description[ 0 ];
}
return result;
} );
} );
}
LIMIT = 4;
if (
relatedArticles.length > 0 &&
config.wgNamespaceNumber === 0 &&
!config.wgIsMainPage &&
config.skin === 'minerva' &&
config.wgMFMode === 'beta'
) {
module = 'ext.relatedArticles.readMore.minerva';
$( function () {
$.when(
mw.loader.using( module ),
getData( relatedArticles )
).done( function ( _, data ) {
mw.track( 'ext.relatedArticles.init', { pages: data } );
} );
$.when(
// Note we load dependencies here rather than ResourceLoader
// to avoid PHP exceptions when MobileFrontend not installed
// which should never happen given the if statement.
mw.loader.using( [ 'mobile.pagelist.scripts', 'ext.relatedArticles.readMore.minerva' ] ),
relatedPages.getForCurrentPage( LIMIT )
).done( function ( _, pages ) {
if ( pages.length ) {
mw.track( 'ext.relatedArticles.init', pages );
}
} );
}

View file

@ -13,23 +13,18 @@
var Page = mw.mobileFrontend.require( 'mobile.startup/Page' );
return $.map( pages, function ( rawPage ) {
return new Page( {
id: rawPage.id,
title: rawPage.title,
thumbnail: rawPage.thumbnail,
wikidataDescription: rawPage.description
} );
return new Page( rawPage );
} );
}
mw.trackSubscribe( 'ext.relatedArticles.init', function ( _, data ) {
mw.trackSubscribe( 'ext.relatedArticles.init', function ( _, pages ) {
var WatchstarPageList = mw.mobileFrontend.require( 'mobile.pagelist.scripts/WatchstarPageList' ),
pageList,
$container = $( '#content' ),
$readMore;
pageList = new WatchstarPageList( {
pages: convertPages( data.pages ),
pages: convertPages( pages ),
// FIXME: When the user clicks any watchstar, a
// MobileWebWatching event is logged. Watchstar, which

View file

@ -0,0 +1,108 @@
( function ( $ ) {
// FIXME: Move into separate file as this module becomes larger.
mw.relatedArticles = {};
/**
* @class RelatedPagesGateway
* @param {mw.Api} api
* @param {string} currentPage the page that the editorCuratedArticles relate to
* @param {Array} editorCuratedArticles a list of articles curated by editors for the current page
* @param {boolean} useCirrusSearch whether to hit the API when no editor curated articles are available
*/
function RelatedPagesGateway( api, currentPage, editorCuratedArticles, useCirrusSearch ) {
this.api = api;
this.currentPage = currentPage;
this.editorCuratedArticles = editorCuratedArticles || [];
this.useCirrusSearch = useCirrusSearch;
}
OO.initClass( RelatedPagesGateway );
/**
* Gets the related pages for the current page.
*
* If there are related pages assigned to this page using the `related`
* parser function, then they are returned.
*
* If there aren't any related pages assigned to the page, then the
* CirrusSearch extension's {@link https://www.mediawiki.org/wiki/Help:CirrusSearch#morelike: "morelike:" feature}
* is used. If the CirrusSearch extension isn't installed, then the API
* call will fail gracefully and no related pages will be returned.
* Thus the dependency on the CirrusSearch extension is soft.
*
* Related pages will have the following information:
*
* * The ID of the page corresponding to the title
* * The thumbnail, if any
* * The Wikidata description, if any
*
* @method
* @param {number} limit of articles to get
* @return {jQuery.Promise}
*/
RelatedPagesGateway.prototype.getForCurrentPage = function ( limit ) {
var parameters = {
action: 'query',
formatversion: 2,
prop: 'pageimages|pageterms',
piprop: 'thumbnail',
pithumbsize: 80,
wbptterms: 'description'
},
relatedArticles = ( this.editorCuratedArticles ).slice( 0, limit );
if ( relatedArticles.length ) {
parameters.pilimit = relatedArticles.length;
parameters[ 'continue' ] = ''; // jscs:ignore requireDotNotation
parameters.titles = relatedArticles;
} else if ( this.useCirrusSearch ) {
parameters.pilimit = limit;
parameters.generator = 'search';
parameters.gsrsearch = 'morelike:' + this.currentPage;
parameters.gsrnamespace = '0';
parameters.gsrlimit = limit;
} else {
return $.Deferred().resolve( [] );
}
return this.api.get( parameters )
.then( getPages )
.then( processPages );
};
/**
* @ignore
*/
function getPages( result ) {
return result && result.query && result.query.pages ? result.query.pages : [];
}
/**
* @ignore
*/
function processPages( pages ) {
return $.map( pages, function ( page ) {
var result = {
id: page.pageid,
isMissing: !page.pageid,
title: page.title,
thumbnail: page.thumbnail,
wikidataDescription: undefined
};
if (
page.terms &&
page.terms.description &&
page.terms.description.length > 0
) {
result.wikidataDescription = page.terms.description[ 0 ];
}
return result;
} );
}
mw.relatedArticles.RelatedPagesGateway = RelatedPagesGateway;
}( jQuery ) );

View file

@ -0,0 +1,71 @@
( function ( M, $ ) {
var RelatedPagesGateway = mw.relatedArticles.RelatedPagesGateway,
relatedPages = {
query: {
pages: {
123: {
id: 123,
title: 'Oh noes',
ns: 0,
thumbnail: {
source: 'http://placehold.it/200x100'
}
}
}
}
},
emptyRelatedPages = {
query: {
pages: {
}
}
};
QUnit.module( 'RelatedArticles readMore - Related pages api', {
setup: function () {
this.api = new mw.Api();
}
} );
QUnit.test( 'Returns an array with the results when api responds', 2, function ( assert ) {
var gateway = new RelatedPagesGateway( this.api, 'Foo', null, true );
this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( relatedPages ) );
gateway.getForCurrentPage( 1 ).then( function ( results ) {
assert.ok( $.isArray( results ), 'Results must be an array' );
assert.strictEqual( results[ 0 ].title, 'Oh noes' );
} );
} );
QUnit.test( 'Empty related pages is handled fine.', 2, function ( assert ) {
var gateway = new RelatedPagesGateway( this.api, 'Foo', null, true );
this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( emptyRelatedPages ) );
gateway.getForCurrentPage( 1 ).then( function ( results ) {
assert.ok( $.isArray( results ), 'Results must be an array' );
assert.strictEqual( results.length, 0 );
} );
} );
QUnit.test( 'Empty related pages with no cirrus search is handled fine. No API request.', 3, function ( assert ) {
var gateway = new RelatedPagesGateway( this.api, 'Foo', [], false ),
spy = this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( relatedPages ) );
gateway.getForCurrentPage( 1 ).then( function ( results ) {
assert.ok( $.isArray( results ), 'Results must be an array' );
assert.ok( !spy.called, 'API is not invoked' );
assert.strictEqual( results.length, 0 );
} );
} );
QUnit.test( 'Related pages from editor curated content', 1, function ( assert ) {
var gateway = new RelatedPagesGateway( this.api, 'Foo', [ { title: 1 } ], false );
this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( relatedPages ) );
gateway.getForCurrentPage( 1 ).then( function ( results ) {
assert.strictEqual( results.length, 1,
'API still hit despite cirrus being disabled.' );
} );
} );
}( mw.mobileFrontend, jQuery ) );