Nuria has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/270867

Change subject: [WIP] Dashiki gets pageview data from pageview API
......................................................................

[WIP] Dashiki gets pageview data from pageview API

TODO: Mobile breakdowns, testing

Bug:T124063

Change-Id: I1d403595cd3b25a099721227f6a1cf0b5fddd34d
---
M package.json
M src/app/apis/pageview-api.js
M src/app/config.js
M src/app/data-converters/factory.js
A src/app/data-converters/pageview-api-response.js
M src/app/data-converters/wikimetrics-timeseries.js
M src/app/require.config.js
A src/app/sitematrix.js
M src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
A src/lib/pageviews.js
10 files changed, 671 insertions(+), 75 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/analytics/dashiki 
refs/changes/67/270867/1

diff --git a/package.json b/package.json
index 326b2e4..649ef8f 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
   "name": "dashiki",
   "version": "0.0.0",
   "devDependencies": {
+    "pageviews": "~1.0.2",
     "chalk": "~0.4.0",
     "cheerio": "^0.19.0",
     "deeply": "~0.1.0",
diff --git a/src/app/apis/pageview-api.js b/src/app/apis/pageview-api.js
index e2767bf..2f3fed5 100644
--- a/src/app/apis/pageview-api.js
+++ b/src/app/apis/pageview-api.js
@@ -12,7 +12,10 @@
         dataConverterFactory = require('dataConverterFactory'),
         uri = require('uri/URI'),
         logger = require('logger'),
-        TimeseriesData = require('converters.timeseries');
+        moment = require('moment'),
+        pageviews = require('pageviews'),
+        config = require('config'),
+        sitematrix = require('sitematrix');
 
     require('uri/URITemplate');
 
@@ -35,20 +38,20 @@
     PageviewApi.prototype.getData = function (metric, project, showBreakdown) {
         var deferred = new $.Deferred();
 
-        //using christian's endpoint
-        //  http://quelltextlich.at/wmf/projectcounts/daily/enwiki.csv
-        var address = 
uri.expand('https://{root}/static/public/datafiles/{metric}/{project}.csv', {
-            root: this.root,
-            metric: metric.name,
-            project: project
-        }).toString();
+        var endDate = moment().format('YYYYMMDDHH');
 
-        $.ajax({
-            //let jquery decide datatype, otherwise things do not work when 
retrieving cvs
-            url: address
-        }).done(function (data) {
-            var converter = this.getDataConverter(),
-                opt = {
+        sitematrix.getProjectUrl(config, project).done(function (projectUrl) {
+
+            pageviews.getAggregatedPageviews({
+                project: projectUrl,
+                granularity: 'daily',
+                start: '2015010100',
+                end: endDate
+            }).then(function (data) {
+                // console.log(JSON.stringify(result, null, 2));
+
+                var converter = this.dataConverter;
+                var opt = {
                     label: project,
                     allColumns: showBreakdown,
                     varyColors: false,
@@ -57,16 +60,18 @@
                     startDate: '2014-01-01'
                 };
 
-            deferred.resolve(converter(opt, data));
-        }.bind(this))
-        .fail(function (error) {
-            // resolve as done with empty results and log the error
-            // to avoid crashing the ui when a metric has problems
-            deferred.resolve(new TimeseriesData());
-            logger.error(error);
-        });
+                deferred.resolve(converter(opt, data));
+
+            }.bind(this)).catch(function (error) {
+                console.log(error);
+            });
+
+
+        }.bind(this));
 
         return deferred.promise();
+
+
     };
 
     PageviewApi.prototype.getDataConverter = function () {
@@ -74,4 +79,4 @@
     };
 
     return new PageviewApi(siteConfig);
-});
+});
\ No newline at end of file
diff --git a/src/app/config.js b/src/app/config.js
index c3ba165..0b23223 100644
--- a/src/app/config.js
+++ b/src/app/config.js
@@ -29,10 +29,9 @@
 
         },
 
-        //placeholder for now, note this is coming from a temporary domain
         pageviewApi: {
-            endpoint: 'metrics.wmflabs.org', // needs to support https
-            format: 'csv'
+            endpoint: '', // not used
+            format: 'pageview-api-response'
 
         },
 
@@ -41,9 +40,13 @@
             format: 'tsv'
         },
 
+        // this doc does not have
+        sitematrix: {
+            endpoint: 
'https://meta.wikimedia.org/w/api.php?action=sitematrix&formatversion=2&format=json&maxage=3600&smaxage=3600'
+        }
 
         //urlProjectLanguageChoices: 
'/stubs/fake-wikimetrics/projectLanguageChoices.json',
         //urlCategorizedMetrics: 
'/stubs/fake-wikimetrics/categorizedMetrics.json',
         //urlDefaultDashboard: '/stubs/fake-wikimetrics/defaultDashboard.json',
     };
-});
+});
\ No newline at end of file
diff --git a/src/app/data-converters/factory.js 
b/src/app/data-converters/factory.js
index 31aa751..18d8b60 100644
--- a/src/app/data-converters/factory.js
+++ b/src/app/data-converters/factory.js
@@ -6,7 +6,8 @@
     'use strict';
 
     var separatedValues = require('converters.separated-values'),
-        wikimetricsTimeseries = require('converters.wikimetrics-timeseries');
+        wikimetricsTimeseries = require('converters.wikimetrics-timeseries'),
+        pageviewApiResponse = require('converters.pageview-api-response');
 
     function ConverterFactory() {
         return;
@@ -33,6 +34,8 @@
 
         case 'json':
             return wikimetricsTimeseries();
+        case 'pageview-api-response':
+            return pageviewApiResponse();
 
         }
     };
diff --git a/src/app/data-converters/pageview-api-response.js 
b/src/app/data-converters/pageview-api-response.js
new file mode 100644
index 0000000..ac06564
--- /dev/null
+++ b/src/app/data-converters/pageview-api-response.js
@@ -0,0 +1,64 @@
+/**
+ * This module returns a method that knows how to translate json data from
+ *  pageview API to the canonical format understood by dashiki
+
+ * Responses look like:
+{"items":[
+{"project":"en.wikipedia","access":"all-access","agent":"all-agents","granularity":"daily","timestamp":"2015120200","views":291268249},
+{"project":"en.wikipedia","access":"all-access","agent":"all-agents","granularity":"daily","timestamp":"2015120300","views":284829416},
+{"project":"en.wikipedia","access":"all-access","agent":"all-agents","granularity":"daily","timestamp":"2015120400","views":280259970}
+]
+*/
+define(function (require) {
+    'use strict';
+
+    var _ = require('lodash'),
+        moment = require('moment'),
+        sitematrix = require('sitematrix'),
+        TimeseriesData = require('converters.timeseries');
+
+    /**
+     * Parameters
+     *   options    : a dictionary of options.  Required options:
+     *     defaultSubmetrics - dictionary of metric names to default 
submetrics to use
+     *
+     *   rawData            : json data, as fetched from the pageview API 
public endpoint
+     * Returns
+     *   A TimeseriesData instance
+     */
+
+    return function () {
+
+        return function (options, rawData) {
+
+            if (!_.has(rawData, 'items')) {
+                return new TimeseriesData([]);
+            }
+
+            var parameters = rawData.parameters,
+                metricName = 'pageviews',
+                project;
+
+
+
+            // transform array of items into hash so TimeSeries data can 
digest it
+            var dict = {};
+            _.forEach(rawData.items, function (value, index) {
+                var ts = moment(value.timestamp, 
"YYYYMMDDHH").format('YYYY-MM-DD');
+
+                dict[ts] = [
+                    [value.views ? parseFloat(value.views) : null]
+                ]
+            });
+
+            return new TimeseriesData(
+                // labels and colors can use the cohort name
+                ["Total"], //TODO
+                // wrap the values in an array to match the header
+                dict, [project],
+                // but keep patterns globally constant
+                [0]
+            );
+        };
+    };
+});
\ No newline at end of file
diff --git a/src/app/data-converters/wikimetrics-timeseries.js 
b/src/app/data-converters/wikimetrics-timeseries.js
index 113b300..a6abff3 100644
--- a/src/app/data-converters/wikimetrics-timeseries.js
+++ b/src/app/data-converters/wikimetrics-timeseries.js
@@ -1,6 +1,19 @@
 /**
  * This module returns a method that knows how to translate json data from
- *   wikimetrics to the canonical timeseries format understood by dashiki
+ *  wikimetrics to the canonical timeseries format understood by dashiki
+ * wikimetrics format is as follows:
+ * {{
+    "result": {
+        "Sum": {
+            "edits": {
+                "2015-06-12 00:00:00": 24625.0,
+                "2015-09-18 00:00:00": 20972.0,
+                "2015-09-01 00:00:00": 30450.0,
+                "2015-12-29 00:00:00": 26637.0,
+                "2015-08-14 00:00:00": 21502.0,
+                "2015-08-22 00:00:00": 21239.0,
+
+                }
  */
 define(function (require) {
     'use strict';
@@ -40,7 +53,7 @@
                 // labels and colors can use the cohort name
                 [parameters.Cohort],
                 // wrap the values in an array to match the header
-                _.forEach(rawData.result.Sum[submetric], function (value, key, 
dict) {
+                _.forEach(rawData.items, function (value, key, dict) {
                     dict[key] = [[value ? parseFloat(value) : null]];
                 }),
                 [parameters.Cohort],
diff --git a/src/app/require.config.js b/src/app/require.config.js
index 5e94fe2..df74ca1 100644
--- a/src/app/require.config.js
+++ b/src/app/require.config.js
@@ -5,63 +5,66 @@
 var require = {
     baseUrl: '/src',
     paths: {
-        'jquery'                : 'bower_modules/jquery/dist/jquery',
-        'lodash'                : 'bower_modules/lodash/main',
+        'jquery': 'bower_modules/jquery/dist/jquery',
+        'lodash': 'bower_modules/lodash/main',
         // NOTE: the minified ko build is broken in 3.2.0
         // (Issue reported https://github.com/knockout/knockout/issues/1528)
-        'knockout'              : 'bower_modules/knockout/dist/knockout.debug',
-        'text'                  : 'bower_modules/requirejs-text/text',
-        'd3'                    : 'bower_modules/d3/d3',
-        'vega'                  : 'bower_modules/vega/vega',
-        'topojson'              : 'bower_modules/topojson/topojson',
-        'moment'                : 'bower_modules/moment/moment',
-        'semantic-dropdown'     : 
'bower_modules/semantic/build/uncompressed/modules/dropdown',
-        'semantic-popup'        : 
'bower_modules/semantic/build/uncompressed/modules/popup',
-        'mediawiki-storage'     : 
'bower_modules/mediawiki-storage/dist/mediawiki-storage',
-        'marked'                : 'bower_modules/marked/lib/marked',
-        'twix'                  : 'bower_modules/twix/bin/twix',
-        'dygraphs'              : 'bower_modules/dygraphs/dygraph-combined',
-        'nvd3'                  : 'bower_modules/nvd3/build/nv.d3',
-        'rickshaw'              : 'bower_modules/rickshaw/rickshaw',
+        'knockout': 'bower_modules/knockout/dist/knockout.debug',
+        'text': 'bower_modules/requirejs-text/text',
+        'd3': 'bower_modules/d3/d3',
+        'vega': 'bower_modules/vega/vega',
+        'topojson': 'bower_modules/topojson/topojson',
+        'moment': 'bower_modules/moment/moment',
+        'semantic-dropdown': 
'bower_modules/semantic/build/uncompressed/modules/dropdown',
+        'semantic-popup': 
'bower_modules/semantic/build/uncompressed/modules/popup',
+        'mediawiki-storage': 
'bower_modules/mediawiki-storage/dist/mediawiki-storage',
+        'marked': 'bower_modules/marked/lib/marked',
+        'twix': 'bower_modules/twix/bin/twix',
+        'dygraphs': 'bower_modules/dygraphs/dygraph-combined',
+        'nvd3': 'bower_modules/nvd3/build/nv.d3',
+        'rickshaw': 'bower_modules/rickshaw/rickshaw',
         // NOTE: if you want functions like uri.expand, you must include both
         // URI and URITemplate like define(['uri/URI', 'uri/URITemplate'] ...
         // because URITemplate modifies URI when it's parsed
-        'uri'                   : 'bower_modules/URIjs/src',
-        'config'                : 'app/config',
-        'logger'                : 'lib/logger',
+        'uri': 'bower_modules/URIjs/src',
+        'config': 'app/config',
+        'logger': 'lib/logger',
 
-        'api-finder'            : 'app/apis/api-finder',
-        'dataConverterFactory'  : 'app/data-converters/factory',
-        'typeahead'             : 
'bower_modules/typeahead.js/dist/typeahead.bundle',
-        'ajaxWrapper'           : 'lib/ajax-wrapper',
-        'utils'                 : 'lib/utils',
-        'window'                : 'lib/window',
-        'stateManager'          : 'lib/state-manager',
+        'api-finder': 'app/apis/api-finder',
+        'dataConverterFactory': 'app/data-converters/factory',
+        'typeahead': 'bower_modules/typeahead.js/dist/typeahead.bundle',
+        'ajaxWrapper': 'lib/ajax-wrapper',
+        'utils': 'lib/utils',
+        'window': 'lib/window',
+        'stateManager': 'lib/state-manager',
+        'pageviews': 'lib/pageviews',
+        'sitematrix': 'app/sitematrix',
 
         // *** viewmodels
-        'viewmodels.copy-params'    : 
'app/ko-extensions/common-viewmodels/copy-params',
-        'viewmodels.single-select'  : 
'app/ko-extensions/common-viewmodels/single-select',
+        'viewmodels.copy-params': 
'app/ko-extensions/common-viewmodels/copy-params',
+        'viewmodels.single-select': 
'app/ko-extensions/common-viewmodels/single-select',
 
         // *** custom observables
-        'observables.async'         : 'app/ko-extensions/async-observables',
+        'observables.async': 'app/ko-extensions/async-observables',
 
         // *** apis
-        'apis.wikimetrics'          : 'app/apis/wikimetrics',
-        'apis.annotations'          : 'app/apis/annotations-api',
-        'apis.pageview'             : 'app/apis/pageview-api',
-        'apis.datasets'             : 'app/apis/datasets-api',
-        'apis.config'               : 'app/apis/config-api',
+        'apis.wikimetrics': 'app/apis/wikimetrics',
+        'apis.annotations': 'app/apis/annotations-api',
+        'apis.pageview': 'app/apis/pageview-api',
+        'apis.datasets': 'app/apis/datasets-api',
+        'apis.config': 'app/apis/config-api',
 
         // *** converters
-        'converters.separated-values'       : 
'app/data-converters/separated-values',
+        'converters.separated-values': 'app/data-converters/separated-values',
         'converters.simple-separated-values': 
'app/data-converters/simple-separated-values',
-        'converters.wikimetrics-timeseries' : 
'app/data-converters/wikimetrics-timeseries',
-        'converters.funnel-data'            : 
'app/data-converters/funnel-data',
-        'converters.timeseries'             : 
'app/data-converters/timeseries-data',
-        'converters.annotations'            : 
'app/data-converters/annotations-data',
+        'converters.wikimetrics-timeseries': 
'app/data-converters/wikimetrics-timeseries',
+        'converters.funnel-data': 'app/data-converters/funnel-data',
+        'converters.timeseries': 'app/data-converters/timeseries-data',
+        'converters.annotations': 'app/data-converters/annotations-data',
+        'converters.pageview-api-response': 
'app/data-converters/pageview-api-response',
 
         // *** lib
-        'lib.polyfills'             : 'lib/polyfills',
+        'lib.polyfills': 'lib/polyfills',
     },
     shim: {
         'ajaxWrapper': {
@@ -74,13 +77,15 @@
             //typeahead
             deps: ['jquery']
         },
-        d3: { exports: 'd3' },
+        d3: {
+            exports: 'd3'
+        },
         nvd3: {
-          exports: 'nv',
-          deps: ['d3']
+            exports: 'nv',
+            deps: ['d3']
         },
         'semantic-popup': {
-          deps: ['jquery']
+            deps: ['jquery']
         },
     }
 };
diff --git a/src/app/sitematrix.js b/src/app/sitematrix.js
new file mode 100644
index 0000000..56a6627
--- /dev/null
+++ b/src/app/sitematrix.js
@@ -0,0 +1,87 @@
+/**
+ * Mdule that gets the sitematrix and parses it.
+ * Site matrix location is on config.js
+ * Once initialized this class is just a singleton that holds an application 
scoped cache
+ * 
https://meta.wikimedia.org/w/api.php?action=sitematrix&formatversion=2&format=json&&maxage=3600&smaxage=3600
+ * Format:
+ * {"sitematrix":{"count":894,
+ * "0":{"code":"aa","name":"Qafár af","
+    
site":[{"url":"https://aa.wikipedia.org","dbname":"aawiki","code":"wiki","sitename":"Wikipedia","closed":""},{"url":"https://aa.wiktionary.org","dbname":"aawiktionary","code":"wiktionary","sitename":"Wiktionary","closed":""},{"url":"https://aa.wikibooks.org","dbname":"aawikibooks","code":"wikibooks","sitename":"Wikibooks","closed":""}],"localname":"Afar"},
+ * "1":{"code":"ab","name":"Аҧсшәа",
+    
"site":[{"url":"https://ab.wikipedia.org","dbname":"abwiki","code":"wiki","sitename":"Авикипедиа"},
+ */
+define(function (require) {
+    'use strict';
+
+    var config = require('config'),
+        _ = require('lodash');
+
+
+    function Sitematrix() {}
+
+    Sitematrix.cache = null;
+    /**
+     * Jsonp request for sitematrix, wikimedia api doesn't allow CORS
+     * from non-whitelisted domains
+     */
+    Sitematrix.prototype.getProjectUrl = function (config, dbname) {
+        var endpoint = config.sitematrix.endpoint;
+        var deferred = new $.Deferred();
+
+        // jsonp request for sitematrix, cors is not allowed
+        $.ajax({
+                url: endpoint,
+                // Tell jQuery we're expecting JSONP
+                dataType: "jsonp",
+                //otherwise jquery takes the liberty of not caching your jsonp 
requests
+                cache: true
+
+            }).then(function (data) {
+
+                // transform sitematrix in structure that allows easy o(1) 
lookups
+                var _sitematrix = {};
+                _.forEach(data.sitematrix, function (value, key) {
+
+                    _.forEach(value.site, function (_value, index) {
+
+                        /*
+                        Each record is like:
+                        closed: ""
+                        code: "wiki"
+                        dbname: "aawiki"
+                        sitename: "Wikipedia"
+                        url: "https://aa.wikipedia.org";
+                        url needs to be transform to: aa.wikipedia
+                        */
+                        var urlEndpoint = _value.url.replace("https://";, 
"").replace(".org", "");
+
+                        //building lookup both ways
+                        _sitematrix[_value.dbname] = urlEndpoint;
+                        _sitematrix[urlEndpoint] = _value.dbname;
+
+                    });
+
+                });
+                // can we populate an application wide cache now? or is this 
bad practice?
+                if (!Sitematrix.cache) {
+                    Sitematrix.cache = _sitematrix;
+                }
+
+
+                deferred.resolve(_sitematrix[dbname]);
+            })
+            .fail(function (error) {
+
+                logger.error(error);
+                deferred.reject(error);
+            });
+
+
+
+        return deferred.promise();
+    };
+
+
+
+    return new Sitematrix();
+});
\ No newline at end of file
diff --git a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js 
b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
index a633cbe..b058eb0 100644
--- a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
+++ b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
@@ -43,10 +43,14 @@
                 promises = projects.map(function (project) {
                     return api.getData(metric, project.database, 
showBreakdown);
                 });
+
+                //invoqued when all promises are done
                 $.when.apply(this, promises).then(function () {
+
                     
this.mergedData(TimeseriesData.mergeAll(_.toArray(arguments)));
                     this.applyColors(projects);
                 }.bind(this));
+
             } else {
                 this.mergedData(new TimeseriesData([]));
             }
@@ -88,4 +92,4 @@
         viewModel: WikimetricsVisualizer,
         template: templateMarkup
     };
-});
+});
\ No newline at end of file
diff --git a/src/lib/pageviews.js b/src/lib/pageviews.js
new file mode 100644
index 0000000..a051c27
--- /dev/null
+++ b/src/lib/pageviews.js
@@ -0,0 +1,411 @@
+/**
+ * @license
+ * Copyright 2015 Thomas Steiner (@tomayac). All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define([], function () {
+  'use strict';
+
+  var request;
+
+  var USER_AGENT = 'pageviews.js';
+
+  //TODO pull request bower module
+
+  // Dynamically adapt to the runtime environment
+  var environment = typeof window === 'undefined' ? 'node' : 'browser';
+  if (environment === 'node') {
+    // Node.js
+    request = require('request');
+    // browser chokes on this due to stric
+    //TODO pull request
+    //var package = require('./package.json');
+    // The user agent to use
+    USER_AGENT = 'pageviews.js';
+  } else {
+    // Browser
+    request = function (options, callback) {
+      var xhr = new XMLHttpRequest();
+      xhr.addEventListener('load', function () {
+        return callback(null, {
+          statusCode: this.status
+        }, this.responseText);
+      });
+      xhr.addEventListener('error', function (e) {
+        return callback(e);
+      });
+      xhr.open('GET', options.url);
+      xhr.send();
+    };
+  }
+
+  var pageviews = (function () {
+    // The Pageviews base URL
+    var BASE_URL = 'https://wikimedia.org/api/rest_v1';
+
+    var _access = {
+      default: 'all-access',
+      allowed: ['all-access', 'desktop', 'mobile-web', 'mobile-app']
+    };
+
+    var _agent = {
+      default: 'all-agents',
+      allowed: ['all-agents', 'user', 'spider', 'bot']
+    };
+
+    var _granularityAggregated = {
+      default: 'hourly',
+      allowed: ['daily', 'hourly', 'monthly']
+    };
+
+    var _granularityPerArticle = {
+      default: 'daily',
+      allowed: ['daily']
+    };
+
+    /**
+     * Checks the input parameters for validity.
+     */
+    var _checkParams = function (params, caller) {
+      if (!params) {
+        return new Error('Required parameters missing.');
+      }
+      // Required: project or projects
+      if ((!params.project) && (!params.projects)) {
+        if (caller === 'getAggregatedPageviews' || caller === 
'getTopPageviews') {
+          return new Error('Required parameter "project" or "projects" 
missing.');
+        } else {
+          return new Error('Required parameter "project" missing.');
+        }
+      }
+      if ((params.project) && (params.project.indexOf('.') === -1)) {
+        return new Error('Required parameter "project" invalid.');
+      }
+      if ((caller === 'getAggregatedPageviews') ||
+        (caller === 'getTopPageviews')) {
+        if (params.projects) {
+          if ((!Array.isArray(params.projects)) || (!params.projects.length) ||
+            (params.projects.filter(function (project) {
+              return project.indexOf('.') === -1;
+            }).length)
+          ) {
+            return new Error('Required parameter "projects" invalid.');
+          }
+        }
+      }
+      // Required: article or articles
+      if (caller === 'getPerArticlePageviews') {
+        if ((!params.article) && (!params.articles)) {
+          return new Error('Required parameter "article" or "articles" 
missing.');
+        }
+        if (params.articles) {
+          if ((!Array.isArray(params.articles)) || (!params.articles.length)) {
+            return new Error('Required parameter "articles" invalid.');
+          }
+        }
+      }
+      if (caller === 'getPerArticlePageviews') {
+        // Required: start
+        if ((!params.start) ||
+          (!/^(?:19|20)\d\d[- /.]?(?:0[1-9]|1[012])[- 
/.]?(?:0[1-9]|[12][0-9]|3[01])$/.test(params.start))) {
+          return new Error('Required parameter "start" missing or invalid.');
+        }
+        // Required: end
+        if ((!params.end) ||
+          (!/^(19|20)\d\d[- /.]?(0[1-9]|1[012])[- 
/.]?(0[1-9]|[12][0-9]|3[01])$/.test(params.end))) {
+          return new Error('Required parameter "end" missing or invalid.');
+        }
+      } else if (caller === 'getAggregatedPageviews') {
+        // Required: start
+        if ((!params.start) ||
+          (!/^(?:19|20)\d\d[- /.]?(?:0[1-9]|1[012])[- 
/.]?(?:0[1-9]|[12][0-9]|3[01])[- /.]?(?:[012][0-9])$/.test(params.start))) {
+          return new Error('Required parameter "start" missing or invalid.');
+        }
+        // Required: end
+        if ((!params.end) ||
+          (!/^(19|20)\d\d[- /.]?(0[1-9]|1[012])[- 
/.]?(0[1-9]|[12][0-9]|3[01])[- /.]?(?:[012][0-9])$/.test(params.end))) {
+          return new Error('Required parameter "end" missing or invalid.');
+        }
+      }
+      if (caller === 'getTopPageviews') {
+        // Required: year
+        if ((!params.year) || (!/^(?:19|20)\d\d$/.test(params.year))) {
+          return new Error('Required parameter "year" missing or invalid.');
+        }
+        // Required: month
+        if ((!params.month) || (!/^(?:0?[1-9]|1[012])$/.test(params.month))) {
+          return new Error('Required parameter "month" missing or invalid.');
+        }
+        // Required: day
+        if ((!params.day) || 
(!/^(?:0?[1-9]|[12][0-9]|3[01])$/.test(params.day))) {
+          return new Error('Required parameter "day" missing or invalid.');
+        }
+        if ((params.limit) && !/^\d+$/.test(params.limit) &&
+          (0 < params.limit) && (params.limit <= 1000)) {
+          return new Error('Invalid optional parameter "limit".');
+        }
+      }
+      // Optional: access
+      if ((params.access) && (_access.allowed.indexOf(params.access) === -1)) {
+        return new Error('Invalid optional parameter "access".');
+      }
+      // Optional: agent
+      if ((params.agent) && (_agent.allowed.indexOf(params.agent) === -1)) {
+        return new Error('Invalid optional parameter "agent".');
+      }
+      // Optional: granularity
+      if (params.granularity) {
+        if (caller === 'getAggregatedPageviews') {
+          if (_granularityAggregated.allowed.indexOf(params.granularity) === 
-1) {
+            return new Error('Invalid optional parameter "granularity".');
+          }
+        } else if (caller === 'getPerArticlePageviews') {
+          if (_granularityPerArticle.allowed.indexOf(params.granularity) === 
-1) {
+            return new Error('Invalid optional parameter "granularity".');
+          }
+        }
+      }
+      return params;
+    };
+
+    /**
+     * Checks the results for validity, in case of success returns the parsed
+     * data, else returns the error details.
+     */
+    var _checkResult = function (error, response, body) {
+      var data;
+      if (error || response.statusCode !== 200) {
+        if (error) {
+          return error;
+        }
+        if (response.statusCode === 404) {
+          try {
+            data = JSON.parse(body);
+            return new Error(data.detail);
+          } catch (e) {
+            return new Error(e);
+          }
+        }
+        return new Error('Status code ' + response.statusCode);
+      }
+      try {
+        data = JSON.parse(body);
+      } catch (e) {
+        return new Error(e);
+      }
+      return data;
+    };
+
+    var _getPerArticlePageviews = function (params) {
+      return new Promise(function (resolve, reject) {
+        params = _checkParams(params, 'getPerArticlePageviews');
+        if (params.stack) {
+          return reject(params);
+        }
+        // Call yourself recursively in case of multiple articles
+        if (params.articles) {
+          var promises = [];
+          params.articles.map(function (article, i) {
+            var newParams = params;
+            delete newParams.articles;
+            newParams.article = article;
+            promises[i] = _getPerArticlePageviews(newParams);
+          });
+          return resolve(Promise.all(promises));
+        }
+        // Required params
+        var project = params.project;
+        var article = encodeURIComponent(params.article.replace(/\s/g, '_'));
+        var start = params.start;
+        var end = params.end;
+        // Optional params
+        var access = params.access ? params.access : _access.default;
+        var agent = params.agent ? params.agent : _agent.default;
+        var granularity = params.granularity ?
+          params.granularity : _granularityPerArticle.default;
+
+        var options = {
+          url: BASE_URL + '/metrics/pageviews/per-article' +
+            '/' + project +
+            '/' + access +
+            '/' + agent +
+            '/' + article +
+            '/' + granularity +
+            '/' + start +
+            '/' + end,
+          headers: {
+            'User-Agent': USER_AGENT
+          }
+        };
+        request(options, function (error, response, body) {
+          var result = _checkResult(error, response, body);
+          if (result.stack) {
+            return reject(result);
+          }
+          return resolve(result);
+        });
+      });
+    };
+
+    var _getAggregatedPageviews = function (params) {
+      return new Promise(function (resolve, reject) {
+        params = _checkParams(params, 'getAggregatedPageviews');
+        if (params.stack) {
+          return reject(params);
+        }
+        // Call yourself recursively in case of multiple projects
+        if (params.projects) {
+          var promises = [];
+          params.projects.map(function (project, i) {
+            var newParams = params;
+            delete newParams.projects;
+            newParams.project = project;
+            promises[i] = _getAggregatedPageviews(newParams);
+          });
+          return resolve(Promise.all(promises));
+        }
+        // Required params
+        var project = params.project;
+        var start = params.start;
+        var end = params.end;
+        // Optional params
+        var access = params.access ? params.access : _access.default;
+        var agent = params.agent ? params.agent : _agent.default;
+        var granularity = params.granularity ?
+          params.granularity : _granularityAggregated.default;
+        var options = {
+          url: BASE_URL + '/metrics/pageviews/aggregate' +
+            '/' + project +
+            '/' + access +
+            '/' + agent +
+            '/' + granularity +
+            '/' + start +
+            '/' + end,
+          headers: {
+            'User-Agent': USER_AGENT
+          }
+        };
+        request(options, function (error, response, body) {
+          var result = _checkResult(error, response, body);
+          if (result.stack) {
+            return reject(result);
+          }
+          return resolve(result);
+        });
+      });
+    };
+
+    var _getTopPageviews = function (params) {
+      return new Promise(function (resolve, reject) {
+        params = _checkParams(params, 'getTopPageviews');
+        if (params.stack) {
+          return reject(params);
+        }
+        // Call yourself recursively in case of multiple projects
+        if (params.projects) {
+          var promises = [];
+          params.projects.map(function (project, i) {
+            var newParams = params;
+            delete newParams.projects;
+            newParams.project = project;
+            promises[i] = _getTopPageviews(newParams);
+          });
+          return resolve(Promise.all(promises));
+        }
+        // Required params
+        var project = params.project;
+        var year = params.year;
+        var month = typeof params.month === 'number' && params.month < 10 ?
+          '0' + params.month : params.month;
+        var day = typeof params.day === 'number' && params.day < 10 ?
+          '0' + params.day : params.day;
+        var limit = params.limit || false;
+        // Optional params
+        var access = params.access ? params.access : _access.default;
+        var options = {
+          url: BASE_URL + '/metrics/pageviews/top' +
+            '/' + project +
+            '/' + access +
+            '/' + year +
+            '/' + month +
+            '/' + day,
+          headers: {
+            'User-Agent': USER_AGENT
+          }
+        };
+        request(options, function (error, response, body) {
+          var result = _checkResult(error, response, body);
+          if (result.stack) {
+            return reject(result);
+          }
+          if (limit) {
+            result.items[0].articles = result.items[0].articles.slice(0, 
limit);
+          }
+          return resolve(result);
+        });
+      });
+    };
+
+    var _getPageviewsDimensions = function () {
+      return new Promise(function (resolve, reject) {
+        var options = {
+          url: BASE_URL + '/metrics/pageviews/',
+          headers: {
+            'User-Agent': USER_AGENT
+          }
+        };
+        request(options, function (error, response, body) {
+          var result = _checkResult(error, response, body);
+          if (result.stack) {
+            return reject(result);
+          }
+          return resolve(result);
+        });
+      });
+    };
+
+    return {
+      /**
+       * This is the root of all pageview data endpoints. The list of paths 
that
+       * this returns includes ways to query by article, project, top articles,
+       * etc. If browsing the interactive documentation, see the specifics for
+       * each endpoint below.
+       */
+      getPageviewsDimensions: _getPageviewsDimensions,
+
+      /**
+       * Given a Mediawiki article and a date range, returns a daily 
timeseries of
+       * its pageview counts. You can also filter by access method and/or agent
+       * type.
+       */
+      getPerArticlePageviews: _getPerArticlePageviews,
+
+      /**
+       * Given a date range, returns a timeseries of pageview counts. You can
+       * filter by project, access method and/or agent type. You can choose
+       * between daily and hourly granularity as well.
+       */
+      getAggregatedPageviews: _getAggregatedPageviews,
+
+      /**
+       * Lists the 1000 most viewed articles for a given project and timespan
+       * (year, month or day). You can filter by access method.
+       */
+      getTopPageviews: _getTopPageviews
+    };
+  });
+
+  return pageviews();
+});
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/270867
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I1d403595cd3b25a099721227f6a1cf0b5fddd34d
Gerrit-PatchSet: 1
Gerrit-Project: analytics/dashiki
Gerrit-Branch: master
Gerrit-Owner: Nuria <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to