jenkins-bot has submitted this change and it was merged. Change subject: Add route for featured article of the day ......................................................................
Add route for featured article of the day http://localhost:6927/en.wikipedia.org/v1/page/featured/2016/4/15 Currently only for enwiki only. Code adopted from the iOS app, with minor improvements. Added a dateUtil library since this is probably useful for other feed endpoints as well. Bug: T132764 Change-Id: I8aa1db44d1ffdf8d618241c3c177af4036ffba11 --- A lib/dateUtil.js A lib/feed/featured.js M lib/mobile-util.js M lib/mwapi.js A routes/featured.js M spec.yaml A test/features/featured/pagecontent.js A test/lib/dateUtil/date-util-test.js 8 files changed, 389 insertions(+), 2 deletions(-) Approvals: Mholloway: Looks good to me, approved Ppchelko: Checked jenkins-bot: Verified diff --git a/lib/dateUtil.js b/lib/dateUtil.js new file mode 100644 index 0000000..c37427b --- /dev/null +++ b/lib/dateUtil.js @@ -0,0 +1,40 @@ +'use strict'; + +var sUtil = require('../lib/util'); + +var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +]; + +/** + * Returns a String formatted in English date format. + * + * Example: "May 16, 2016" + * + * @param {Date} date date to be used + * @return {String} formatted date string + */ +function formatDateEnglish(date) { + var year = date.getUTCFullYear().toString(); + var month = monthNames[date.getUTCMonth()]; + var day = date.getUTCDate().toString(); + return `${month} ${day}, ${year}`; +} + +/** + * Returns a Date object with the desired date as specified in the request. + * The expected format is "yyyy/mm/dd". + * + * Example: "2016/05/11" + * + * @param {Object} req Object (looking for params property with subproperties yyyy, mm, dd. + * @return {Date} date object + */ +function getRequestedDate(req) { + return new Date(Date.UTC(req.params.yyyy, req.params.mm - 1, req.params.dd)); // month is 0-based +} + +module.exports = { + formatDateEnglish: formatDateEnglish, + getRequestedDate: getRequestedDate +}; diff --git a/lib/feed/featured.js b/lib/feed/featured.js new file mode 100644 index 0000000..7515a79 --- /dev/null +++ b/lib/feed/featured.js @@ -0,0 +1,122 @@ +/** + * To retrieve TFA -- Today's featured article -- for a given date. + */ + +'use strict'; + +var preq = require('preq'); +var api = require('../api-util'); +var mwapi = require('../mwapi'); +var dateUtil = require('../dateUtil'); +var sUtil = require('../util'); +var HTTPError = sUtil.HTTPError; + + +/** + * Builds the request to get the Featured article of a given date. + * + * @param {Object} app the application object + * @param {String} domain the requested domain, e.g. 'de.wikipedia.org' + * @param {Date} date for which day the featured article is requested + * @return {Promise} a promise resolving as an JSON object containing the response + */ +function requestFeaturedArticleTitle(app, domain, date) { + var formattedDateString = dateUtil.formatDateEnglish(date); + return api.mwApiGet(app, domain, { + action: 'query', + format: 'json', + formatversion: 2, + exchars: 255, + explaintext: '', + titles: `Template:TFA_title/${formattedDateString}`, + prop: 'extracts' + }); +} + +// -- functions dealing with responses: + +function getPageObject(response, dontThrow) { + if (response.body.query && response.body.query.pages[0]) { + var page = response.body.query.pages[0]; + if (!page.extract || !page.pageid || page.missing === true) { + throw new HTTPError({ + status: 404, + type: 'not_found', + title: 'No featured article for this date', + detail: 'There is no featured article for this date.' + }); + } + return page; + } else { + if (!dontThrow) { + throw new HTTPError({ + status: 500, + type: 'unknown_backend_response', + title: 'Unexpected backend response', + detail: 'The backend responded with gibberish.' + }); + } + } +} + +/** + * HAX: TextExtracts extension will (sometimes) add "..." to the extract. In this particular case, we don't + * want it, so we remove it if present. + */ +function removeEllipsis(extract) { + if (extract.endsWith('...')) { + return extract.slice(0, -3); + } + return extract; +} + +function getRevision(extractObj) { + return extractObj.revisions[0].revid; +} + +function buildResponse(pageTitle, extractPageObj) { + return { + page: { + title: pageTitle, + thumbnail: extractPageObj.thumbnail, + description: extractPageObj.terms && extractPageObj.terms.description[0], + extract: extractPageObj.extract, + revid: getRevision(extractPageObj) + } + }; +} + +function promise(app, req) { + if (req.params.domain.indexOf('en') !== 0) { + throw new HTTPError({ + status: 501, + type: 'unsupported_language', + title: 'Language not supported', + detail: 'The language you have requested is not yet supported.' + }); + } + + var tfaPageObj, pageTitle; + + return requestFeaturedArticleTitle(app, req.params.domain, dateUtil.getRequestedDate(req)) + .then(function (response) { + mwapi.checkForQueryPagesInResponse(req, response); + tfaPageObj = getPageObject(response); + pageTitle = removeEllipsis(tfaPageObj.extract); + req.params.title = pageTitle; + return mwapi.requestExtractAndDescription(app, req); + }).then(function (extractResponse) { + mwapi.checkForQueryPagesInResponse(req, extractResponse); + var extractPageObj = getPageObject(extractResponse, true); + return { + payload: buildResponse(pageTitle, extractPageObj), + meta: { + etag: tfaPageObj.pageid + '/' + getRevision(extractPageObj) + } + }; + }); +} + +module.exports = { + promise: promise +}; diff --git a/lib/mobile-util.js b/lib/mobile-util.js index c1676d3..a88c00f 100644 --- a/lib/mobile-util.js +++ b/lib/mobile-util.js @@ -37,6 +37,16 @@ } /** + * Sets the ETag header on the response object to a specified value. + * + * @param {Object} response the HTTPResponse object on which to set the header + * @param {Object} value to set the ETag to + */ +function setETagToValue(response, value) { + response.set('etag', '' + value); +} + +/** * Sets the ETag header on the response object. First, the request object is * checked for the X-Restbase-ETag header. If present, that is used as the ETag * header. Otherwise, a new ETag is created, comprised of the revision ID and @@ -56,11 +66,12 @@ if (!tid) { tid = uuid.now().toString(); } - response.set('etag', '' + revision + '/' + tid); + setETagToValue(response, revision + '/' + tid); } module.exports = { filterEmpty: filterEmpty, defaultVal: defaultVal, + setETagToValue: setETagToValue, setETag: setETag }; diff --git a/lib/mwapi.js b/lib/mwapi.js index faf26c5..2e0b9e9 100644 --- a/lib/mwapi.js +++ b/lib/mwapi.js @@ -103,7 +103,6 @@ } /** - * * Requests an article extract. * * @param {Object} app the application object @@ -121,6 +120,30 @@ explaintext: true, piprop: 'thumbnail', pithumbsize: 320, + titles: req.params.title + }; + return api.mwApiGet(app, req.params.domain, query); +} + +/** + * Requests an article extract and its description. + * + * @param {Object} app the application object + * @param {Object} req the request object + * @return {Promise} a promise resolving as an JSON object containing the response + */ +function requestExtractAndDescription(app, req) { + var query = { + action: 'query', + format: 'json', + formatversion: '2', + redirects: true, + prop: 'extracts|pageimages|pageterms|revisions', + exsentences: 5, // see T59669 + T117082 + explaintext: true, + piprop: 'thumbnail', + pithumbsize: 320, + wbptterms: 'description', titles: req.params.title }; return api.mwApiGet(app, req.params.domain, query); @@ -168,6 +191,7 @@ buildLeadImageUrls: buildLeadImageUrls, checkForQueryPagesInResponse: checkForQueryPagesInResponse, requestExtract: requestExtract, + requestExtractAndDescription: requestExtractAndDescription, // VisibleForTesting _buildLeadImageUrls: buildLeadImageUrls diff --git a/routes/featured.js b/routes/featured.js new file mode 100644 index 0000000..d1880f4 --- /dev/null +++ b/routes/featured.js @@ -0,0 +1,42 @@ +/** + * Featured article of the day + */ + +'use strict'; + +var mUtil = require('../lib/mobile-util'); +var sUtil = require('../lib/util'); +var featured = require('../lib/feed/featured'); + +/** + * The main router object + */ +var router = sUtil.router(); + +/** + * The main application object reported when this module is require()d + */ +var app; + +/** + * GET {domain}/v1/page/featured/{year}/{month}/{day} + * Gets the title and other metadata for a featured article of a given date. + * ETag is set to the pageid. This should be specific enough. + */ +router.get('/featured/:yyyy/:mm/:dd', function (req, res) { + return featured.promise(app, req) + .then(function (response) { + res.status(200); + mUtil.setETagToValue(res, response.meta.etag); + res.json(response.payload).end(); + }); +}); + +module.exports = function (appObj) { + app = appObj; + return { + path: '/page', + api_version: 1, + router: router + }; +}; diff --git a/spec.yaml b/spec.yaml index 69f890b..ad79504 100644 --- a/spec.yaml +++ b/spec.yaml @@ -55,6 +55,56 @@ description: /.+/ version: /.+/ home: /.+/ + # from routes/featured.js + /{domain}/v1/page/featured/{yyyy}/{mm}/{dd}: + get: + tags: + - Featured article for a given date + description: title of the featured article (only works on enwiki for now) + produces: + - application/json + parameters: + - name: yyyy + in: path + description: "Year the featured article is requested for" + type: integer + required: true + minimum: 2016 + maximum: 2999 + - name: mm + in: path + description: "Month the featured article is requested for" + type: integer + required: true + minimum: 1 + maximum: 12 + - name: dd + in: path + description: "Day of the month the featured article is requested for" + type: integer + required: true + minimum: 1 + maximum: 31 + x-amples: + - title: retrieve title of the featured article for April 29, 2016 + request: + params: + yyyy: 2016 + mm: 4 + dd: 29 + response: + status: 200 + headers: + content-type: application/json + body: + page: + title: /.+/ + description: /.+/ + extract: /.+/ + thumbnail: + source: /.+/ + width: /.+/ + height: /.+/ # from routes/media.js /{domain}/v1/page/media/{title}: get: diff --git a/test/features/featured/pagecontent.js b/test/features/featured/pagecontent.js new file mode 100644 index 0000000..cbeaf8b --- /dev/null +++ b/test/features/featured/pagecontent.js @@ -0,0 +1,77 @@ +'use strict'; + +var assert = require('../../utils/assert.js'); +var preq = require('preq'); +var server = require('../../utils/server.js'); +var headers = require('../../utils/headers.js'); + +describe('featured', function() { + this.timeout(20000); + + before(function () { return server.start(); }); + + it('featured article of a specific date should respond to GET request with expected headers, incl. CORS and CSP headers', function() { + return headers.checkHeaders(server.config.uri + 'en.wikipedia.org/v1/page/featured/2016/04/15', + 'application/json'); + }); + + it('featured article of 4/15/2016 should have title "Cosmic Stories and Stirring Science Stories"', function() { + return preq.get({ uri: server.config.uri + 'en.wikipedia.org/v1/page/featured/2016/04/15' }) + .then(function(res) { + assert.status(res, 200); + // the page id should be stable but not the revision: + assert.ok(res.headers.etag.indexOf('50089449/') == 0); + assert.equal(res.body.page.title, 'Cosmic Stories and Stirring Science Stories'); + assert.equal(res.body.page.thumbnail.source, 'http://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Cosmic_Science-Fiction_May_1941.jpg/226px-Cosmic_Science-Fiction_May_1941.jpg'); + assert.equal(res.body.page.thumbnail.width, 226); + assert.equal(res.body.page.thumbnail.height, 320); + assert.ok(res.body.page.extract.indexOf('Cosmic Stories ') >= 0); + }); + }); + + it('featured article of 4/29/2016 should have a description', function() { + return preq.get({ uri: server.config.uri + 'en.wikipedia.org/v1/page/featured/2016/04/29' }) + .then(function(res) { + assert.status(res, 200); + // the page id should be stable but not the revision: + assert.ok(res.headers.etag.indexOf('50282338/') == 0); + assert.equal(res.body.page.title, 'Lightning (Final Fantasy)'); + assert.ok(res.body.page.description.indexOf('Final Fantasy') >= 0); + assert.ok(res.body.page.extract.indexOf('Lightning ') >= 0); + }); + }); + + it('incomplete date should return 404', function() { + return preq.get({ uri: server.config.uri + 'en.wikipedia.org/v1/page/featured/2016/04' }) + .then(function(res) { + }, function(err) { + assert.status(err, 404); + }); + }); + + it('extra uri path parameter after date should return 404', function() { + return preq.get({ uri: server.config.uri + 'en.wikipedia.org/v1/page/featured/2016/04/15/11' }) + .then(function(res) { + }, function(err) { + assert.status(err, 404); + }); + }); + + it('unsupported language', function() { + return preq.get({ uri: server.config.uri + 'fr.wikipedia.org/v1/page/featured/2016/04/15' }) + .then(function(res) { + }, function(err) { + assert.status(err, 501); + assert.equal(err.body.type, 'unsupported_language'); + }); + }); + + it('featured article of an old date should return 404', function() { + return preq.get({ uri: server.config.uri + 'en.wikipedia.org/v1/page/featured/1970/12/31' }) + .then(function(res) { + }, function(err) { + assert.status(err, 404); + assert.equal(err.body.type, 'not_found'); + }); + }); +}); diff --git a/test/lib/dateUtil/date-util-test.js b/test/lib/dateUtil/date-util-test.js new file mode 100644 index 0000000..85c6adb --- /dev/null +++ b/test/lib/dateUtil/date-util-test.js @@ -0,0 +1,21 @@ +'use strict'; + +var assert = require('../../utils/assert.js'); +var dateUtil = require('../../../lib/dateUtil'); + +describe('lib:dateUtil', function() { + this.timeout(20000); + + it('getRequestedDate(2016/04/15) should return a valid Date object', function() { + var actual = dateUtil.getRequestedDate({ + params: { + yyyy: 2016, + mm: 4, + dd: 15 + } + }); + assert.equal(actual.getUTCFullYear(), 2016); + assert.equal(actual.getUTCMonth(), 4 - 1); + assert.equal(actual.getUTCDate(), 15); + }); +}); -- To view, visit https://gerrit.wikimedia.org/r/290859 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I8aa1db44d1ffdf8d618241c3c177af4036ffba11 Gerrit-PatchSet: 12 Gerrit-Project: mediawiki/services/mobileapps Gerrit-Branch: master Gerrit-Owner: BearND <[email protected]> Gerrit-Reviewer: BearND <[email protected]> Gerrit-Reviewer: Dbrant <[email protected]> Gerrit-Reviewer: Fjalapeno <[email protected]> Gerrit-Reviewer: GWicke <[email protected]> Gerrit-Reviewer: Jhernandez <[email protected]> Gerrit-Reviewer: Mholloway <[email protected]> Gerrit-Reviewer: Mhurd <[email protected]> Gerrit-Reviewer: Mobrovac <[email protected]> Gerrit-Reviewer: Niedzielski <[email protected]> Gerrit-Reviewer: Ppchelko <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
