Mforns has uploaded a new change for review. https://gerrit.wikimedia.org/r/181424
Change subject: Add Annotations API ...................................................................... Add Annotations API Adds an API to get metric annotations from Mediawiki. The API uses mediawiki-storage to retrieve the pages. Also, performs the following validations on annotations read from mediawiki pages: * Checks that dates are valid or undefined * Checks that start date <= end date * Checks that note is a string If an annotation is not valid, it is filtered and the others are returned. Bug: T78151 Change-Id: If372e9d6065aefc7ad0d6abb49894b18f06b7caf --- A src/app/apis/annotations-api.js M src/app/require.config.js M test/app/apis.js 3 files changed, 263 insertions(+), 2 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/analytics/dashiki refs/changes/24/181424/1 diff --git a/src/app/apis/annotations-api.js b/src/app/apis/annotations-api.js new file mode 100644 index 0000000..44af7d8 --- /dev/null +++ b/src/app/apis/annotations-api.js @@ -0,0 +1,138 @@ +/** + * This module gets metric annotations that reside in Mediawiki. + * To get them, it uses mediawiki-storage library. + */ +define(['mediawiki-storage', 'moment'], function (mediawikiStorage, moment) { + 'use strict'; + + function AnnotationsApi () { + // only fetch annotations for a given metric + // once per app life and keep their promise + this._promises = {}; + + // default promise for empty responses + this._emptyPromise = $.Deferred().resolve([]); + } + + + /** + * Retrieves the annotations for the given metric. + * + * Parameters + * + * metric : Metric object containing (or not) the following fields: + * { + * ... + * annotations: { + * host: 'mediawiki.host', + * pageName: 'PageName' + * }, + * ... + * } + * If it contains this substructure, the method + * will try to get the data from mediawiki. + * Otherwise, it will return en empty list. + * + * success : [optional] A function that will be called when finished + * with the resulting data as single parameter. + * + * error : [optional] A function that will be called in case + * of failure with the risen error as single parameter. + * + * Returns + * + * A jquery promise where 'done' and 'fail' callbacks can be submitted. + * These, will receive the same parameters as success and error callbacks + * respectively. In case of success, this will be the annotation's format: + * [ + * { + * start : '2013-01-01 17:32:13', + * end : '2014-02-02 03:55:08', + * note : 'Annotation text.' + * }, + * ... + * ] + **/ + AnnotationsApi.prototype.get = function (metric, success, error) { + if (typeof metric !== 'object') { + throw new TypeError('function must receive an object'); + } + + var params = metric.annotations; + if (!this._checkParams(params)) { + // accept metrics without annotation params + // and just return an empty array + return this._emptyPromise.done(success); + } + + if (!this._promises[params]) { + // the requested annotations have not been found in cache + // so they must be retrieved and cached for further use + var deferred = $.Deferred(), + that = this; + + mediawikiStorage.get({ + host: params.host, + pageName: params.pageName + }) + .fail(deferred.reject) + .done(function (data) { + that._checkAnnotations(data, deferred); + }); + + this._promises[params] = deferred.promise(); + } + + return this._promises[params].done(success).fail(error); + }; + + AnnotationsApi.prototype._checkParams = function (params) { + return ( + typeof params === 'object' && + typeof params.host === 'string' && + typeof params.pageName === 'string' + ); + }; + + AnnotationsApi.prototype._checkDate = function (date) { + return ( + date === void 0 || + typeof date === 'string' && + moment(date).isValid() + ); + }; + + AnnotationsApi.prototype._checkInterval = function (start, end) { + // assumes both dates have passed _checkDate + return ( + start === void 0 || + end === void 0 || + moment(start) <= moment(end) + ); + }; + + AnnotationsApi.prototype._checkAnnotations = function (data, deferred) { + if (!(data instanceof Array)) { + var error = new TypeError( + 'Mediawiki page must hold an array of annotations.' + ); + deferred.reject(error); + } + + var that = this; + // remove annotations that are incorrect + var annotations = data.filter(function (annotation) { + return ( + typeof annotation === 'object' && + that._checkDate(annotation.start) && + that._checkDate(annotation.end) && + that._checkInterval(annotation.start, annotation.end) && + typeof annotation.note === 'string' + ); + }); + + deferred.resolve(annotations); + }; + + return new AnnotationsApi(); +}); diff --git a/src/app/require.config.js b/src/app/require.config.js index 1afcce3..43033e8 100644 --- a/src/app/require.config.js +++ b/src/app/require.config.js @@ -24,6 +24,7 @@ 'config' : 'app/config', 'logger' : 'lib/logger', 'wikimetricsApi' : 'app/apis/wikimetrics', + 'annotationsApi' : 'app/apis/annotations-api', 'pageviewApi' : 'app/apis/legacy-pageview-api', 'configApi' : 'app/apis/config-api', 'dataConverterFactory' : 'app/data-converters/factory', diff --git a/test/app/apis.js b/test/app/apis.js index 903ee21..007070f 100644 --- a/test/app/apis.js +++ b/test/app/apis.js @@ -1,6 +1,6 @@ define([ - 'config', 'wikimetricsApi', 'configApi', 'mediawiki-storage', 'jquery' -], function (siteConfig, wikimetrics, configApi, mediawikiStorage, $) { + 'config', 'wikimetricsApi', 'configApi', 'annotationsApi', 'mediawiki-storage', 'jquery' +], function (siteConfig, wikimetrics, configApi, annotationsApi, mediawikiStorage, $) { describe('Wikimetrics API', function () { @@ -83,5 +83,127 @@ done(); }); }); + + it('should get metric annotations', function (done) { + var mediawikiHost = 'some.mediawiki.host', + annotationsPage = 'SomeMediawikiPageName', + startDate = '2014-01-01 00:00:00', + endDate = '2014-01-01 00:00:00', + note = 'Some text.'; + annotations = [{start: startDate, end: endDate, note: note}]; + + sinon.stub(mediawikiStorage, 'get', function (options) { + expect(options.host).toBe(mediawikiHost); + expect(options.pageName).toBe(annotationsPage); + + var deferred = new $.Deferred(); + deferred.resolve(annotations); + return deferred.promise(); + }); + + var metric = { + annotations: { + host: mediawikiHost, + pageName: annotationsPage + } + }; + annotationsApi.get(metric, function (returned) { + expect(returned instanceof Array).toBe(true); + expect(returned.length).toBe(1); + expect(typeof returned[0]).toBe('object'); + expect(returned[0].start).toBe(startDate); + expect(returned[0].end).toBe(endDate); + expect(returned[0].note).toBe(note); + mediawikiStorage.get.restore(); + done(); + }); + }); + + it('should return empty list when metric has no annotations info', function (done) { + sinon.stub(mediawikiStorage, 'get', function (options) { + expect(true).toBe(false); // should not get here + }); + + var metric = {}; // metric has no annotations information + annotationsApi.get(metric, function (returned) { + expect(returned instanceof Array).toBe(true); + expect(returned.length).toBe(0); + mediawikiStorage.get.restore(); + done(); + }); + }); + + it('should filter out annotations with invalid dates', function (done) { + sinon.stub(mediawikiStorage, 'get', function (options) { + var deferred = new $.Deferred(); + deferred.resolve([ + {start: 'Bad date', note: 'Some note.'}, + {start: '2014-01-01 00:00:00', note: 'Some note.'} + ]); + return deferred.promise(); + }); + + var metric = { + annotations: { + host: 'some.mediawiki.host', + pageName: 'SomeMediawikiPageName' + } + }; + annotationsApi.get(metric, function (returned) { + expect(returned instanceof Array).toBe(true); + expect(returned.length).toBe(1); + mediawikiStorage.get.restore(); + done(); + }); + }); + + it('should filter out annotations with no note', function (done) { + sinon.stub(mediawikiStorage, 'get', function (options) { + var deferred = new $.Deferred(); + deferred.resolve([ + {start: '2014-01-01 00:00:00'}, + {start: '2014-01-01 00:00:00', note: 'Some note.'} + ]); + return deferred.promise(); + }); + + var metric = { + annotations: { + host: 'some.mediawiki.host', + pageName: 'SomeMediawikiPageName' + } + }; + annotationsApi.get(metric, function (returned) { + expect(returned instanceof Array).toBe(true); + expect(returned.length).toBe(1); + mediawikiStorage.get.restore(); + done(); + }); + }); + + it('should filter out annotations with bad time interval', function (done) { + sinon.stub(mediawikiStorage, 'get', function (options) { + var deferred = new $.Deferred(); + deferred.resolve([ + // end date before start date + {start: '2014-01-01', end: '2013-01-01', note: 'Some note.'}, + {start: '2014-01-01', end: '2014-01-02', note: 'Some note.'} + ]); + return deferred.promise(); + }); + + var metric = { + annotations: { + host: 'some.mediawiki.host', + pageName: 'SomeMediawikiPageName' + } + }; + annotationsApi.get(metric, function (returned) { + expect(returned instanceof Array).toBe(true); + expect(returned.length).toBe(1); + mediawikiStorage.get.restore(); + done(); + }); + }); }); }); -- To view, visit https://gerrit.wikimedia.org/r/181424 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: If372e9d6065aefc7ad0d6abb49894b18f06b7caf Gerrit-PatchSet: 1 Gerrit-Project: analytics/dashiki Gerrit-Branch: master Gerrit-Owner: Mforns <mfo...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits