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

Reply via email to