<commit_before>/*jslint white: true */
/*global exports, String, Buffer, require, console */

exports.panasonicController = exports.panasonicController || (function () {
  'use strict';

  /**
   * @author brian@bevey.org
   * @fileoverview Basic control of Panasonic Smart TV
   * @requires http
   */
  return {
    version : '0.0.0.0.1 alpha',
    /**
     * Whitelist of available key codes to use.
     */
    keymap  : ['30S_SKIP', '3D', 'BD', 'BLUE', 'CANCEL', 'CC', 'CHG_INPUT', 'CHG_NETWORK', 'CH_DOWN', 'CH_UP', 'D0', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'DATA', 'DIGA_CTL', 'DISP_MODE', 'DMS_CH_DOWN', 'DMS_CH_UP', 'DOWN', 'DRIVE', 'ECO', 'ENTER', 'EPG', 'EZ_SYNC', 'FAVORITE', 'FF', 'GAME', 'GREEN', 'HOLD', 'INDEX', 'INFO', 'INTERNET', 'LEFT', 'MENU', 'MPX', 'MUTE', 'OFFTIMER', 'PAUSE', 'PICTAI', 'PLAY', 'POWER', 'PROG', 'P_NR', 'REC', 'RECLIST', 'RED', 'RETURN', 'REW', 'RIGHT', 'R_SCREEN', 'R_TUNE', 'R_TUNE', 'SAP', 'SD_CARD', 'SKIP_NEXT', 'SKIP_PREV', 'SPLIT', 'STOP', 'STTL', 'SUBMENU', 'SWAP', 'TEXT', 'TV', 'TV_MUTE_OFF', 'TV_MUTE_ON', 'UP', 'VIERA_LINK', 'VOD', 'VOLDOWN', 'VOLUP', 'VTOOLS', 'YELLOW'],

    /**
     * Prepare XML request.
     */
    requestEnvelope : function (config) {
      var response = '';

      response += '<?xml version="1.0" encoding="utf-8"?>';
      response += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">';
      response += '<s:Body>';
      response += '  <u:' + config.action + ' xmlns:u="urn:' + config.urn + '">';
      response += '    ' + config.args;
      response += '  </u:' + config.action + '>';
      response += '</s:Body>';
      response += '</s:Envelope>';

      return response;
    },

    /**
     * Prepare a POST request for a Roku command.
     */
    postPrepare : function (that) {
      var path    = '/',
          method  = 'POST';

      if(this.command) {
        path += 'keypress/' + this.command;
      }

      if(this.text) {

      }

      return {
        host    : this.deviceIp,
        port    : this.devicePort,
        path    : path,
        method  : method
      };
    },

    send : function (config) {
      this.deviceIp   = config.deviceIp;
      this.command    = config.command    || '';
      this.text       = config.text       || '';
      this.devicePort = config.devicePort || 55000;
      this.cbConnect  = config.cbConnect  || function () {};
      this.cbError    = config.cbError    || function () {};

      var that        = this,
          http        = require('http'),
          dataReply   = '',
          request;

      request = http.request(this.postPrepare(that), function(response) {
                  response.on('data', function(response) {
                    console.log('connected');

                    dataReply += response;
                  });

                  response.on('end', function() {
                    that.cbConnect(dataReply);
                  });
                });

      request.on('error', function(error) {
        var errorMsg = '';

        if(error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED') {
          errorMsg = 'Device is off or unreachable';
        }

        else {
          errorMsg = error.code;
        }

        console.log(errorMsg);

        that.cbError(errorMsg);
      });

      request.end();

      return dataReply;
    }
  };
} ());<commit_msg>More work on Panasonic TVs.  This is probably very close, but completely untested (I dont have a TV to work off of).<commit_after>/*jslint white: true */
/*global exports, String, Buffer, require, console */

exports.panasonicController = exports.panasonicController || (function () {
  'use strict';

  /**
   * @author brian@bevey.org
   * @fileoverview Basic control of Panasonic Smart TV
   * @requires http
   * @note This is a full-scale rip-off of the fine work done here:
   *       http://cocoontech.com/forums/topic/21266-panasonic-viera-plasma-ip-control/page-2
   */
  return {
    version : '0.0.0.0.1 alpha',
    /**
     * Whitelist of available key codes to use.
     */
    keymap  : ['30S_SKIP', '3D', 'BD', 'BLUE', 'CANCEL', 'CC', 'CHG_INPUT', 'CHG_NETWORK', 'CH_DOWN', 'CH_UP', 'D0', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'DATA', 'DIGA_CTL', 'DISP_MODE', 'DMS_CH_DOWN', 'DMS_CH_UP', 'DOWN', 'DRIVE', 'ECO', 'ENTER', 'EPG', 'EZ_SYNC', 'FAVORITE', 'FF', 'GAME', 'GREEN', 'HOLD', 'INDEX', 'INFO', 'INTERNET', 'LEFT', 'MENU', 'MPX', 'MUTE', 'OFFTIMER', 'PAUSE', 'PICTAI', 'PLAY', 'POWER', 'PROG', 'P_NR', 'REC', 'RECLIST', 'RED', 'RETURN', 'REW', 'RIGHT', 'R_SCREEN', 'R_TUNE', 'R_TUNE', 'SAP', 'SD_CARD', 'SKIP_NEXT', 'SKIP_PREV', 'SPLIT', 'STOP', 'STTL', 'SUBMENU', 'SWAP', 'TEXT', 'TV', 'TV_MUTE_OFF', 'TV_MUTE_ON', 'UP', 'VIERA_LINK', 'VOD', 'VOLDOWN', 'VOLUP', 'VTOOLS', 'YELLOW'],

    /**
     * Prepare a POST request for a command.
     */
    postPrepare : function (that) {
      var path    = '/',
          method  = 'POST';

      if(that.command || that.text) {
        path += 'nrc/control_0';
      }

      else {
        path += 'dmr/control_0';
      }

      return {
        host    : that.deviceIp,
        port    : that.devicePort,
        path    : path,
        method  : method
      };
    },

    /**
     * Prepare a POST data the request.
     */
    postData : function (that) {
      var response = '',
          action   = 'X_SendKey',
          urn      = 'panasonic-com:service:p00NetworkControl:1',
          value    = '<X_KeyEvent>' + that.command + '</X_KeyEvent>';

      if(that.text) {
        action = 'X_SendString';
        urn    = 'panasonic-com:service:p00NetworkControl:1';
        value  = '<X_String>' + that.text + '</X_String>';
      }

      response += '<?xml version="1.0" encoding="utf-8"?>';
      response += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">';
      response += '<s:Body>';
      response += '  <u:' + action + ' xmlns:u="urn:' + urn + '">';
      response += '    ' + value;
      response += '  </u:' + action + '>';
      response += '</s:Body>';
      response += '</s:Envelope>';

      return response;
    },

    send : function (config) {
      this.deviceIp   = config.deviceIp;
      this.command    = config.command    || '';
      this.text       = config.text       || '';
      this.devicePort = config.devicePort || 55000;
      this.cbConnect  = config.cbConnect  || function () {};
      this.cbError    = config.cbError    || function () {};

      var that        = this,
          http        = require('http'),
          dataReply   = '',
          request;

      request = http.request(this.postPrepare(that), function(response) {
                  response.on('data', function(response) {
                    console.log('connected');

                    dataReply += response;
                  });

                  response.on('end', function() {
                    that.cbConnect(dataReply);
                  });
                });

      request.on('error', function(error) {
        var errorMsg = '';

        if(error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED') {
          errorMsg = 'Device is off or unreachable';
        }

        else {
          errorMsg = error.code;
        }

        console.log(errorMsg);

        that.cbError(errorMsg);
      });

      request.write(this.postData(that));
      request.end();

      return dataReply;
    }
  };
} ());<|endoftext|>


<commit_before>var config = {
  allScriptsTimeout: 11000,

  specs: [
    'e2e/**/*.js'
  ],

  capabilities: {
    'browserName': 'chrome',
    'platform': 'OS X 10.9',
    'deviceName': ''
  },

  baseUrl: 'http://localhost:8080/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  },

  onPrepare: function() {
    browser.driver.executeScript("window.name='PROTRACTOR';");
    browser.driver.manage().window().maximize();
  }
};

if (process.env.TRAVIS) {
  config.sauceUser = process.env.SAUCE_USERNAME;
  config.sauceKey = process.env.SAUCE_ACCESS_KEY;
  config.capabilities['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER;
  config.capabilities['build'] = process.env.TRAVIS_BUILD_NUMBER;
  config.capabilities['name'] = "coopr-ngui build#"+process.env.TRAVIS_BUILD_NUMBER;
}

module.exports.config = config;
<commit_msg>platform should not be set to run locally<commit_after>var config = {
  allScriptsTimeout: 11000,

  specs: [
    'e2e/**/*.js'
  ],

  capabilities: {
    'browserName': 'chrome'
  },

  baseUrl: 'http://localhost:8080/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  },

  onPrepare: function() {
    browser.driver.executeScript("window.name='PROTRACTOR';");
    browser.driver.manage().window().maximize();
  }
};

if (process.env.TRAVIS) {
  config.sauceUser = process.env.SAUCE_USERNAME;
  config.sauceKey = process.env.SAUCE_ACCESS_KEY;

  config.capabilities['platform'] = 'OS X 10.9';
  config.capabilities['deviceName'] = '';

  config.capabilities['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER;
  config.capabilities['build'] = process.env.TRAVIS_BUILD_NUMBER;
  config.capabilities['name'] = "coopr-ngui build#"+process.env.TRAVIS_BUILD_NUMBER;
}

module.exports.config = config;
<|endoftext|>


<commit_before>/*globals require, console*/
/**
 * Created by Zsolt on 5/16/2014.
 *
 * http://localhost:8888/rest/external/executor/
 * http://localhost:8888/rest/external/executor/create/[validhash]
 *
 * THIS IS A THROW AWAY CODE AND IMPLEMENTATION.
 *
 * TEMPORARY CODE AND IMPLEMENTATION.
 *
 */

define(['logManager',
    'fs',
    'path',
    'unzip',
    'child_process',
    'minimatch',
    'nedb',
    'executor/JobInfo',
    'executor/WorkerInfo'
    ],
    function (logManager, fs, path, unzip, child_process, minimatch, Datastore, JobInfo, WorkerInfo) {

    var logger = logManager.create('REST-External-Executor'); //how to define your own logger which will use the global settings

    var jobList = new Datastore({ filename: 'jobList.nedb', autoload: true });
    jobList.ensureIndex({ fieldName: 'hash', unique: true }, function (err) {
        if (err) {
            console.error(err);
            process.exit(1);
        }
    });

    var workerRefreshInterval = 5 * 1000;
    // worker = { clientId:, lastSeen: }
    var workerList = new Datastore({ filename: 'workerList.nedb', autoload: true});
    var workerTimeout = function() {
        if (process.uptime() < workerRefreshInterval / 1000 * 5) {
            return;
        }
        workerList.find({ lastSeen: { $lt: (new Date).getTime() / 1000 - workerRefreshInterval / 1000 * 5} }, function(err, docs) {
            for (var i = 0; i < docs.length; i += 1) {
                // reset unfinished jobs assigned to worker to CREATED, so they'll be executed by someone else
                logger.info('worker "' + docs[i].clientId + '" is gone');
                workerList.remove({_id: docs[i]._id});
                // FIXME: race after assigning finishTime between this and uploading to blob
                jobList.update({ worker: docs[i].clientId, finishTime: null}, { $set: { worker: null, status: 'CREATED', startTime: null }}, function () { });
            }
        });
    };
    setInterval(workerTimeout, 10 * 1000);

    var labelJobs = {}; // map from label to blob hash
    var labelJobsFilename = 'labelJobs.json'; // TODO put somewhere that makes sense
    function updateLabelJobs() {
        var fs = require('fs');
        fs.readFile(labelJobsFilename, {encoding: 'utf-8'}, function (err, data) {
            logger.info("Reading " + labelJobsFilename);
            labelJobs = JSON.parse(data);
        });
    }
    function watchLabelJobs() {
        var fs = require('fs');
        fs.exists(labelJobsFilename, function (exists) {
            if (exists) {
                updateLabelJobs();
                fs.watch(labelJobsFilename, { persistent: false }, function() { setTimeout(updateLabelJobs, 200); });
            } else {
                setTimeout(watchLabelJobs, 10 * 1000);
            }
        });
    };
    watchLabelJobs();

    var ExecutorREST = function(req,res,next){
        //global config is accessible via webGMEGlobal.getConfig()
        var config = webGMEGlobal.getConfig();
        // logger.debug('Executor request');

        var url = require('url').parse(req.url);
        var pathParts = url.pathname.split("/");

        if (pathParts.length < 2) {
            res.send(404);
            return;
        }

        if (pathParts.length === 2 && pathParts[1] === '') {
            //next should be always called / the response should be sent otherwise this thread will stop without and end

            var query = {}
            if (req.query.hasOwnProperty('status')) {
                query.status = req.query.status;
            }
            jobList.find(query, function(err, docs) {
                if (err) {
                    res.send(500);
                    return;
                }
                var jobList = {};
                for (var i = 0; i < docs.length; i += 1) {
                    jobList[docs[i].hash] = docs[i];
                    delete docs[i]._id;
                }
                res.send(jobList);
            });

            // TODO: send status
            // FIXME: this path will not be safe
//            res.sendfile(path.join('src', 'rest', 'executor', 'index2.html'), function (err) {
//                if (err) {
//                    logger.error(err);
//                    res.send(500);
//                }
//            });

        } else {

            switch (pathParts[1]) {
                case "create":
                    ExecutorRESTCreate(req, res, next);
                    break;
                case "worker":
                    ExecutorRESTWorkerAPI(req, res, next);
                    break;
                case "cancel":
                    ExecutorRESTCancel(req, res, next);
                    break;
                case "info":
                    ExecutorRESTInfo(req, res, next);
                    break;
                case "update":
                    ExecutorRESTUpdate(req, res, next);
                    break;
                default:
                    res.send(404);
                    break;
            }

        }

    };

    var ExecutorRESTCreate = function(req, res, next) {
        if (req.method !== 'POST') {
            res.send(405);
        }
        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(404);
            return;
        }
        var hash = url[2];

        var info = req.body;
        info.hash = hash;
        info.createTime = new Date().toISOString();
        var jobInfo = new JobInfo(info);
        // TODO: check if hash ok
        jobList.update({ hash: hash }, jobInfo, { upsert: true }, function(err) {
            if (err) {
                res.send(500);
            } else {
                delete jobInfo._id;
                res.send(jobInfo);
            }
        });

        // TODO: get job description
    };

    var ExecutorRESTUpdate = function(req, res, next) {
        if (req.method !== 'POST') {
            res.send(405);
        }
        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(404);
            return;
        }
        var hash = url[2];

        if (hash) {
        } else {
            res.send(500);
            return;
        }

        jobList.find({hash: hash}, function(err, docs) {
            if (err) {
                res.send(500);
            } else if (docs.length) {
                var jobInfo = new JobInfo(docs[0]);
                var jobInfoUpdate = new JobInfo(req.body);
                for (var i in jobInfoUpdate) {
                    if (jobInfoUpdate.hasOwnProperty(i)) {
                        jobInfo[i] = jobInfoUpdate[i];
                    }
                }
                jobList.update({ hash: hash }, jobInfo, function(err, numReplaced) {
                    if (err) {
                        res.send(500);
                    } else if (numReplaced !== 1) {
                        res.send(404);
                    } else {
                        res.send(200);
                    }
                });
            } else {
                res.send(404);
            }
        });

    };

    var ExecutorRESTWorkerGET = function(req, res, next) {
        var response = {};
        workerList.find({ }, function (err, workers) {
            var jobQuery = function (i) {
                if (i === workers.length) {
                    res.send(JSON.stringify(response));
                    return;
                }
                var worker = workers[i];
                jobList.find({status: 'RUNNING', worker: worker.clientId}).sort({createTime: 1}).exec(function (err, jobs) {
                    // FIXME: index jobList on status?
                    for (var j = 0; j < jobs.length; j += 1) {
                        delete jobs[j]._id;
                    }
                    response[worker.clientId] = jobs;

                    jobQuery(i + 1);
                });
            }
            jobQuery(0);
        });
    }

    var ExecutorRESTWorkerAPI = function(req, res, next) {
        if (req.method !== 'POST' && req.method !== 'GET') {
            res.send(405);
            return;
        }
        if (req.method === 'GET') {
            return ExecutorRESTWorkerGET(req, res, next);
        }

        var url = require('url').parse(req.url);
        var pathParts = url.pathname.split("/");

        if (pathParts.length < 2) {
            res.send(404);
            return;
        }

        var serverResponse = new WorkerInfo.ServerResponse({ refreshPeriod: workerRefreshInterval });
        serverResponse.labelJobs = labelJobs;
        var clientRequest = new WorkerInfo.ClientRequest(req.body);

        workerList.update({ clientId: clientRequest.clientId }, { $set: { lastSeen: (new Date).getTime() / 1000 }}, { upsert: true }, function() {
            if (clientRequest.availableProcesses) {
                jobList.find({status: 'CREATED', $not: { labels: {$nin: clientRequest.labels} }}).limit(clientRequest.availableProcesses).exec(function (err, docs) {
                    if (err) {
                        res.send(500);
                        return; // FIXME need to return 2x
                    }

                    var callback = function (i) {
                        if (i === docs.length) {
                            res.send(JSON.stringify(serverResponse));
                            return;
                        }
                        jobList.update({_id: docs[i]._id, status: 'CREATED'}, {$set: {status: 'RUNNING', worker: clientRequest.clientId}}, function (err, numReplaced) {
                            if (err) {
                                res.send(500);
                                return;
                            } else if (numReplaced) {
                                serverResponse.jobsToStart.push(docs[i].hash);
                            }
                            callback(i + 1);
                        });
                    };
                    callback(0);
                });
            } else {
                res.send(JSON.stringify(serverResponse));
            }
        });
    };

    var ExecutorRESTCancel = function(req, res, next) {
        if (req.method !== 'POST') {
            res.send(405);
            return;
        }

        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(500);
            return;
        }

        var hash = url[2];

        if (false) {
            // TODO
            executorBackend.cancelJob(hash);

            res.send(200);
        } else {
            res.send(500);
        }

    };

    var ExecutorRESTInfo = function(req, res, next) {
        if (req.method !== 'GET') {
            res.send(405);
            return;
        }

        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(500);
            return;
        }

        var hash = url[2];

        if (hash) {
            jobList.find({hash: hash}, function(err, docs) {
                if (err) {
                    res.send(500);
                } else if (docs.length) {
                    res.send(docs[0]);
                } else {
                    res.send(404);
                }
            });
        } else {
            res.send(500);
        }
    };

    var setup = function() { //it has to be done this way, but this is probably a placeholder for later option parameters...
        return ExecutorREST;
    };
    return setup();
});
<commit_msg>Expose worker metadata in GET .../executor/worker<commit_after>/*globals require, console*/
/**
 * Created by Zsolt on 5/16/2014.
 *
 * http://localhost:8888/rest/external/executor/
 * http://localhost:8888/rest/external/executor/create/[validhash]
 *
 * THIS IS A THROW AWAY CODE AND IMPLEMENTATION.
 *
 * TEMPORARY CODE AND IMPLEMENTATION.
 *
 */

define(['logManager',
    'fs',
    'path',
    'unzip',
    'child_process',
    'minimatch',
    'nedb',
    'executor/JobInfo',
    'executor/WorkerInfo'
    ],
    function (logManager, fs, path, unzip, child_process, minimatch, Datastore, JobInfo, WorkerInfo) {

    var logger = logManager.create('REST-External-Executor'); //how to define your own logger which will use the global settings

    var jobList = new Datastore({ filename: 'jobList.nedb', autoload: true });
    jobList.ensureIndex({ fieldName: 'hash', unique: true }, function (err) {
        if (err) {
            console.error(err);
            process.exit(1);
        }
    });

    var workerRefreshInterval = 5 * 1000;
    // worker = { clientId:, lastSeen: }
    var workerList = new Datastore({ filename: 'workerList.nedb', autoload: true});
    var workerTimeout = function() {
        if (process.uptime() < workerRefreshInterval / 1000 * 5) {
            return;
        }
        workerList.find({ lastSeen: { $lt: (new Date).getTime() / 1000 - workerRefreshInterval / 1000 * 5} }, function(err, docs) {
            for (var i = 0; i < docs.length; i += 1) {
                // reset unfinished jobs assigned to worker to CREATED, so they'll be executed by someone else
                logger.info('worker "' + docs[i].clientId + '" is gone');
                workerList.remove({_id: docs[i]._id});
                // FIXME: race after assigning finishTime between this and uploading to blob
                jobList.update({ worker: docs[i].clientId, finishTime: null}, { $set: { worker: null, status: 'CREATED', startTime: null }}, function () { });
            }
        });
    };
    setInterval(workerTimeout, 10 * 1000);

    var labelJobs = {}; // map from label to blob hash
    var labelJobsFilename = 'labelJobs.json'; // TODO put somewhere that makes sense
    function updateLabelJobs() {
        var fs = require('fs');
        fs.readFile(labelJobsFilename, {encoding: 'utf-8'}, function (err, data) {
            logger.info("Reading " + labelJobsFilename);
            labelJobs = JSON.parse(data);
        });
    }
    function watchLabelJobs() {
        var fs = require('fs');
        fs.exists(labelJobsFilename, function (exists) {
            if (exists) {
                updateLabelJobs();
                fs.watch(labelJobsFilename, { persistent: false }, function() { setTimeout(updateLabelJobs, 200); });
            } else {
                setTimeout(watchLabelJobs, 10 * 1000);
            }
        });
    };
    watchLabelJobs();

    var ExecutorREST = function(req,res,next){
        //global config is accessible via webGMEGlobal.getConfig()
        var config = webGMEGlobal.getConfig();
        // logger.debug('Executor request');

        var url = require('url').parse(req.url);
        var pathParts = url.pathname.split("/");

        if (pathParts.length < 2) {
            res.send(404);
            return;
        }

        if (pathParts.length === 2 && pathParts[1] === '') {
            //next should be always called / the response should be sent otherwise this thread will stop without and end

            var query = {}
            if (req.query.hasOwnProperty('status')) {
                query.status = req.query.status;
            }
            jobList.find(query, function(err, docs) {
                if (err) {
                    res.send(500);
                    return;
                }
                var jobList = {};
                for (var i = 0; i < docs.length; i += 1) {
                    jobList[docs[i].hash] = docs[i];
                    delete docs[i]._id;
                }
                res.send(jobList);
            });

            // TODO: send status
            // FIXME: this path will not be safe
//            res.sendfile(path.join('src', 'rest', 'executor', 'index2.html'), function (err) {
//                if (err) {
//                    logger.error(err);
//                    res.send(500);
//                }
//            });

        } else {

            switch (pathParts[1]) {
                case "create":
                    ExecutorRESTCreate(req, res, next);
                    break;
                case "worker":
                    ExecutorRESTWorkerAPI(req, res, next);
                    break;
                case "cancel":
                    ExecutorRESTCancel(req, res, next);
                    break;
                case "info":
                    ExecutorRESTInfo(req, res, next);
                    break;
                case "update":
                    ExecutorRESTUpdate(req, res, next);
                    break;
                default:
                    res.send(404);
                    break;
            }

        }

    };

    var ExecutorRESTCreate = function(req, res, next) {
        if (req.method !== 'POST') {
            res.send(405);
        }
        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(404);
            return;
        }
        var hash = url[2];

        var info = req.body;
        info.hash = hash;
        info.createTime = new Date().toISOString();
        var jobInfo = new JobInfo(info);
        // TODO: check if hash ok
        jobList.update({ hash: hash }, jobInfo, { upsert: true }, function(err) {
            if (err) {
                res.send(500);
            } else {
                delete jobInfo._id;
                res.send(jobInfo);
            }
        });

        // TODO: get job description
    };

    var ExecutorRESTUpdate = function(req, res, next) {
        if (req.method !== 'POST') {
            res.send(405);
        }
        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(404);
            return;
        }
        var hash = url[2];

        if (hash) {
        } else {
            res.send(500);
            return;
        }

        jobList.find({hash: hash}, function(err, docs) {
            if (err) {
                res.send(500);
            } else if (docs.length) {
                var jobInfo = new JobInfo(docs[0]);
                var jobInfoUpdate = new JobInfo(req.body);
                for (var i in jobInfoUpdate) {
                    if (jobInfoUpdate.hasOwnProperty(i)) {
                        jobInfo[i] = jobInfoUpdate[i];
                    }
                }
                jobList.update({ hash: hash }, jobInfo, function(err, numReplaced) {
                    if (err) {
                        res.send(500);
                    } else if (numReplaced !== 1) {
                        res.send(404);
                    } else {
                        res.send(200);
                    }
                });
            } else {
                res.send(404);
            }
        });

    };

    var ExecutorRESTWorkerGET = function(req, res, next) {
        var response = {};
        workerList.find({ }, function (err, workers) {
            var jobQuery = function (i) {
                if (i === workers.length) {
                    res.send(JSON.stringify(response));
                    return;
                }
                var worker = workers[i];
                jobList.find({status: 'RUNNING', worker: worker.clientId}).sort({createTime: 1}).exec(function (err, jobs) {
                    // FIXME: index jobList on status?
                    for (var j = 0; j < jobs.length; j += 1) {
                        delete jobs[j]._id;
                    }
                    delete worker._id;
                    response[worker.clientId] = worker;
                    response[worker.clientId].jobs = jobs;

                    jobQuery(i + 1);
                });
            }
            jobQuery(0);
        });
    }

    var ExecutorRESTWorkerAPI = function(req, res, next) {
        if (req.method !== 'POST' && req.method !== 'GET') {
            res.send(405);
            return;
        }
        if (req.method === 'GET') {
            return ExecutorRESTWorkerGET(req, res, next);
        }

        var url = require('url').parse(req.url);
        var pathParts = url.pathname.split("/");

        if (pathParts.length < 2) {
            res.send(404);
            return;
        }

        var serverResponse = new WorkerInfo.ServerResponse({ refreshPeriod: workerRefreshInterval });
        serverResponse.labelJobs = labelJobs;
        var clientRequest = new WorkerInfo.ClientRequest(req.body);

        workerList.update({ clientId: clientRequest.clientId }, { $set: { lastSeen: (new Date).getTime() / 1000, labels: clientRequest.labels }}, { upsert: true }, function() {
            if (clientRequest.availableProcesses) {
                jobList.find({status: 'CREATED', $not: { labels: {$nin: clientRequest.labels} }}).limit(clientRequest.availableProcesses).exec(function (err, docs) {
                    if (err) {
                        res.send(500);
                        return; // FIXME need to return 2x
                    }

                    var callback = function (i) {
                        if (i === docs.length) {
                            res.send(JSON.stringify(serverResponse));
                            return;
                        }
                        jobList.update({_id: docs[i]._id, status: 'CREATED'}, {$set: {status: 'RUNNING', worker: clientRequest.clientId}}, function (err, numReplaced) {
                            if (err) {
                                res.send(500);
                                return;
                            } else if (numReplaced) {
                                serverResponse.jobsToStart.push(docs[i].hash);
                            }
                            callback(i + 1);
                        });
                    };
                    callback(0);
                });
            } else {
                res.send(JSON.stringify(serverResponse));
            }
        });
    };

    var ExecutorRESTCancel = function(req, res, next) {
        if (req.method !== 'POST') {
            res.send(405);
            return;
        }

        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(500);
            return;
        }

        var hash = url[2];

        if (false) {
            // TODO
            executorBackend.cancelJob(hash);

            res.send(200);
        } else {
            res.send(500);
        }

    };

    var ExecutorRESTInfo = function(req, res, next) {
        if (req.method !== 'GET') {
            res.send(405);
            return;
        }

        var url = req.url.split('/');

        if (url.length < 3 || !url[2]) {
            res.send(500);
            return;
        }

        var hash = url[2];

        if (hash) {
            jobList.find({hash: hash}, function(err, docs) {
                if (err) {
                    res.send(500);
                } else if (docs.length) {
                    res.send(docs[0]);
                } else {
                    res.send(404);
                }
            });
        } else {
            res.send(500);
        }
    };

    var setup = function() { //it has to be done this way, but this is probably a placeholder for later option parameters...
        return ExecutorREST;
    };
    return setup();
});
<|endoftext|>


<commit_before>'use strict';

angular.module('myApp.masterPage', ['ngRoute'])

.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/', {
    templateUrl: 'masterPage/masterPage.html',
    controller: 'MasterPageCtrl'
  });
}])

.controller('MasterPageCtrl', [function() {

}]);
<commit_msg>Master controller now requests 10 latest blocks.<commit_after>'use strict';

var masterPage = angular.module('myApp.masterPage', ['ngRoute']);

masterPage.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/', {
    templateUrl: 'masterPage/masterPage.html',
    controller: 'MasterPageCtrl'
  });
}]);

masterPage.controller('MasterPageCtrl', ['$http', function($http) {

  this.rawBlocks = [];

  $http.get('/blockExplorer/q/latesthash').then(function(response) {
    this.getBlocks(10, response.data);
  }.bind(this));

  this.getBlocks = function(maxBlocks, fromHash) {
    this.chainGetBlock(this.getBlock(fromHash, maxBlocks));
  }

  this.chainGetBlock = function(previousGet, maxBlocks) {

    if (this.rawBlocks.length === maxBlocks) {
      return;
    }

    previousGet.then(this.onBlockResponse.bind(this));
  };

  this.onBlockResponse = function(response) {
    var rawBlock = response.data;
    this.rawBlocks.push(rawBlock);
    this.chainGetBlock(this.getBlock(rawBlock['prev_block']));
  }

  this.getBlock = function(hash) {
    return $http.get('/blockExplorer/rawblock/' + hash);
  }

}]);
<|endoftext|>


<commit_before>var config = require('config');
var type = require('helper').type;

var INDEX = '/';
var MAINTENANCE = '/maintenance';
var PAY_SUBSCRIPTION = '/account/pay-subscription';
var ENABLE = '/account/enable';
var LOGOUT = '/account/logout';
var DISABLED = '/account/disabled';

module.exports = function (req, res, next) {

  var user = req.user;

  function pathIs (path) {

    // We use indexOf instead of a simple comparison since
    // somethings we have a redirect query...
    if (type(path, 'string')) return req.originalUrl.indexOf(path) === 0;

    var paths = path;
    var match = false;

    paths.forEach(function(path){
      if (req.originalUrl.indexOf(path) === 0) match = true;
    });

    return match;
  }

  function pathIsNot (path) {
    return !pathIs(path);
  }

  // Don't expose any features which modify the database
  if (config.maintenance && pathIsNot(MAINTENANCE)) {
    return res.redirect(MAINTENANCE);
  }

  // Only serve the maintenance page if we are doing maintenance
  if (!config.maintenance && pathIs(MAINTENANCE)) {
    return res.redirect(INDEX);
  }

  // Only allow the user to pay
  if (user.needsToPay && pathIsNot(PAY_SUBSCRIPTION)) {
    return res.redirect(PAY_SUBSCRIPTION);
  }

  // Only let the user see these pages
  if (user.isDisabled && pathIsNot([ENABLE, LOGOUT, DISABLED])) {
    return res.redirect(DISABLED);
  }

  return next();
};<commit_msg>allow contact page in various modes<commit_after>var config = require('config');
var type = require('helper').type;

var INDEX = '/';
var CONTACT = '/contact';
var MAINTENANCE = '/maintenance';
var PAY_SUBSCRIPTION = '/account/pay-subscription';
var ENABLE = '/account/enable';
var LOGOUT = '/account/logout';
var DISABLED = '/account/disabled';

module.exports = function (req, res, next) {

  var user = req.user;

  function pathIs (path) {

    // We use indexOf instead of a simple comparison since
    // somethings we have a redirect query...
    if (type(path, 'string')) return req.originalUrl.indexOf(path) === 0;

    var paths = path;
    var match = false;

    paths.forEach(function(path){
      if (req.originalUrl.indexOf(path) === 0) match = true;
    });

    return match;
  }

  function pathIsNot (path) {
    return !pathIs(path);
  }

  // Don't expose any features which modify the database
  if (config.maintenance && pathIsNot([MAINTENANCE, CONTACT])) {
    return res.redirect(MAINTENANCE);
  }

  // Only serve the maintenance page if we are doing maintenance
  if (!config.maintenance && pathIs(MAINTENANCE)) {
    return res.redirect(INDEX);
  }

  // Only allow the user to pay
  if (user.needsToPay && pathIsNot([PAY_SUBSCRIPTION, CONTACT])) {
    return res.redirect(PAY_SUBSCRIPTION);
  }

  // Only let the user see these pages
  if (user.isDisabled && pathIsNot([ENABLE, LOGOUT, DISABLED, CONTACT])) {
    return res.redirect(DISABLED);
  }

  return next();
};<|endoftext|>


<commit_before>export default ({ colors, fonts }) => {
  const defaultStyles = {
    fontFamily: fonts.default,
    textDecoration: 'none',
    color: colors.text.link,
  };

  return {
    default: defaultStyles,
    inverted: {
      ...defaultStyles,
      color: colors.white,
    },
    underlined: {
      ...defaultStyles,
      textDecoration: 'underline',
    },
  };
};
<commit_msg>[WEB-819] Add active, hover, focus styles to Link<commit_after>export default ({ colors, fonts }) => {
  const defaultStyles = {
    fontFamily: fonts.default,
    textDecoration: 'none',
    color: colors.text.link,
    '&:hover, &:active': {
      color: colors.text.link,
      textDecoration: 'none',
    },
    '&:focus': {
      color: colors.text.link,
      outline: 'none',
      textDecoration: 'underline',
    },
  };

  return {
    default: defaultStyles,
    inverted: {
      ...defaultStyles,
      color: colors.white,
    },
    underlined: {
      ...defaultStyles,
      textDecoration: 'underline',
    },
  };
};
<|endoftext|>


<commit_before>import { DropShadowFilter } from 'pixi-filters';

// Components
import { PixiTrack } from './PixiTrack';
import Inset from './Inset';

// Services
import { chromInfo } from './services';

// Utils
import { absToChr, flatten, tileToCanvas } from './utils';
// import { workerSetPix } from './worker';

// const BASE_RES = 16;
// const BASE_SCALE = 4;
// const BASE_SCALE_UP = 4;

class Insets2dTrack extends PixiTrack {
  constructor(
    scene,
    animate,
    options,
  ) {
    super(scene, options);

    chromInfo.get(options.chromInfoPath).then((_chromInfo) => {
      this.dataToGenomicLoci = locus => absToChr(locus, _chromInfo);
    });

    this.options = options;

    this.dropShadow = new DropShadowFilter(
      90,
      this.options.dropDistance,
      this.options.dropBlur,
      0x000000,
      this.options.dropOpacity,
    );

    this.pBase.alpha = this.options.opacity;
    this.pMain.filters = [this.dropShadow];

    this.insets = {};

    this.animate = animate;

    this.insetMouseHandler = {
      click: this.clickHandler.bind(this),
      mouseOver: this.mouseOverHandler.bind(this),
      mouseOut: this.mouseOutHandler.bind(this),
      mouseDown: this.mouseDownHandler.bind(this),
      mouseUp: this.mouseUpHandler.bind(this)
    };
  }

  init() {
    this.pMain.clear();
  }

  initInset(
    uid,
    dataPos,
    options = this.options,
    mouseHandler = this.insetMouseHandler
  ) {
    this.insets[uid] = new Inset(uid, dataPos, options, mouseHandler);
    this.pMain.addChild(this.insets[uid].graphics());
    return this.insets[uid];
  }

  drawInset(uid, x, y, w, h, sx, sy, dX1, dX2, dY1, dY2) {
    const inset = (
      this.insets[uid] ||
      this.initInset(
        uid,
        [dX1, dX2, dY1, dY2],
        this.options,
        this.insetMouseHandler
      )
    );

    inset.clear(this.options);

    inset.offset(...this.position);
    inset.origin(sx, sy);
    inset.position(x, y);
    inset.size(w, h);
    inset.drawLeaderLine();
    inset.drawBorder();
    return inset.drawImage(this.renderImage, this.dataToGenomicLoci);
  }

  clickHandler(event, inset) {
    console.log('PIXI CLICK', event.type, inset);
  }

  mouseOverHandler(event, inset) {
    console.log('PIXI MOUSE OVER', event.type, inset);
    this.animate();
  }

  mouseOutHandler(event, inset) {
    console.log('PIXI MOUSE OUT', event.type, inset);
    this.animate();
  }

  mouseDownHandler(event, inset) {
    this.hoveringInsetIdx = this.pMain.getChildIndex(inset);
    this.pMain.setChildIndex(inset, this.pMain.children.length - 1);
    this.animate();
  }

  mouseUpHandler(event, inset) {
    this.pMain.setChildIndex(inset, this.hoveringInsetIdx);
    this.animate();
  }

  renderImage(data, w, h) {
    const flatImg = flatten(data.fragments[0]);

    const n = flatImg.length;

    const pixData = new Uint8ClampedArray(n * 4);

    for (let i = 0; i < n; i++) {
      const j = i * 4;
      const val = 255 - (flatImg[i] * 255);
      pixData[j] = val;
      pixData[j + 1] = val;
      pixData[j + 2] = val;
      pixData[j + 3] = 255;
    }

    return tileToCanvas(pixData, w, h);
  }
}

export default Insets2dTrack;
<commit_msg>Avoid too many logs<commit_after>import { DropShadowFilter } from 'pixi-filters';

// Components
import { PixiTrack } from './PixiTrack';
import Inset from './Inset';

// Services
import { chromInfo } from './services';

// Utils
import { absToChr, flatten, tileToCanvas } from './utils';
// import { workerSetPix } from './worker';

// const BASE_RES = 16;
// const BASE_SCALE = 4;
// const BASE_SCALE_UP = 4;

class Insets2dTrack extends PixiTrack {
  constructor(
    scene,
    animate,
    options,
  ) {
    super(scene, options);

    chromInfo.get(options.chromInfoPath).then((_chromInfo) => {
      this.dataToGenomicLoci = locus => absToChr(locus, _chromInfo);
    });

    this.options = options;

    this.dropShadow = new DropShadowFilter(
      90,
      this.options.dropDistance,
      this.options.dropBlur,
      0x000000,
      this.options.dropOpacity,
    );

    this.pBase.alpha = this.options.opacity;
    this.pMain.filters = [this.dropShadow];

    this.insets = {};

    this.animate = animate;

    this.insetMouseHandler = {
      click: this.clickHandler.bind(this),
      mouseOver: this.mouseOverHandler.bind(this),
      mouseOut: this.mouseOutHandler.bind(this),
      mouseDown: this.mouseDownHandler.bind(this),
      mouseUp: this.mouseUpHandler.bind(this)
    };
  }

  init() {
    this.pMain.clear();
  }

  initInset(
    uid,
    dataPos,
    options = this.options,
    mouseHandler = this.insetMouseHandler
  ) {
    this.insets[uid] = new Inset(uid, dataPos, options, mouseHandler);
    this.pMain.addChild(this.insets[uid].graphics());
    return this.insets[uid];
  }

  drawInset(uid, x, y, w, h, sx, sy, dX1, dX2, dY1, dY2) {
    const inset = (
      this.insets[uid] ||
      this.initInset(
        uid,
        [dX1, dX2, dY1, dY2],
        this.options,
        this.insetMouseHandler
      )
    );

    inset.clear(this.options);

    inset.offset(...this.position);
    inset.origin(sx, sy);
    inset.position(x, y);
    inset.size(w, h);
    inset.drawLeaderLine();
    inset.drawBorder();
    return inset.drawImage(this.renderImage, this.dataToGenomicLoci);
  }

  clickHandler(event, inset) {
    // console.log('PIXI CLICK', event.type, inset);
  }

  mouseOverHandler(event, inset) {
    // console.log('PIXI MOUSE OVER', event.type, inset);
    this.animate();
  }

  mouseOutHandler(event, inset) {
    // console.log('PIXI MOUSE OUT', event.type, inset);
    this.animate();
  }

  mouseDownHandler(event, inset) {
    this.hoveringInsetIdx = this.pMain.getChildIndex(inset);
    this.pMain.setChildIndex(inset, this.pMain.children.length - 1);
    this.animate();
  }

  mouseUpHandler(event, inset) {
    this.pMain.setChildIndex(inset, this.hoveringInsetIdx);
    this.animate();
  }

  renderImage(data, w, h) {
    const flatImg = flatten(data.fragments[0]);

    const n = flatImg.length;

    const pixData = new Uint8ClampedArray(n * 4);

    for (let i = 0; i < n; i++) {
      const j = i * 4;
      const val = 255 - (flatImg[i] * 255);
      pixData[j] = val;
      pixData[j + 1] = val;
      pixData[j + 2] = val;
      pixData[j + 3] = 255;
    }

    return tileToCanvas(pixData, w, h);
  }
}

export default Insets2dTrack;
<|endoftext|>


<commit_before>
var SignalsView = Backbone.View.extend({

  el: '.signals',

  events: {
    'click .link-signals' : 'moveLeft',
    'click .back' : 'moveRight'
  },

  initialize: function() {
    this.video = document.querySelector('#signals-video');
    this.isPlaying = false;

    var startlimit = $('.signals').offset().top - 200,
      endlimit = $('.signals').offset().top + $('.signals').height();

    $(window).on('scroll', _.bind(function() {

      if (this.checkYOffset() > startlimit  &&
        this.checkYOffset() < endlimit &&
        !this.isPlaying) {
        this.playVideo();
      }

      if (this.checkYOffset() < startlimit / 2 && this.isPlaying ||
        this.checkYOffset() > endlimit && this.isPlaying) {
        this.stopVideo();
      }
    }, this));

  },

  checkYOffset: function() {
    return window.pageYOffset;
  },

  playVideo: function() {
    $(this.video).removeClass('invisible');
    this.video.play();
    this.isPlaying = true;
  },

  stopVideo: function() {
    $(this.video).addClass('invisible');
    this.video.pause();
    this.video.currentTime = 0;

    this.isPlaying = false;
  },

  moveLeft: function(e) {
    e.preventDefault();

    this.$el.find('.slide-1').removeClass('move m-right');
    this.$el.find('.slide-2').removeClass('move m-right');

    this.$el.find('.slide-1').addClass('move m-left');
    this.$el.find('.slide-2').addClass('move m-left');
  },

  moveRight: function(e) {
    e.preventDefault();

    this.$el.find('.slide-1').removeClass('move m-left');
    this.$el.find('.slide-2').removeClass('move left');

    this.$el.find('.slide-1').addClass('move m-right');
    this.$el.find('.slide-2').addClass('move m-right');
  }

});

<commit_msg>refactor signals view<commit_after>
var SignalsView = Backbone.View.extend({

  el: '.signals',

  events: {
    'click .link-signals' : 'moveLeft',
    'click .back' : 'moveRight'
  },

  initialize: function() {
    this.initVars();
    this.setListeners();
  },

  initVars: function(){
    // Video
    this.video = document.querySelector('#signals-video');
    this.isPlaying = false;

    // Sliders
    this.$slides = [
      this.$el.find('.slide-1'),
      this.$el.find('.slide-2')
    ];

    // Offsets
    this.startlimit = $('.signals').offset().top - 200;
    this.endlimit = $('.signals').offset().top + $('.signals').height();
  },

  setListeners: function() {
    $(window).on('scroll', _.bind(function() {
      this.checkPlayVideo();
    }, this));
  },

  checkPlayVideo: function() {
    if (this.checkYOffset() > this.startlimit  &&
        this.checkYOffset() < this.endlimit &&
        !this.isPlaying) {
        this.playVideo();
      }

    if (this.checkYOffset() < this.startlimit / 2 && this.isPlaying ||
      this.checkYOffset() > this.endlimit && this.isPlaying) {
      this.stopVideo();
    }
  },

  checkYOffset: function() {
    return window.pageYOffset;
  },

  playVideo: function() {
    $(this.video).removeClass('invisible');
    this.video.play();
    this.isPlaying = true;
  },

  stopVideo: function() {
    $(this.video).addClass('invisible');
    this.video.pause();
    this.video.currentTime = 0;

    this.isPlaying = false;
  },

  moveLeft: function(e) {
    if (e) {
      e.preventDefault();
    }

    this.$slides.forEach(function(slide) {
      slide.removeClass('move m-right').addClass('move m-left');
    });
  },

  moveRight: function(e) {
    if (e) {
      e.preventDefault();
    }

    this.$slides.forEach(function(slide) {
      slide.removeClass('move m-left').addClass('move m-right');
    });
  }

});

<|endoftext|>


<commit_before>import Ember from 'ember';
import DS from 'ember-data';
import moment from 'moment';

const { computed, observer, on, RSVP, isEmpty, inject, get } = Ember;
const { PromiseArray } = DS;
const { service } = inject;

export default Ember.Service.extend({
  store: service(),
  session: service(),
  currentUserId: computed('session.data.authenticated.jwt', function(){
    const session = this.get('session');
    if(isEmpty(session)){
      return null;
    }

    const jwt = session.get('data.authenticated.jwt');

    if(isEmpty(jwt)){
      return null;
    }
    const js = atob(jwt.split('.')[1]);
    const obj = Ember.$.parseJSON(js);

    return get(obj, 'user_id');
  }),

  model: computed('currentUserId', function(){
    let deferred = Ember.RSVP.defer();
    let currentUserId = this.get('currentUserId');
    if (!currentUserId) {
      deferred.resolve(null);
    } else {
      this.get('store').find('user', currentUserId).then((user) => {
        deferred.resolve(user);
      });
    }

    return DS.PromiseObject.create({
      promise: deferred.promise
    });
  }),

  availableCohortsObserver: observer('availableCohorts.[]', function(){
    var self = this;
    this.get('availableCohorts').then(function(cohorts){
      if(!cohorts.contains(self.get('currentCohort'))){
        self.set('currentCohort', null);
      }
    });
  }),
  currentCohort: null,
  availableCohorts: computed('currentSchool', function(){
    var self = this;
    return new Ember.RSVP.Promise(function(resolve) {
      self.get('currentSchool').then(function(school){
        school.get('programs').then(function(programs){
          var promises = programs.map(function(program){
            return program.get('programYears');
          });
          Ember.RSVP.hash(promises).then(function(hash){
            var promises = [];
            Object.keys(hash).forEach(function(key) {
              hash[key].forEach(function(programYear){
                promises.push(programYear.get('cohort'));
              });
            });
            Ember.RSVP.hash(promises).then(function(hash){
              var cohorts = Ember.A();
              Object.keys(hash).forEach(function(key) {
                cohorts.pushObject(hash[key]);
              });
              resolve(cohorts);
            });
          });
        });
      });
    });
  }),

  /**
   * All cohorts from all schools that the current user is associated with,
   * via primary school association and the explicit schools permissions.
   * @property availableCohortsForAllSchools
   * @type Ember.computed
   * @readOnly
   * @public
   */
  cohortsInAllAssociatedSchools: computed('model.schools.[]', {
    get() {
      let defer = RSVP.defer();
      this.get('model').then(user => {
        user.get('schools').then(schools => {
          RSVP.all(schools.mapBy('programs')).then(programsArrays => {
            let programs = [];
            programsArrays.forEach(arr => {
              arr.forEach(program => {
                programs.push(program);
              });
            });
            RSVP.all(programs.mapBy('programYears')).then(programYearsArrays => {
              let programYears = [];
              programYearsArrays.forEach(arr => {
                arr.forEach(programYear => {
                  programYears.push(programYear);
                });
              });
              RSVP.all(programYears.mapBy('cohort')).then(cohorts => {
                defer.resolve(cohorts);
              });
            });
          });
        });
      });
      return PromiseArray.create({
        promise: defer.promise
      });
    }
  }).readOnly(),

  userRoleTitles: computed('model.roles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('model').then(user => {
        if(!user){
          resolve([]);
        } else {
          user.get('roles').then(roles => {
            let roleTitles = roles.map(role => role.get('title').toLowerCase());
            resolve(roleTitles);
          });
        }
      });
    });
  }),
  userIsCourseDirector: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('course director'));
      });
    });
  }),
  userIsFaculty: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('faculty'));
      });
    });
  }),
  userIsDeveloper: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('developer'));
      });
    });
  }),
  userIsStudent: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('student'));
      });
    });
  }),
  userIsPublic: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('public'));
      });
    });
  }),
  userIsFormerStudent: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('former student'));
      });
    });
  }),
  //not really used other than to trigger other properties
  privileges: computed(
    'userIsCourseDirector',
    'userIsFaculty',
    'userIsDeveloper',
    'userIsFormerStudent',
    'userIsPublic',
    'userIsStudent',
  function(){
    Ember.RSVP.all([
      this.get('userIsCourseDirector'),
      this.get('userIsFaculty'),
      this.get('userIsDeveloper'),
      this.get('userIsFormerStudent'),
      this.get('userIsPublic'),
      this.get('userIsStudent'),
    ]).then(arr => {
      return arr;
    });
  }),
  //will be customizable
  preferredDashboard: 'dashboard.week',
  //School
  canViewSchool: computed('model', function(){
    return false;
  }),
  canViewSchools: false,
  canEditSchools: false,
  schoolsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewSchools', hasRole.contains(true));
        this.set('canEditSchools', hasRole.contains(true));
      });
    }
  )),
  //Program
  canViewProgram: computed('model', function(){
    return false;
  }),
  canViewPrograms: false,
  canEditPrograms: false,
  programsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewPrograms', hasRole.contains(true));
        this.set('canEditPrograms', hasRole.contains(true));
      });
    }
  )),
  //Course
  canViewCourse: computed('model', function(){
    return false;
  }),
  canViewCourses: false,
  canEditCourses: false,
  canPrintUnpublishedCourse: false,
  coursesPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsFaculty'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewCourses', hasRole.contains(true));
        this.set('canEditCourses', hasRole.contains(true));
        this.set('canPrintUnpublishedCourse', hasRole.contains(true));
      });
    }
  )),
  canViewInstructorGroups: false,
  canEditInstructorGroups: false,
  instructorGroupsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewInstructorGroups', hasRole.contains(true));
        this.set('canEditInstructorGroups', hasRole.contains(true));
      });
    }
  )),
  //Instructor Group
  canViewInstructorGroup: computed('model', function(){
    return false;
  }),
  canViewLearnerGroups: false,
  canEditLearnerGroups: false,
  learnerGroupsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewLearnerGroups', hasRole.contains(true));
        this.set('canEditLearnerGroups', hasRole.contains(true));
      });
    }
  )),
  //Learner Group
  canViewLearnerGroup: computed('model', function(){
    return false;
  }),

  //Curriculum Inventory
  canViewCurriculumInventory: false,
  canViewCurriculumInventoryObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewCurriculumInventory', hasRole.contains(true));
      });
    }
  )),
  canEditCurriculumInventory: false,
  canEditCurriculumInventoryObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canEditCurriculumInventory', hasRole.contains(true));
      });
    }
  )),

  //Report
  canViewReport: computed('model', function(){
    return false;
  }),
  //Reports
  canViewReports: computed('model', function(){
    return false;
  }),
  canViewAdminDashboard: false,
  canViewAdminDashboardObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        this.set('canViewAdminDashboard', hasRole.contains(true));
      });
    }
  )),
  activeRelatedCoursesInThisYearAndLastYear: computed(
    'model',
    'model.instructedOfferings.[]',
    'model.instructorGroups.[]',
    'model.instructedLearnerGroups.[]',
    'model.directedCourses.[]',
    'model.instructorIlmSessions.[]',
    function(){
      let defer = RSVP.defer();
      this.get('model').then( user => {
        if(isEmpty(user)){
          defer.resolve([]);
          return;
        }
        let currentYear = moment().format('YYYY');
        const currentMonth = parseInt(moment().format('M'));
        if(currentMonth < 6){
          currentYear--;
        }
        const previousYear = currentYear -1;
        const nextYear = currentYear +1;
        this.get('store').query('course', {
          my: true,
          filters: {
            year: [previousYear, currentYear, nextYear],
            locked: false,
            archived: false
          }
        }).then(filteredCourses => {
          defer.resolve(filteredCourses);
        });
      });
      return PromiseArray.create({
        promise: defer.promise
      });
    }
  )
});
<commit_msg>Ensure currentUser still exists before setting properties<commit_after>import Ember from 'ember';
import DS from 'ember-data';
import moment from 'moment';

const { computed, observer, on, RSVP, isEmpty, inject, get } = Ember;
const { PromiseArray } = DS;
const { service } = inject;

export default Ember.Service.extend({
  store: service(),
  session: service(),
  currentUserId: computed('session.data.authenticated.jwt', function(){
    const session = this.get('session');
    if(isEmpty(session)){
      return null;
    }

    const jwt = session.get('data.authenticated.jwt');

    if(isEmpty(jwt)){
      return null;
    }
    const js = atob(jwt.split('.')[1]);
    const obj = Ember.$.parseJSON(js);

    return get(obj, 'user_id');
  }),

  model: computed('currentUserId', function(){
    let deferred = Ember.RSVP.defer();
    let currentUserId = this.get('currentUserId');
    if (!currentUserId) {
      deferred.resolve(null);
    } else {
      this.get('store').find('user', currentUserId).then((user) => {
        deferred.resolve(user);
      });
    }

    return DS.PromiseObject.create({
      promise: deferred.promise
    });
  }),

  availableCohortsObserver: observer('availableCohorts.[]', function(){
    var self = this;
    this.get('availableCohorts').then(function(cohorts){
      if(!cohorts.contains(self.get('currentCohort'))){
        self.set('currentCohort', null);
      }
    });
  }),
  currentCohort: null,
  availableCohorts: computed('currentSchool', function(){
    var self = this;
    return new Ember.RSVP.Promise(function(resolve) {
      self.get('currentSchool').then(function(school){
        school.get('programs').then(function(programs){
          var promises = programs.map(function(program){
            return program.get('programYears');
          });
          Ember.RSVP.hash(promises).then(function(hash){
            var promises = [];
            Object.keys(hash).forEach(function(key) {
              hash[key].forEach(function(programYear){
                promises.push(programYear.get('cohort'));
              });
            });
            Ember.RSVP.hash(promises).then(function(hash){
              var cohorts = Ember.A();
              Object.keys(hash).forEach(function(key) {
                cohorts.pushObject(hash[key]);
              });
              resolve(cohorts);
            });
          });
        });
      });
    });
  }),

  /**
   * All cohorts from all schools that the current user is associated with,
   * via primary school association and the explicit schools permissions.
   * @property availableCohortsForAllSchools
   * @type Ember.computed
   * @readOnly
   * @public
   */
  cohortsInAllAssociatedSchools: computed('model.schools.[]', {
    get() {
      let defer = RSVP.defer();
      this.get('model').then(user => {
        user.get('schools').then(schools => {
          RSVP.all(schools.mapBy('programs')).then(programsArrays => {
            let programs = [];
            programsArrays.forEach(arr => {
              arr.forEach(program => {
                programs.push(program);
              });
            });
            RSVP.all(programs.mapBy('programYears')).then(programYearsArrays => {
              let programYears = [];
              programYearsArrays.forEach(arr => {
                arr.forEach(programYear => {
                  programYears.push(programYear);
                });
              });
              RSVP.all(programYears.mapBy('cohort')).then(cohorts => {
                defer.resolve(cohorts);
              });
            });
          });
        });
      });
      return PromiseArray.create({
        promise: defer.promise
      });
    }
  }).readOnly(),

  userRoleTitles: computed('model.roles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('model').then(user => {
        if(!user){
          resolve([]);
        } else {
          user.get('roles').then(roles => {
            let roleTitles = roles.map(role => role.get('title').toLowerCase());
            resolve(roleTitles);
          });
        }
      });
    });
  }),
  userIsCourseDirector: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('course director'));
      });
    });
  }),
  userIsFaculty: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('faculty'));
      });
    });
  }),
  userIsDeveloper: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('developer'));
      });
    });
  }),
  userIsStudent: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('student'));
      });
    });
  }),
  userIsPublic: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('public'));
      });
    });
  }),
  userIsFormerStudent: computed('useRoleTitles.[]', function(){
    return new Ember.RSVP.Promise((resolve) => {
      this.get('userRoleTitles').then(roleTitles => {
        resolve(roleTitles.contains('former student'));
      });
    });
  }),
  //not really used other than to trigger other properties
  privileges: computed(
    'userIsCourseDirector',
    'userIsFaculty',
    'userIsDeveloper',
    'userIsFormerStudent',
    'userIsPublic',
    'userIsStudent',
  function(){
    Ember.RSVP.all([
      this.get('userIsCourseDirector'),
      this.get('userIsFaculty'),
      this.get('userIsDeveloper'),
      this.get('userIsFormerStudent'),
      this.get('userIsPublic'),
      this.get('userIsStudent'),
    ]).then(arr => {
      return arr;
    });
  }),
  //will be customizable
  preferredDashboard: 'dashboard.week',
  //School
  canViewSchool: computed('model', function(){
    return false;
  }),
  canViewSchools: false,
  canEditSchools: false,
  schoolsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewSchools', hasRole.contains(true));
          this.set('canEditSchools', hasRole.contains(true));
        }
      });
    }
  )),
  //Program
  canViewProgram: computed('model', function(){
    return false;
  }),
  canViewPrograms: false,
  canEditPrograms: false,
  programsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewPrograms', hasRole.contains(true));
          this.set('canEditPrograms', hasRole.contains(true));
        }
      });
    }
  )),
  //Course
  canViewCourse: computed('model', function(){
    return false;
  }),
  canViewCourses: false,
  canEditCourses: false,
  canPrintUnpublishedCourse: false,
  coursesPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsFaculty'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewCourses', hasRole.contains(true));
          this.set('canEditCourses', hasRole.contains(true));
          this.set('canPrintUnpublishedCourse', hasRole.contains(true));
        }
      });
    }
  )),
  canViewInstructorGroups: false,
  canEditInstructorGroups: false,
  instructorGroupsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewInstructorGroups', hasRole.contains(true));
          this.set('canEditInstructorGroups', hasRole.contains(true));
        }
      });
    }
  )),
  //Instructor Group
  canViewInstructorGroup: computed('model', function(){
    return false;
  }),
  canViewLearnerGroups: false,
  canEditLearnerGroups: false,
  learnerGroupsPrivilegesObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsCourseDirector'),
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewLearnerGroups', hasRole.contains(true));
          this.set('canEditLearnerGroups', hasRole.contains(true));
        }
      });
    }
  )),
  //Learner Group
  canViewLearnerGroup: computed('model', function(){
    return false;
  }),

  //Curriculum Inventory
  canViewCurriculumInventory: false,
  canViewCurriculumInventoryObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewCurriculumInventory', hasRole.contains(true));
        }
      });
    }
  )),
  canEditCurriculumInventory: false,
  canEditCurriculumInventoryObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canEditCurriculumInventory', hasRole.contains(true));
        }
      });
    }
  )),

  //Report
  canViewReport: computed('model', function(){
    return false;
  }),
  //Reports
  canViewReports: computed('model', function(){
    return false;
  }),
  canViewAdminDashboard: false,
  canViewAdminDashboardObserver: on('init', observer('privileges',
    function(){
      Ember.RSVP.all([
        this.get('userIsDeveloper')
      ]).then(hasRole => {
        if ( !(this.get('isDestroyed') || this.get('isDestroying')) ) {
          this.set('canViewAdminDashboard', hasRole.contains(true));
        }
      });
    }
  )),
  activeRelatedCoursesInThisYearAndLastYear: computed(
    'model',
    'model.instructedOfferings.[]',
    'model.instructorGroups.[]',
    'model.instructedLearnerGroups.[]',
    'model.directedCourses.[]',
    'model.instructorIlmSessions.[]',
    function(){
      let defer = RSVP.defer();
      this.get('model').then( user => {
        if(isEmpty(user)){
          defer.resolve([]);
          return;
        }
        let currentYear = moment().format('YYYY');
        const currentMonth = parseInt(moment().format('M'));
        if(currentMonth < 6){
          currentYear--;
        }
        const previousYear = currentYear -1;
        const nextYear = currentYear +1;
        this.get('store').query('course', {
          my: true,
          filters: {
            year: [previousYear, currentYear, nextYear],
            locked: false,
            archived: false
          }
        }).then(filteredCourses => {
          defer.resolve(filteredCourses);
        });
      });
      return PromiseArray.create({
        promise: defer.promise
      });
    }
  )
});
<|endoftext|>


<commit_before>/**
 * @ngdoc    service
 * @name     alquimia.alquimia:WPApi
 * @requires restangular
 * @author    Mauro Constantinescu <mauro.constantinescu@gmail.com>
 * @copyright © 2015 White, Red & Green Digital S.r.l.
 *
 * @description
 * Allows to easily communicate with the Wordpress' **WP REST API** plugin and specifically with the
 * Alquimia Wordpress plugin for the API.
 */
module.exports = function WPApiProvider() {
  var $q, Restangular, defaultFilters;

  this.$get = ['$q', 'RestFullResponse', function WPApiFactory( _$q, RestFullResponse ) {
    $q = _$q;
    Restangular = RestFullResponse;

    return WPApi;
  }];

  /**
   * @ngdoc    method
   * @name     WPApi
   * @methodOf alquimia.alquimia:WPApi
   *
   * @param    {String} endpoint
   * The API endpoint name. This is defined by the API URL entry point fragment. For example, for the default "posts"
   * endpoint, handled by `WP_JSON_Posts`, the URL is `http://your.domain.com/wordpress/wp-json/posts` and the
   * `endpoint` parameter is "posts"
   *
   * @param    {Object} config
   * A configuration object. It can contain two keys that defines the WPApi instance behaviour:
   * - `filters`: it can contain up to three objects, with keys `defaults`, `items` and `item`. The properties of these
   *   three object can be atomic values or functions and are used to send GET parameters out to the API. For example:
   *
   *   ```
   *   new WPApi( 'posts', {
   *     filters: {
   *       defaults: {
   *         lang: function() { return getCurrentLanguage(); }
   *       },
   *       items: {
   *         posts_per_page: -1
   *       },
   *       item: {}
   *     }
   *   } );
   *   ```
   *
   *   Means: send a `filter[lang]` parameter for every request, executing the `getCurrentLanguage` function and
   *   sending the returned value. When requesting all items (sending a request to `posts`), disable the pagination
   *   through a `filter[posts_per_page]=-1` parameter.
   *
   *   You can add filter when requesting a single item (`posts/post-slug`) too, putting something into the `item` key.
   * - `transform`: a function used to transform each item returned from the API before they are cached and returned.
   *   It is called with the item and the response headers as the arguments:
   *
   *   Example:
   *
   *   ```
   *   new WPApi( 'posts', {
   *     transform: function( item, headers ) {
   *       item.totalPages = headers['x-wp-totalpages'];
   *       item.totalPosts = headers['x-wp-total'];
   *       return item;
   *     }
   *   } );
   *   ```
   *
   *   Remember to return the item!
   *
   * @returns  {Object}
   * A `WPApi` instance mapped to the provided `endpoint` and configured to send `filters` and `transform`
   * the responses.
   *
   * @description
   * `WPApi` constructor.
   */
  function WPApi( endpoint, config ) {
    var backend = Restangular.all( endpoint );
    var items = {}, cache = {};
    var valid = true;
    var defaultFilters = {}, itemsFilters = {}, itemFilters = {};
    var transform;

    if ( config && angular.isObject( config ) ){
      if ( config.filters && angular.isObject( config.filters ) ) {
        defaultFilters = config.filters.defaults || defaultFilters;
        itemsFilters = config.filters.items || itemsFilters;
        itemFilters = config.filters.item || itemFilters;
      }

      if ( config.transform && angular.isFunction( config.transform ) ) {
        transform = config.transform;
      }
    }

    /**
     * @ngdoc    method
     * @name     getItems
     * @methodOf alquimia.alquimia:WPApi
     *
     * @param {Boolean} flush
     * If `false` and this method was called before, the network request is skipped and the items are taken from
     * cache. If `true`, the network request is sent even if cached items are available.
     *
     * @param {Object} filters
     * An optional set of additional filters to be sent along with the API request. Default filters
     * from `config.filters.defaults` are merged with and overridden by items filters from `config.filters.items`,
     * that are merged with and overridden by these filters.
     *
     * @param {Object} params
     * An optional set of additional parameters to be sent along the API request.
     * Properties of this object are passed outside the `filter[]` array.
     *
     * @returns {Promise}
     * A Javascript Promise. The `resolve` function is called with a `Restangular` response as the only argument.
     * The `reject` function is called with a `WP_Error` from WP REST API`, that contains a `code` and a `message`
     * keys.
     *
     * @description
     * Sends a request for getting all the items from the WP REST API endpoint.
     */
    this.getItems = function( flush, filters, params ) {
      params = params || {};

      return $q( function( resolve, reject ) {
        filters = angular.extend( {}, defaultFilters, itemsFilters, filters );
        filters = parseFilters( filters );
        filters = angular.extend( filters, params );

        var cacheKey = getCacheKey( filters );

        /* Items from cache */
        if ( valid && ! flush && cache[cacheKey] ) {
          var res = [];

          for ( var i = cache[cacheKey].length - 1; i >= 0; i-- ) {
            res.unshift( items[cache[cacheKey][i]] );
          }

          resolve( res );
          return;
        }

        /* Items from the API */
        backend.getList( filters ).then( function( response ) {
          var headers = response.headers();
          var res = [];
          cache[cacheKey] = [];

          for ( var i = response.data.length - 1; i >= 0; i-- ) {
            var item = response.data[i];
            var slug = item.slug;

            item.route = item.route + '/' + item.ID;

            if ( transform ) item = transform( item, headers );

            items[slug] = item;
            cache[cacheKey].unshift( slug );
            res.unshift( item );
          }

          valid = true;
          resolve( res, true );
        }, function( error ) {
          return error.data[0];
        } );
      } );
    };

    /**
     * @ngdoc    method
     * @name     getItem
     * @methodOf alquimia.alquimia:WPApi
     *
     * @param {String} slug
     * The post slug to be requested. Usually, it is taken directly from the
     * {@link alquimia.alquimia:WPApi#methods_getItems getItems} response.
     *
     * @param {Boolean} flush
     * If `false` and this method was called before, the network request is skipped and the items are taken from
     * cache. If `true`, the network request is sent even if cached items are available.
     *
     * @param {Object} filters
     * An optional set of additional filters to be sent along with the API request. Default filters
     * from `config.filters.defaults` are merged with and overridden by items filters from `config.filters.item`,
     * that are merged with and overridden by these filters.
     *
     * @param {Object} params
     * An optional set of additional parameters to be sent along the API request.
     * Properties of this object are passed outside the `filter[]` array.
     *
     * @returns {Promise}
     * A Javascript Promise. The `resolve` function is called with a `Restangular` response as the only argument.
     * The `reject` function is called with a `WP_Error` from WP REST API`, that contains a `code` and a `message`
     * keys.
     *
     * @description
     * Sends a request for getting one item from the WP REST API endpoint.
     */
    this.getItem = function( slug, flush, filters, params ) {
      return $q( function( resolve, reject ) {
        if ( ! flush && items[slug] ) {
          /* Item from cache */
          resolve( items[slug] );
          return;
        }

        /* Item from API */
        params = params || {};
        filters = angular.extend( {}, defaultFilters, itemFilters, filters );
        filters = parseFilters( filters );
        filters = angular.extend( filters, params );

        backend.one( slug ).get( filters ).then( function( response ) {
          var item = response.data;
          var headers = response.headers();

          if ( transform ) {
            item = transform( item, headers );
          }

          items[slug] = item;

          resolve( item );
        }, function( error ) {
          return error.data[0];
        } );
      } );
    };

    /**
     * @ngdoc    method
     * @name     invalidate
     * @methodOf alquimia.alquimia:WPApi
     *
     * @description
     * TODO: now that caching depends on filters, this method may be useless
     * Schedule the cache to be discarded on the next request. This is useful when you know that a request is
     * about to be sent, but the object that is going to send it doesn't know that the cache should be discarded.
     *
     * Let's say that your API can handle traslations and react to language inconsistencies, so if you request a
     * post that is in French, but your request has a GET parameter that says "German", it will return the German
     * post. You have a dropdown menu on your post page that lets a user pick a language.
     *
     * Now, let's say that a user lands on the French category page that shows all the posts within that category,
     * following a link a friend gave him, but he/she only speaks German. He/She changes language, and your
     * application does this:
     *
     * - Intentionally sends a request that asks for the French category with German language;
     * - picks the slug of the German translation;
     * - invalidates the WPApi cache;
     * - changes `$location` so the URL is consistent with the right category translation;
     * - asks for posts again.
     *
     * In this case, not invalidating the cache would have caused WPApi to serve the cached posts, because it
     * doesn't know that something changed. The post request couln't be done with `flush: true`, unless you
     * saved the last language before doing the request. This is why this method is useful.
     */
    this.invalidate = function() {
      valid = false;
    };

    function parseFilters( filters ) {
      var ret = {};

      for ( var i in filters ) {
        if ( angular.isFunction( filters[i] ) ) {
          ret['filter[' + i + ']'] = filters[i]();
        } else {
          ret['filter[' + i + ']'] = filters[i];
        }
      }

      return ret;
    }

    function getCacheKey( filters ) {
      var a = [];

      for ( var i in filters ) {
        a.push( i + filters[i] );
      }

      a.sort();
      return 'qf_' + a.join( '' );
    }
  }
};
<commit_msg>Fixed WPApi provider's error handling<commit_after>/**
 * @ngdoc    service
 * @name     alquimia.alquimia:WPApi
 * @requires restangular
 * @author    Mauro Constantinescu <mauro.constantinescu@gmail.com>
 * @copyright © 2015 White, Red & Green Digital S.r.l.
 *
 * @description
 * Allows to easily communicate with the Wordpress' **WP REST API** plugin and specifically with the
 * Alquimia Wordpress plugin for the API.
 */
module.exports = function WPApiProvider() {
  var $q, Restangular, defaultFilters;

  this.$get = ['$q', 'RestFullResponse', function WPApiFactory( _$q, RestFullResponse ) {
    $q = _$q;
    Restangular = RestFullResponse;

    return WPApi;
  }];

  /**
   * @ngdoc    method
   * @name     WPApi
   * @methodOf alquimia.alquimia:WPApi
   *
   * @param    {String} endpoint
   * The API endpoint name. This is defined by the API URL entry point fragment. For example, for the default "posts"
   * endpoint, handled by `WP_JSON_Posts`, the URL is `http://your.domain.com/wordpress/wp-json/posts` and the
   * `endpoint` parameter is "posts"
   *
   * @param    {Object} config
   * A configuration object. It can contain two keys that defines the WPApi instance behaviour:
   * - `filters`: it can contain up to three objects, with keys `defaults`, `items` and `item`. The properties of these
   *   three object can be atomic values or functions and are used to send GET parameters out to the API. For example:
   *
   *   ```
   *   new WPApi( 'posts', {
   *     filters: {
   *       defaults: {
   *         lang: function() { return getCurrentLanguage(); }
   *       },
   *       items: {
   *         posts_per_page: -1
   *       },
   *       item: {}
   *     }
   *   } );
   *   ```
   *
   *   Means: send a `filter[lang]` parameter for every request, executing the `getCurrentLanguage` function and
   *   sending the returned value. When requesting all items (sending a request to `posts`), disable the pagination
   *   through a `filter[posts_per_page]=-1` parameter.
   *
   *   You can add filter when requesting a single item (`posts/post-slug`) too, putting something into the `item` key.
   * - `transform`: a function used to transform each item returned from the API before they are cached and returned.
   *   It is called with the item and the response headers as the arguments:
   *
   *   Example:
   *
   *   ```
   *   new WPApi( 'posts', {
   *     transform: function( item, headers ) {
   *       item.totalPages = headers['x-wp-totalpages'];
   *       item.totalPosts = headers['x-wp-total'];
   *       return item;
   *     }
   *   } );
   *   ```
   *
   *   Remember to return the item!
   *
   * @returns  {Object}
   * A `WPApi` instance mapped to the provided `endpoint` and configured to send `filters` and `transform`
   * the responses.
   *
   * @description
   * `WPApi` constructor.
   */
  function WPApi( endpoint, config ) {
    var backend = Restangular.all( endpoint );
    var items = {}, cache = {};
    var valid = true;
    var defaultFilters = {}, itemsFilters = {}, itemFilters = {};
    var transform;

    if ( config && angular.isObject( config ) ){
      if ( config.filters && angular.isObject( config.filters ) ) {
        defaultFilters = config.filters.defaults || defaultFilters;
        itemsFilters = config.filters.items || itemsFilters;
        itemFilters = config.filters.item || itemFilters;
      }

      if ( config.transform && angular.isFunction( config.transform ) ) {
        transform = config.transform;
      }
    }

    /**
     * @ngdoc    method
     * @name     getItems
     * @methodOf alquimia.alquimia:WPApi
     *
     * @param {Boolean} flush
     * If `false` and this method was called before, the network request is skipped and the items are taken from
     * cache. If `true`, the network request is sent even if cached items are available.
     *
     * @param {Object} filters
     * An optional set of additional filters to be sent along with the API request. Default filters
     * from `config.filters.defaults` are merged with and overridden by items filters from `config.filters.items`,
     * that are merged with and overridden by these filters.
     *
     * @param {Object} params
     * An optional set of additional parameters to be sent along the API request.
     * Properties of this object are passed outside the `filter[]` array.
     *
     * @returns {Promise}
     * A Javascript Promise. The `resolve` function is called with a `Restangular` response as the only argument.
     * The `reject` function is called with a `WP_Error` from WP REST API`, that contains a `code` and a `message`
     * keys.
     *
     * @description
     * Sends a request for getting all the items from the WP REST API endpoint.
     */
    this.getItems = function( flush, filters, params ) {
      params = params || {};

      return $q( function( resolve, reject ) {
        filters = angular.extend( {}, defaultFilters, itemsFilters, filters );
        filters = parseFilters( filters );
        filters = angular.extend( filters, params );

        var cacheKey = getCacheKey( filters );

        /* Items from cache */
        if ( valid && ! flush && cache[cacheKey] ) {
          var res = [];

          for ( var i = cache[cacheKey].length - 1; i >= 0; i-- ) {
            res.unshift( items[cache[cacheKey][i]] );
          }

          resolve( res );
          return;
        }

        /* Items from the API */
        backend.getList( filters ).then( function( response ) {
          var headers = response.headers();
          var res = [];
          cache[cacheKey] = [];

          for ( var i = response.data.length - 1; i >= 0; i-- ) {
            var item = response.data[i];
            var slug = item.slug;

            item.route = item.route + '/' + item.ID;

            if ( transform ) item = transform( item, headers );

            items[slug] = item;
            cache[cacheKey].unshift( slug );
            res.unshift( item );
          }

          valid = true;
          resolve( res, true );
        }, function() {
          reject.apply( this, arguments );
          return;
        } );
      } );
    };

    /**
     * @ngdoc    method
     * @name     getItem
     * @methodOf alquimia.alquimia:WPApi
     *
     * @param {String} slug
     * The post slug to be requested. Usually, it is taken directly from the
     * {@link alquimia.alquimia:WPApi#methods_getItems getItems} response.
     *
     * @param {Boolean} flush
     * If `false` and this method was called before, the network request is skipped and the items are taken from
     * cache. If `true`, the network request is sent even if cached items are available.
     *
     * @param {Object} filters
     * An optional set of additional filters to be sent along with the API request. Default filters
     * from `config.filters.defaults` are merged with and overridden by items filters from `config.filters.item`,
     * that are merged with and overridden by these filters.
     *
     * @param {Object} params
     * An optional set of additional parameters to be sent along the API request.
     * Properties of this object are passed outside the `filter[]` array.
     *
     * @returns {Promise}
     * A Javascript Promise. The `resolve` function is called with a `Restangular` response as the only argument.
     * The `reject` function is called with a `WP_Error` from WP REST API`, that contains a `code` and a `message`
     * keys.
     *
     * @description
     * Sends a request for getting one item from the WP REST API endpoint.
     */
    this.getItem = function( slug, flush, filters, params ) {
      return $q( function( resolve, reject ) {
        if ( ! flush && items[slug] ) {
          /* Item from cache */
          resolve( items[slug] );
          return;
        }

        /* Item from API */
        params = params || {};
        filters = angular.extend( {}, defaultFilters, itemFilters, filters );
        filters = parseFilters( filters );
        filters = angular.extend( filters, params );

        backend.one( slug ).get( filters ).then( function( response ) {
          var item = response.data;
          var headers = response.headers();

          if ( transform ) {
            item = transform( item, headers );
          }

          items[slug] = item;

          resolve( item );
        }, function( error ) {
          reject.apply( this, arguments );
          return;
        } );
      } );
    };

    /**
     * @ngdoc    method
     * @name     invalidate
     * @methodOf alquimia.alquimia:WPApi
     *
     * @description
     * TODO: now that caching depends on filters, this method may be useless
     * Schedule the cache to be discarded on the next request. This is useful when you know that a request is
     * about to be sent, but the object that is going to send it doesn't know that the cache should be discarded.
     *
     * Let's say that your API can handle traslations and react to language inconsistencies, so if you request a
     * post that is in French, but your request has a GET parameter that says "German", it will return the German
     * post. You have a dropdown menu on your post page that lets a user pick a language.
     *
     * Now, let's say that a user lands on the French category page that shows all the posts within that category,
     * following a link a friend gave him, but he/she only speaks German. He/She changes language, and your
     * application does this:
     *
     * - Intentionally sends a request that asks for the French category with German language;
     * - picks the slug of the German translation;
     * - invalidates the WPApi cache;
     * - changes `$location` so the URL is consistent with the right category translation;
     * - asks for posts again.
     *
     * In this case, not invalidating the cache would have caused WPApi to serve the cached posts, because it
     * doesn't know that something changed. The post request couln't be done with `flush: true`, unless you
     * saved the last language before doing the request. This is why this method is useful.
     */
    this.invalidate = function() {
      valid = false;
    };

    function parseFilters( filters ) {
      var ret = {};

      for ( var i in filters ) {
        if ( angular.isFunction( filters[i] ) ) {
          ret['filter[' + i + ']'] = filters[i]();
        } else {
          ret['filter[' + i + ']'] = filters[i];
        }
      }

      return ret;
    }

    function getCacheKey( filters ) {
      var a = [];

      for ( var i in filters ) {
        a.push( i + filters[i] );
      }

      a.sort();
      return 'qf_' + a.join( '' );
    }
  }
};
<|endoftext|>