<commit_before>/*!
 * OS.js - JavaScript Cloud/Web Desktop Platform
 *
 * Copyright (c) 2011-2017, Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */
/*eslint strict:["error", "global"]*/
'use strict';

const _path = require('path');
const _fs = require('fs-extra');
const _glob = require('glob');

const _config = require('./config.js');
const _manifest = require('./manifest.js');
const _themes = require('./themes.js');
const _packages = require('./packages.js');
const _core = require('./core.js');
const _dist = require('./dist.js');
const _generate = require('./generate.js');
const _utils = require('./utils.js');
const _watcher = require('./watcher.js');
const _logger = _utils.logger;

const ROOT = _path.dirname(_path.dirname(_path.join(__dirname)));

///////////////////////////////////////////////////////////////////////////////
// HELPERS
///////////////////////////////////////////////////////////////////////////////

/*
 * Iterates all given tasks
 */
function _eachTask(cli, args, taskName, namespace) {
  if ( !args ) {
    return Promise.reject('Not enough arguments');
  }

  return new Promise((resolve, reject) => {
    _config.getConfiguration().then((cfg) => {
      _utils.eachp(args.replace(/\s/, '').split(',').map((iter) => {
        return function() {
          iter = (iter || '').replace('-', '_');
          if ( typeof namespace === 'function' ) {
            return namespace(cli, cfg, iter);
          } else {
            if ( namespace[iter] ) {
              let msg = _logger.color([taskName, iter].join(':'), 'green');
              if ( cli.option('debug') ) {
                msg += ' (' + _logger.color('debug mode', 'blue') + ')';
              }

              _logger.log(_logger.color('Running task:', 'bold'), msg);

              return namespace[iter](cli, cfg);
            }
          }

          return Promise.reject('Invalid task: ' + iter);
        };
      })).then(resolve).catch(reject);
    }).catch(reject);
  });
}

///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////

const TASKS = {
  build: {
    config: function(cli, cfg) {
      return _config.writeConfiguration(cli, cfg);
    },

    core: function(cli, cfg) {
      return _core.buildFiles(cli, cfg);
    },

    dist: function(cli, cfg) {
      return _dist.buildFiles(cli, cfg);
    },

    theme: function(cli, cfg) {
      const targets = [
        [cli.option('style'), _themes.buildStyle],
        [cli.option('icons'), _themes.buildIcon],
        [cli.option('static'), _themes.buildStatic],
        [cli.option('fonts'), _themes.buildFonts],
        [cli.option('sounds'), _themes.buildSounds]
      ];

      const list = targets.filter((iter) => {
        return iter && iter[0];
      }).map((iter) => {
        return iter[1](cli, cfg, iter[0]);
      });

      return Promise.all(list);
    },

    themes: function(cli, cfg) {
      return _themes.buildAll(cli, cfg);
    },

    manifest: function(cli, cfg) {
      return _manifest.writeManifest(cli, cfg);
    },

    package: function(cli, cfg) {
      const name = cli.option('name');
      if ( !name || name.indexOf('/') === -1 ) {
        throw new Error('Invalid package name');
      }

      return _packages.buildPackage(cli, cfg, name);
    },

    packages: function(cli, cfg) {
      return _packages.buildPackages(cli, cfg);
    }
  },

  config: {
    set: function(cli, cfg) {
      return _config.set(cfg, cli.option('name'), cli.option('value'), cli.option('import'), cli.option('out'));
    },
    get: function(cli, cfg) {
      return _config.get(cfg, cli.option('name'));
    },
    add_mount: function(cli, cfg) {
      return _config.addMount(cfg, cli.option('name'), cli.option('description') || cli.option('desc'), cli.option('path'), cli.option('transport'), cli.option('ro'));
    },
    add_preload: function(cli, cfg) {
      return _config.addPreload(cfg, cli.option('name'), cli.option('path'), cli.option('type'));
    },
    add_repository: function(cli, cfg) {
      return _config.addRepository(cfg, cli.option('name'));
    },
    add_script: function(cli, cfg) {
      return _config.addOverlayFile(cfg, 'javascript', cli.option('path'), cli.option('overlay', 'custom'));
    },
    add_style: function(cli, cfg) {
      return _config.addOverlayFile(cfg, 'stylesheets', cli.option('path'), cli.option('overlay', 'custom'));
    },
    add_locale: function(cli, cfg) {
      return _config.addOverlayFile(cfg, 'locales', cli.option('path'), cli.option('overlay', 'custom'));
    },
    remove_repository: function(cli, cfg) {
      return _config.removeRepository(cfg, cli.option('name'));
    },
    enable_package: function(cli, cfg) {
      return _config.enablePackage(cfg, cli.option('name'));
    },
    disable_package: function(cli, cfg) {
      return _config.disablePackage(cfg, cli.option('name'));
    },
    list_packages: function(cli, cfg) {
      return _config.listPackages(cfg);
    }
  },

  generate: function(cli, cfg, task) {
    if ( _generate[task] ) {
      return new Promise((resolve, reject) => {
        _generate[task](cli, cfg).then((arg) => {

          const out = cli.option('out');
          if ( out ) {
            _fs.writeFileSync(out, String(arg));
            resolve();
            return;
          } else if ( arg ) {
            _logger.log(arg);
          }

          resolve();
        }).catch(reject);
      });
    } else {
      return Promise.reject('Invalid generator: ' + task);
    }
  }
};

const ORIGINAL_TASKS = (function() {
  const tmp = {};
  Object.keys(TASKS).forEach(function(s) {
    tmp[s] = Object.keys(TASKS[s]);
  });
  return tmp;
})();

///////////////////////////////////////////////////////////////////////////////
// EXPORTS
///////////////////////////////////////////////////////////////////////////////

/*
 * Init
 */
module.exports._init = function() {
  _glob.sync(_path.join(__dirname, 'modules/*.js')).forEach((file) => {
    require(file).register(TASKS);
  });
};

/*
 * Task: `build`
 */
module.exports.build = function(cli, args) {
  if ( !args ) {
    args = ['config', 'dist', 'core', 'themes', 'manifest', 'packages'];

    args = args.concat(Object.keys(TASKS.build).filter(function(i) {
      return ORIGINAL_TASKS.build.indexOf(i) === -1 && args.indexOf(i) === -1;
    })).join(',');
  }

  return _eachTask(cli, args, 'build', TASKS.build);
};

/*
 * Task: `config`
 */
module.exports.config = function(cli, arg) {
  return new Promise((resolve, reject) => {
    arg = (arg || '').replace('-', '_');

    if ( TASKS.config[arg] ) {
      _config.getConfiguration().then((cfg) => {
        TASKS.config[arg](cli, cfg).then((arg) => {
          if ( typeof arg !== 'undefined' ) {
            _logger.log(arg);
          }
          resolve();
        }).catch(reject);
      });
    } else {
      reject('Invalid action: ' + arg);
    }
  });
};

/*
 * Task: `watch`
 */
module.exports.watch = function(cli, args) {
  return _watcher.watch(cli);
};

/*
 * Task: `generate`
 */
module.exports.generate = function(cli, args) {
  return _eachTask(cli, args, 'generate', TASKS.generate);
};

/*
 * Task: `clean`
 */
module.exports.clean = function(cli, args) {
  return new Promise((resolve, reject) => {
    _logger.log(_logger.color('Running task:', 'bold'), _logger.color('clean', 'green'));

    _config.getConfiguration().then((cfg) => {
      Promise.all([
        _config.clean(cli, cfg),
        _dist.clean(cli, cfg),
        _core.clean(cli, cfg),
        _manifest.clean(cli, cfg),
        _themes.clean(cli, cfg),
        _packages.clean(cli, cfg)
      ]).then(resolve).catch(reject);
    });
  });
};

/*
 * Task: `run`
 */
module.exports.run = function(cli, args) {
  const instance = require(_path.join(ROOT, 'src/server/node/core/instance.js'));
  const settings = require(_path.join(ROOT, 'src/server/node/core/settings.js'));

  const opts = {
    HOSTNAME: cli.option('hostname'),
    DEBUG: cli.option('debug'),
    PORT: cli.option('port'),
    LOGLEVEL: cli.option('loglevel')
  };

  instance.init(opts).then((env) => {
    const config = settings.get();
    if ( config.tz ) {
      process.env.TZ = config.tz;
    }

    process.on('exit', () => {
      instance.destroy();
    });

    instance.run();

    process.on('uncaughtException', (error) => {
      _logger.log('UNCAUGHT EXCEPTION', error, error.stack);
    });

    ['SIGTERM', 'SIGINT'].forEach((sig) => {
      process.on(sig, () => {
        console.log('\n');
        instance.destroy((err) => {
          process.exit(err ? 1 : 0);
        });
      });
    });
  }).catch((error) => {
    _logger.log(error);
    process.exit(1);
  });

  return new Promise(() => {
    // This is one promise we can't keep! :(
  });
};
<commit_msg>build: Print unhandled rejections in 'run'<commit_after>/*!
 * OS.js - JavaScript Cloud/Web Desktop Platform
 *
 * Copyright (c) 2011-2017, Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */
/*eslint strict:["error", "global"]*/
'use strict';

const _path = require('path');
const _fs = require('fs-extra');
const _glob = require('glob');

const _config = require('./config.js');
const _manifest = require('./manifest.js');
const _themes = require('./themes.js');
const _packages = require('./packages.js');
const _core = require('./core.js');
const _dist = require('./dist.js');
const _generate = require('./generate.js');
const _utils = require('./utils.js');
const _watcher = require('./watcher.js');
const _logger = _utils.logger;

const ROOT = _path.dirname(_path.dirname(_path.join(__dirname)));

///////////////////////////////////////////////////////////////////////////////
// HELPERS
///////////////////////////////////////////////////////////////////////////////

/*
 * Iterates all given tasks
 */
function _eachTask(cli, args, taskName, namespace) {
  if ( !args ) {
    return Promise.reject('Not enough arguments');
  }

  return new Promise((resolve, reject) => {
    _config.getConfiguration().then((cfg) => {
      _utils.eachp(args.replace(/\s/, '').split(',').map((iter) => {
        return function() {
          iter = (iter || '').replace('-', '_');
          if ( typeof namespace === 'function' ) {
            return namespace(cli, cfg, iter);
          } else {
            if ( namespace[iter] ) {
              let msg = _logger.color([taskName, iter].join(':'), 'green');
              if ( cli.option('debug') ) {
                msg += ' (' + _logger.color('debug mode', 'blue') + ')';
              }

              _logger.log(_logger.color('Running task:', 'bold'), msg);

              return namespace[iter](cli, cfg);
            }
          }

          return Promise.reject('Invalid task: ' + iter);
        };
      })).then(resolve).catch(reject);
    }).catch(reject);
  });
}

///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////

const TASKS = {
  build: {
    config: function(cli, cfg) {
      return _config.writeConfiguration(cli, cfg);
    },

    core: function(cli, cfg) {
      return _core.buildFiles(cli, cfg);
    },

    dist: function(cli, cfg) {
      return _dist.buildFiles(cli, cfg);
    },

    theme: function(cli, cfg) {
      const targets = [
        [cli.option('style'), _themes.buildStyle],
        [cli.option('icons'), _themes.buildIcon],
        [cli.option('static'), _themes.buildStatic],
        [cli.option('fonts'), _themes.buildFonts],
        [cli.option('sounds'), _themes.buildSounds]
      ];

      const list = targets.filter((iter) => {
        return iter && iter[0];
      }).map((iter) => {
        return iter[1](cli, cfg, iter[0]);
      });

      return Promise.all(list);
    },

    themes: function(cli, cfg) {
      return _themes.buildAll(cli, cfg);
    },

    manifest: function(cli, cfg) {
      return _manifest.writeManifest(cli, cfg);
    },

    package: function(cli, cfg) {
      const name = cli.option('name');
      if ( !name || name.indexOf('/') === -1 ) {
        throw new Error('Invalid package name');
      }

      return _packages.buildPackage(cli, cfg, name);
    },

    packages: function(cli, cfg) {
      return _packages.buildPackages(cli, cfg);
    }
  },

  config: {
    set: function(cli, cfg) {
      return _config.set(cfg, cli.option('name'), cli.option('value'), cli.option('import'), cli.option('out'));
    },
    get: function(cli, cfg) {
      return _config.get(cfg, cli.option('name'));
    },
    add_mount: function(cli, cfg) {
      return _config.addMount(cfg, cli.option('name'), cli.option('description') || cli.option('desc'), cli.option('path'), cli.option('transport'), cli.option('ro'));
    },
    add_preload: function(cli, cfg) {
      return _config.addPreload(cfg, cli.option('name'), cli.option('path'), cli.option('type'));
    },
    add_repository: function(cli, cfg) {
      return _config.addRepository(cfg, cli.option('name'));
    },
    add_script: function(cli, cfg) {
      return _config.addOverlayFile(cfg, 'javascript', cli.option('path'), cli.option('overlay', 'custom'));
    },
    add_style: function(cli, cfg) {
      return _config.addOverlayFile(cfg, 'stylesheets', cli.option('path'), cli.option('overlay', 'custom'));
    },
    add_locale: function(cli, cfg) {
      return _config.addOverlayFile(cfg, 'locales', cli.option('path'), cli.option('overlay', 'custom'));
    },
    remove_repository: function(cli, cfg) {
      return _config.removeRepository(cfg, cli.option('name'));
    },
    enable_package: function(cli, cfg) {
      return _config.enablePackage(cfg, cli.option('name'));
    },
    disable_package: function(cli, cfg) {
      return _config.disablePackage(cfg, cli.option('name'));
    },
    list_packages: function(cli, cfg) {
      return _config.listPackages(cfg);
    }
  },

  generate: function(cli, cfg, task) {
    if ( _generate[task] ) {
      return new Promise((resolve, reject) => {
        _generate[task](cli, cfg).then((arg) => {

          const out = cli.option('out');
          if ( out ) {
            _fs.writeFileSync(out, String(arg));
            resolve();
            return;
          } else if ( arg ) {
            _logger.log(arg);
          }

          resolve();
        }).catch(reject);
      });
    } else {
      return Promise.reject('Invalid generator: ' + task);
    }
  }
};

const ORIGINAL_TASKS = (function() {
  const tmp = {};
  Object.keys(TASKS).forEach(function(s) {
    tmp[s] = Object.keys(TASKS[s]);
  });
  return tmp;
})();

///////////////////////////////////////////////////////////////////////////////
// EXPORTS
///////////////////////////////////////////////////////////////////////////////

/*
 * Init
 */
module.exports._init = function() {
  _glob.sync(_path.join(__dirname, 'modules/*.js')).forEach((file) => {
    require(file).register(TASKS);
  });
};

/*
 * Task: `build`
 */
module.exports.build = function(cli, args) {
  if ( !args ) {
    args = ['config', 'dist', 'core', 'themes', 'manifest', 'packages'];

    args = args.concat(Object.keys(TASKS.build).filter(function(i) {
      return ORIGINAL_TASKS.build.indexOf(i) === -1 && args.indexOf(i) === -1;
    })).join(',');
  }

  return _eachTask(cli, args, 'build', TASKS.build);
};

/*
 * Task: `config`
 */
module.exports.config = function(cli, arg) {
  return new Promise((resolve, reject) => {
    arg = (arg || '').replace('-', '_');

    if ( TASKS.config[arg] ) {
      _config.getConfiguration().then((cfg) => {
        TASKS.config[arg](cli, cfg).then((arg) => {
          if ( typeof arg !== 'undefined' ) {
            _logger.log(arg);
          }
          resolve();
        }).catch(reject);
      });
    } else {
      reject('Invalid action: ' + arg);
    }
  });
};

/*
 * Task: `watch`
 */
module.exports.watch = function(cli, args) {
  return _watcher.watch(cli);
};

/*
 * Task: `generate`
 */
module.exports.generate = function(cli, args) {
  return _eachTask(cli, args, 'generate', TASKS.generate);
};

/*
 * Task: `clean`
 */
module.exports.clean = function(cli, args) {
  return new Promise((resolve, reject) => {
    _logger.log(_logger.color('Running task:', 'bold'), _logger.color('clean', 'green'));

    _config.getConfiguration().then((cfg) => {
      Promise.all([
        _config.clean(cli, cfg),
        _dist.clean(cli, cfg),
        _core.clean(cli, cfg),
        _manifest.clean(cli, cfg),
        _themes.clean(cli, cfg),
        _packages.clean(cli, cfg)
      ]).then(resolve).catch(reject);
    });
  });
};

/*
 * Task: `run`
 */
module.exports.run = function(cli, args) {
  const instance = require(_path.join(ROOT, 'src/server/node/core/instance.js'));
  const settings = require(_path.join(ROOT, 'src/server/node/core/settings.js'));

  const opts = {
    HOSTNAME: cli.option('hostname'),
    DEBUG: cli.option('debug'),
    PORT: cli.option('port'),
    LOGLEVEL: cli.option('loglevel')
  };

  instance.init(opts).then((env) => {
    const config = settings.get();
    if ( config.tz ) {
      process.env.TZ = config.tz;
    }

    process.on('exit', () => {
      instance.destroy();
    });

    instance.run();

    process.on('uncaughtException', (error) => {
      _logger.log('UNCAUGHT EXCEPTION', error, error.stack);
    });

    process.on('unhandledRejection', (error) => {
      console.log('UNCAUGHT REJECTION', error);
    });

    ['SIGTERM', 'SIGINT'].forEach((sig) => {
      process.on(sig, () => {
        console.log('\n');
        instance.destroy((err) => {
          process.exit(err ? 1 : 0);
        });
      });
    });
  }).catch((error) => {
    _logger.log(error);
    process.exit(1);
  });

  return new Promise(() => {
    // This is one promise we can't keep! :(
  });
};
<|endoftext|>


<commit_before>/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
var CKBUILDER_CONFIG = {
  skin: 'moono',
  ignore: [
    'bender.js',
    '.bender',
    'bender-err.log',
    'bender-out.log',
    'dev',
    '.DS_Store',
    '.editorconfig',
    '.gitignore',
    '.gitattributes',
    'gruntfile.js',
    '.idea',
    '.jscsrc',
    '.jshintignore',
    '.jshintrc',
    '.mailmap',
    'node_modules',
    'package.json',
    'README.md',
    'tests',
    // Additional ignores for the XWiki WebJar.
    'samples',
    'CHANGES.md',
    'LICENSE.md'
  ],
  plugins: {
    a11yhelp: 1,
    basicstyles: 1,
    bidi: 1,
    blockquote: 1,
    clipboard: 1,
    colorbutton: 1,
    contextmenu: 1,
    dialogadvtab: 1,
    elementspath: 1,
    enterkey: 1,
    entities: 1,
    filebrowser: 1,
    find: 1,
    floatingspace: 1,
    font: 1,
    format: 1,
    horizontalrule: 1,
    image2: 1,
    indentlist: 1,
    justify: 1,
    link: 1,
    list: 1,
    liststyle: 1,
    magicline: 1,
    maximize: 1,
    notification: 1,
    pastefromword: 1,
    pastetext: 1,
    removeformat: 1,
    resize: 1,
    save: 1,
    showborders: 1,
    sourcearea: 1,
    specialchar: 1,
    stylescombo: 1,
    tab: 1,
    table: 1,
    tableresize: 1,
    tabletools: 1,
    toolbar: 1,
    undo: 1,
    uploadimage: 1,
    wysiwygarea: 1,
    'xwiki-filter': 1,
    'xwiki-image': 1,
    'xwiki-link': 1,
    'xwiki-macro': 1,
    'xwiki-save': 1,
    'xwiki-source': 1,
    'xwiki-table': 1
  }
};
<commit_msg>CKEDITOR-84: Enable the auto-link plugin by default<commit_after>/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
var CKBUILDER_CONFIG = {
  skin: 'moono',
  ignore: [
    'bender.js',
    '.bender',
    'bender-err.log',
    'bender-out.log',
    'dev',
    '.DS_Store',
    '.editorconfig',
    '.gitignore',
    '.gitattributes',
    'gruntfile.js',
    '.idea',
    '.jscsrc',
    '.jshintignore',
    '.jshintrc',
    '.mailmap',
    'node_modules',
    'package.json',
    'README.md',
    'tests',
    // Additional ignores for the XWiki WebJar.
    'samples',
    'CHANGES.md',
    'LICENSE.md'
  ],
  plugins: {
    a11yhelp: 1,
    autolink: 1,
    basicstyles: 1,
    bidi: 1,
    blockquote: 1,
    clipboard: 1,
    colorbutton: 1,
    contextmenu: 1,
    dialogadvtab: 1,
    elementspath: 1,
    enterkey: 1,
    entities: 1,
    filebrowser: 1,
    find: 1,
    floatingspace: 1,
    font: 1,
    format: 1,
    horizontalrule: 1,
    image2: 1,
    indentlist: 1,
    justify: 1,
    link: 1,
    list: 1,
    liststyle: 1,
    magicline: 1,
    maximize: 1,
    notification: 1,
    pastefromword: 1,
    pastetext: 1,
    removeformat: 1,
    resize: 1,
    save: 1,
    showborders: 1,
    sourcearea: 1,
    specialchar: 1,
    stylescombo: 1,
    tab: 1,
    table: 1,
    tableresize: 1,
    tabletools: 1,
    toolbar: 1,
    undo: 1,
    uploadimage: 1,
    wysiwygarea: 1,
    'xwiki-filter': 1,
    'xwiki-image': 1,
    'xwiki-link': 1,
    'xwiki-macro': 1,
    'xwiki-save': 1,
    'xwiki-source': 1,
    'xwiki-table': 1
  }
};
<|endoftext|>


<commit_before>var Builder = require('../index');
var expect = require('chai').expect;
var toFileURL = require('../lib/utils.js').toFileURL;
var fs = require('fs');

suite('Test compiler cache', function() {
  var builder = new Builder('test/fixtures/test-cache-tree');
  builder.config({ transpiler: 'babel' });

  test('Use compile cache entry when available', function() {
    var loadName = 'simple.js';
    var outputPath = 'test/output/cached.js';
    var cacheObj;
    var tree;

    return builder.trace(loadName).then(function(_tree) {
      tree = _tree;
      return builder.bundle(tree);
    })
    .then(function() {
      var cacheEntry = builder.getCache();

      expect(cacheEntry).to.be.an('object');

      cacheObj = cacheEntry.compile.loads['simple.js'];

      expect(cacheObj).to.be.an('object');
      expect(cacheObj.hash).to.be.a('string');
      expect(cacheObj.output).to.be.an('object');

      // poison cache
      cacheObj.output.source = cacheObj.output.source.replace('hate', 'love');

      return builder.bundle(tree);
    })
    .then(function(output) {
      // verify buildTree use poisoned cache rather than recompiling
      var outputSource = output.source;
      expect(outputSource).not.to.contain('hate caches');
      expect(outputSource).to.contain('love caches');

      // invalidate poisoned cache entry and rebuild
      cacheObj.hash = 'out of date';
      return builder.bundle(tree);
    })
    .then(function(output) {
      // verify original source is used once more
      var outputSource = output.source;
      expect(outputSource).to.contain('hate caches');
      expect(outputSource).not.to.contain('love caches');
    });
  });

  test('Use trace cache when available', function() {
    // construct the load record for the cache
    var cacheObj = {
      trace: {
        'simple.js': {
          name: 'simple.js',
          path: 'fixtures/test-cache-tree/simple.js',
          metadata: {
            deps: [],
            format: 'amd',
            isAnon: true
          },
          deps: [],
          depMap: {},
          source: 'define([], function(module) {\n  console.log(\'fake cache\');\n});\n',
          originalSource: 'define([], function(module) {\n  console.log(\'fake cache\');\n});\n'
        }
      }
    };

    builder.reset();
    builder.setCache(cacheObj);

    return builder.bundle('simple.js').then(function(output) {
      expect(output.source).to.contain('fake cache');
    });
  });

  test('Cache invalidation', function() {
    var cacheObj = {
      trace: {
        'simple.js': {},
        'another/path.js': {}
      }
    };

    builder.reset();
    builder.setCache(cacheObj);

    var invalidated = builder.invalidate('*');
    assert.deepEqual(invalidated, [builder.loader.normalizeSync('simple.js'), builder.loader.normalizeSync('another/path.js')]);

    cacheObj = {
      trace: {
        'simple.js': {},
        'new/path.js': {},
        'deep/wildcard/test.js': {}
      }
    };

    builder.setCache(cacheObj);

    invalidated = builder.invalidate('new/path.js');
    assert.deepEqual(invalidated, [builder.loader.normalizeSync('new/path.js')]);

    invalidated = builder.invalidate('deep/*.js');
    assert.deepEqual(invalidated, [builder.loader.normalizeSync('deep/wildcard/test.js')]);
  });

  test('builder.loader.fetch sets load.metadata.timestamp', function() {
    var source = 'export var p = 5;';
    var builder = new Builder('test/output');
    fs.writeFileSync('./test/output/timestamp-module.js', source);
    var address = builder.loader.normalizeSync('./test/output/timestamp-module.js');
    var load = { name: address, address: address, metadata: {} };
    return builder.loader.fetch(load)
      .then(function(text) {
         //console.log(JSON.stringify(load));
         assert(text == source);          // true
         assert(load.metadata.deps);      // true
         assert(load.metadata.timestamp); // false
      })
  });

  test('Bundle example', function() {
    var builder = new Builder('test/output');
    fs.writeFileSync('./test/output/dynamic-module.js', 'export var p = 5;');

    return builder.bundle('dynamic-module.js')
    .then(function(output) {
      assert(output.source.match(/p = 5/));

      fs.writeFileSync('./test/output/dynamic-module.js', 'export var p = 6;');
      builder.invalidate('dynamic-module.js');

      return builder.bundle('dynamic-module.js');
    })
    .then(function(output) {
      assert(output.source.match(/p = 6/));
    });
  });

  test('Bundle example with imported file', function() {
    var builder = new Builder('test/output');

    fs.writeFileSync('./test/output/dynamic-import.js', [
      'const d = 9;',
      'export default d;'
    ].join('\n'));

    fs.writeFileSync('./test/output/dynamic-main.js', [
      'import d from "./dynamic-import.js";',
      'console.log(d);'
    ].join('\n'));

    return builder.bundle('dynamic-main.js')
    .then(function(output) {
      assert(output.source.match(/d = 9/));
      assert(output.source.match(/console/));

      fs.writeFileSync('./test/output/dynamic-import.js', [
        'import "./dynamic-import2.js";', // Add another transitive dependency.
        'const d = 7;',
        'export default d;'
      ].join('\n'));
      builder.invalidate('dynamic-import.js');

      fs.writeFileSync('./test/output/dynamic-import2.js', [
        'const u = "transitive";',
        'export default u;'
      ].join('\n'));

      return builder.bundle('dynamic-main.js');
    })
    .then(function(output) {
      assert(output.source.match(/transitive/));
      assert(output.source.match(/d = 7/));
      assert(output.source.match(/console/));

      // Remove the transitive dependency from the build.
      fs.writeFileSync('./test/output/dynamic-import.js', [
        'const d = 7;',
        'export default d;'
      ].join('\n'));
      builder.invalidate('dynamic-import.js');

      return builder.bundle('dynamic-main.js');
    })
    .then(function(output) {
      assert(!output.source.match(/transitive/));
    });
  });

  test('Static build example statting check', function() {
    var builder = new Builder('test/output');

    fs.writeFileSync('./test/output/static-main.js', "import { testThing } from './static-test-module.js'; testThing();");
    fs.writeFileSync('./test/output/static-test-module.js', "export function testThing() { console.log('test'); }");

    return builder.buildStatic('static-main.js')
    .then(function() {
      builder.invalidate('static-main.js');
      // despite removing the file, it remains cached
      fs.unlinkSync('./test/output/static-test-module.js');
      return builder.buildStatic('static-main.js');
    });
  });

  test('Static build example dependency reload check', function() {
    var builder = new Builder('test/output');

    fs.writeFileSync('./test/output/static-main.js', "import { testThing } from './static-test-module.js'; testThing();");
    fs.writeFileSync('./test/output/static-test-module.js', "export function testThing() { console.log('test'); }");

    return builder.buildStatic('static-main.js')
    .then(function() {
      fs.writeFileSync('./test/output/static-test-module.js', "export function testThing() { console.log('new test'); }")
      builder.invalidate('static-test-module.js');
      return builder.buildStatic('static-main.js');
    });
  });

  test('Static build, fetch override', function () {
    var builder = new Builder('test/fixtures/test-tree');
    return builder.buildStatic('foo.js', {
      fetch: function (load, fetch) {
        if (load.name.indexOf('foo.js') !== -1) {
          return fs.readFileSync('test/fixtures/test-tree/cjs.js', 'utf8');
        } else {
          return fetch(load);
        }
      }
    });
  });

  test('Static string build', function () {
    var builder = new Builder('test/fixtures/test-tree');
    return builder.bundle('foo.js', {
      fetch: function (load, fetch) {
        if (load.name.indexOf('foo.js') !== -1) {
          return fs.readFileSync('test/fixtures/test-tree/cjs.js', 'utf8');
        } else {
          return fetch(load);
        }
      }
    });
  });
});
<commit_msg>include fetch cache test<commit_after>var Builder = require('../index');
var expect = require('chai').expect;
var toFileURL = require('../lib/utils.js').toFileURL;
var fs = require('fs');

suite('Test compiler cache', function() {
  var builder = new Builder('test/fixtures/test-cache-tree');
  builder.config({ transpiler: 'babel' });

  test('Use compile cache entry when available', function() {
    var loadName = 'simple.js';
    var outputPath = 'test/output/cached.js';
    var cacheObj;
    var tree;

    return builder.trace(loadName).then(function(_tree) {
      tree = _tree;
      return builder.bundle(tree);
    })
    .then(function() {
      var cacheEntry = builder.getCache();

      expect(cacheEntry).to.be.an('object');

      cacheObj = cacheEntry.compile.loads['simple.js'];

      expect(cacheObj).to.be.an('object');
      expect(cacheObj.hash).to.be.a('string');
      expect(cacheObj.output).to.be.an('object');

      // poison cache
      cacheObj.output.source = cacheObj.output.source.replace('hate', 'love');

      return builder.bundle(tree);
    })
    .then(function(output) {
      // verify buildTree use poisoned cache rather than recompiling
      var outputSource = output.source;
      expect(outputSource).not.to.contain('hate caches');
      expect(outputSource).to.contain('love caches');

      // invalidate poisoned cache entry and rebuild
      cacheObj.hash = 'out of date';
      return builder.bundle(tree);
    })
    .then(function(output) {
      // verify original source is used once more
      var outputSource = output.source;
      expect(outputSource).to.contain('hate caches');
      expect(outputSource).not.to.contain('love caches');
    });
  });

  test('Use trace cache when available', function() {
    // construct the load record for the cache
    var cacheObj = {
      trace: {
        'simple.js': {
          name: 'simple.js',
          path: 'fixtures/test-cache-tree/simple.js',
          metadata: {
            deps: [],
            format: 'amd',
            isAnon: true
          },
          deps: [],
          depMap: {},
          source: 'define([], function(module) {\n  console.log(\'fake cache\');\n});\n',
          originalSource: 'define([], function(module) {\n  console.log(\'fake cache\');\n});\n'
        }
      }
    };

    builder.reset();
    builder.setCache(cacheObj);

    return builder.bundle('simple.js').then(function(output) {
      expect(output.source).to.contain('fake cache');
    });
  });

  test('Cache invalidation', function() {
    var cacheObj = {
      trace: {
        'simple.js': {},
        'another/path.js': {}
      }
    };

    builder.reset();
    builder.setCache(cacheObj);

    var invalidated = builder.invalidate('*');
    assert.deepEqual(invalidated, [builder.loader.normalizeSync('simple.js'), builder.loader.normalizeSync('another/path.js')]);

    cacheObj = {
      trace: {
        'simple.js': {},
        'new/path.js': {},
        'deep/wildcard/test.js': {}
      }
    };

    builder.setCache(cacheObj);

    invalidated = builder.invalidate('new/path.js');
    assert.deepEqual(invalidated, [builder.loader.normalizeSync('new/path.js')]);

    invalidated = builder.invalidate('deep/*.js');
    assert.deepEqual(invalidated, [builder.loader.normalizeSync('deep/wildcard/test.js')]);
  });

  test('builder.loader.fetch sets load.metadata.timestamp', function() {
    var source = 'export var p = 5;';
    var builder = new Builder('test/output');
    fs.writeFileSync('./test/output/timestamp-module.js', source);
    var address = builder.loader.normalizeSync('./test/output/timestamp-module.js');
    var load = { name: address, address: address, metadata: {} };
    return builder.loader.fetch(load)
      .then(function(text) {
         //console.log(JSON.stringify(load));
         assert(text == source);          // true
         assert(load.metadata.deps);      // true
         assert(load.metadata.timestamp); // false
      })
  });

  test('Bundle example', function() {
    var builder = new Builder('test/output');
    fs.writeFileSync('./test/output/dynamic-module.js', 'export var p = 5;');

    return builder.bundle('dynamic-module.js')
    .then(function(output) {
      assert(output.source.match(/p = 5/));

      fs.writeFileSync('./test/output/dynamic-module.js', 'export var p = 6;');
      builder.invalidate('dynamic-module.js');

      return builder.bundle('dynamic-module.js');
    })
    .then(function(output) {
      assert(output.source.match(/p = 6/));
    });
  });

  test('Bundle example with imported file', function() {
    var builder = new Builder('test/output');

    fs.writeFileSync('./test/output/dynamic-import.js', [
      'const d = 9;',
      'export default d;'
    ].join('\n'));

    fs.writeFileSync('./test/output/dynamic-main.js', [
      'import d from "./dynamic-import.js";',
      'console.log(d);'
    ].join('\n'));

    return builder.bundle('dynamic-main.js')
    .then(function(output) {
      assert(output.source.match(/d = 9/));
      assert(output.source.match(/console/));

      fs.writeFileSync('./test/output/dynamic-import.js', [
        'import "./dynamic-import2.js";', // Add another transitive dependency.
        'const d = 7;',
        'export default d;'
      ].join('\n'));
      builder.invalidate('dynamic-import.js');

      fs.writeFileSync('./test/output/dynamic-import2.js', [
        'const u = "transitive";',
        'export default u;'
      ].join('\n'));

      return builder.bundle('dynamic-main.js');
    })
    .then(function(output) {
      assert(output.source.match(/transitive/));
      assert(output.source.match(/d = 7/));
      assert(output.source.match(/console/));

      // Remove the transitive dependency from the build.
      fs.writeFileSync('./test/output/dynamic-import.js', [
        'const d = 7;',
        'export default d;'
      ].join('\n'));
      builder.invalidate('dynamic-import.js');

      return builder.bundle('dynamic-main.js');
    })
    .then(function(output) {
      assert(!output.source.match(/transitive/));
    });
  });

  test('Static build example statting check', function() {
    var builder = new Builder('test/output');

    fs.writeFileSync('./test/output/static-main.js', "import { testThing } from './static-test-module.js'; testThing();");
    fs.writeFileSync('./test/output/static-test-module.js', "export function testThing() { console.log('test'); }");

    return builder.buildStatic('static-main.js')
    .then(function() {
      builder.invalidate('static-main.js');
      // despite removing the file, it remains cached
      fs.unlinkSync('./test/output/static-test-module.js');
      return builder.buildStatic('static-main.js');
    });
  });

  test('Static build example dependency reload check', function() {
    var builder = new Builder('test/output');

    fs.writeFileSync('./test/output/static-main.js', "import { testThing } from './static-test-module.js'; testThing();");
    fs.writeFileSync('./test/output/static-test-module.js', "export function testThing() { console.log('test'); }");

    return builder.buildStatic('static-main.js')
    .then(function() {
      fs.writeFileSync('./test/output/static-test-module.js', "export function testThing() { console.log('new test'); }")
      builder.invalidate('static-test-module.js');
      return builder.buildStatic('static-main.js');
    });
  });

  test('Static build, fetch override', function () {
    var builder = new Builder('test/fixtures/test-tree');
    return builder.buildStatic('foo.js', {
      fetch: function (load, fetch) {
        if (load.name.indexOf('foo.js') !== -1) {
          return fs.readFileSync('test/fixtures/test-tree/cjs.js', 'utf8');
        } else {
          return fetch(load);
        }
      }
    });
  });

  test('Static build, fetch override with callback', function () {
    var builder = new Builder('test/fixtures/test-tree');
    return builder.buildStatic('cjs.js', {
      fetch: function (load, fetch) {
        return fetch(load);
      }
    });
  });

  test('Static string build', function () {
    var builder = new Builder('test/fixtures/test-tree');
    return builder.bundle('foo.js', {
      fetch: function (load, fetch) {
        if (load.name.indexOf('foo.js') !== -1) {
          return fs.readFileSync('test/fixtures/test-tree/cjs.js', 'utf8');
        } else {
          return fetch(load);
        }
      }
    });
  });
});
<|endoftext|>


<commit_before>/**
 * Extracts peer-review values from responses to retrospective survey questions
 * submitted by a project's team members. Uses these values to compute & update
 * each project member's project-specific and overall stats.
 */
import {getSurveyById} from 'src/server/db/survey'
import {findQuestionsByIds} from 'src/server/db/question'
import {findResponsesBySurveyId} from 'src/server/db/response'
import {savePlayerProjectStats, findPlayersByIds} from 'src/server/db/player'
import {statsByDescriptor} from 'src/server/db/stat'
import {avg, sum, mapById, safePushInt, toPairs} from 'src/server/util'
import {userCan, roundDecimal} from 'src/common/util'
import {
  aggregateBuildCycles,
  relativeContribution,
  expectedContribution,
  expectedContributionDelta,
  effectiveContributionCycles,
  eloRatings,
  experiencePoints,
  technicalHealth,
  cultureContribution,
  cultureContributionStructure,
  cultureContributionSafety,
  cultureContributionTruth,
  cultureContributionChallenge,
  cultureContributionSupport,
  cultureContributionEngagement,
  cultureContributionEnjoyment,
  teamPlay,
  receptiveness,
  flexibleLeadership,
  resultsFocus,
  frictionReduction,
} from 'src/server/util/stats'
import {STAT_DESCRIPTORS} from 'src/common/models/stat'
import {groupResponsesBySubject} from 'src/server/util/survey'
import getPlayerInfo from 'src/server/actions/getPlayerInfo'

const INITIAL_RATINGS = {
  DEFAULT: 1000,
}
const K_FACTORS = {
  BEGINNER: 100,
  DEFAULT: 20,
}

export default async function updatePlayerStatsForProject(project) {
  _assertValidProject(project)
  const {retrospectiveSurveyId} = project

  const retroSurvey = await getSurveyById(retrospectiveSurveyId)
  const retroResponses = await findResponsesBySurveyId(retrospectiveSurveyId)
  const retroQuestionIds = retroSurvey.questionRefs.map(qref => qref.questionId)
  const retroQuestions = await findQuestionsByIds(retroQuestionIds)

  let teamPlayersById = mapById(await findPlayersByIds(project.playerIds))
  const statsQuestions = await _getStatsQuestions(retroQuestions)

  // ensure that we're only looking at valid responses about players who
  // actually played on the team, and adjust relative contribution responses to
  // ensure that they total 100%
  const playerResponses = _getPlayerResponses(project, teamPlayersById, retroResponses, retroQuestions, statsQuestions)
  const adjustedPlayerResponses = _adjustRCResponsesTo100Percent(playerResponses, statsQuestions)
  const playerResponsesById = groupResponsesBySubject(adjustedPlayerResponses)
  const adjustedProject = {...project, playerIds: Array.from(playerResponsesById.keys())}
  teamPlayersById = mapById(Array.from(teamPlayersById.values())
    .filter(player => adjustedProject.playerIds.includes(player.id)))

  // compute all stats and initialize Elo rating
  const playerStatsConfigsById = await _getPlayersStatsConfig(adjustedProject.playerIds)
  const computeStats = _computeStatsClosure(project, teamPlayersById, retroResponses, statsQuestions, playerStatsConfigsById)
  const teamPlayersStats = Array.from(playerResponsesById.values())
    .map(responses => computeStats(responses, statsQuestions))

  // compute updated Elo ratings and merge them in
  const teamPlayersStatsWithUpdatedEloRatings = _mergeEloRatings(teamPlayersStats, playerStatsConfigsById)

  const playerStatsUpdates = teamPlayersStatsWithUpdatedEloRatings.map(({playerId, ...stats}) => {
    return savePlayerProjectStats(playerId, project.id, stats)
  })

  await Promise.all(playerStatsUpdates)
}

function _assertValidProject(project) {
  const {id, name, playerIds, retrospectiveSurveyId} = project
  if (!playerIds || playerIds.length === 0) {
    throw new Error(`No players found on team for project ${name} (${id})`)
  }
  if (!retrospectiveSurveyId) {
    throw new Error(`Retrospective survey ID not set for project ${name} (${id})`)
  }
}

function _getPlayerResponses(project, teamPlayersById, retroResponses, retroQuestions, statsQuestions) {
  const isZeroHoursResponse = response => (response.questionId === statsQuestions.hours.id && parseInt(response.value, 10) === 0)
  const inactivePlayerIds = retroResponses.filter(isZeroHoursResponse).map(_ => _.respondentId)

  const isNotFromOrAboutInactivePlayer = response => {
    return !inactivePlayerIds.includes(response.respondentId) &&
      !inactivePlayerIds.includes(response.subjectId)
  }
  const activeRetroResponses = retroResponses.filter(isNotFromOrAboutInactivePlayer)

  const retroQuestionsById = mapById(retroQuestions)
  const responseQuestionSubjectIsPlayerOrTeam = response => {
    const responseQuestion = retroQuestionsById.get(response.questionId)
    const {subjectType} = responseQuestion || {}
    return subjectType === 'player' || subjectType === 'team'
  }
  const playerResponses = activeRetroResponses.filter(responseQuestionSubjectIsPlayerOrTeam)

  const playerIsOnTeam = playerId => !teamPlayersById.has(playerId)
  const invalidPlayerIds = Array.from(playerResponses
    .map(_ => _.subjectId)
    .filter(playerIsOnTeam)
    .reduce((result, playerId) => {
      result.add(playerId)
      return result
    }, new Set()))
  if (invalidPlayerIds.length > 0) {
    console.warn(
      'Survey responses found for players who are not on project ' +
      `${project.name} (${project.id}): ${invalidPlayerIds.join(', ')}. ` +
      'Ignoring responses from these players.'
    )
    return playerResponses.filter(response => !invalidPlayerIds.includes(response.subjectId))
  }

  return playerResponses
}

function _adjustRCResponsesTo100Percent(playerResponses, statsQuestions) {
  // adjust relative contribution responses so that they always add-up to 100%
  // (especially important because inactive players may have been removed, but
  // we do it for all cases because it is actually "more correct")
  const rcResponsesByRespondentId = playerResponses
    .filter(response => response.questionId === statsQuestions.rc.id)
    .reduce((result, response) => {
      const rcResponsesForRespondent = result.get(response.respondentId) || []
      rcResponsesForRespondent.push(response)
      result.set(response.respondentId, rcResponsesForRespondent)
      return result
    }, new Map())
  return playerResponses.map(response => {
    if (response.questionId !== statsQuestions.rc.id) {
      return response
    }
    const rcResponses = rcResponsesByRespondentId.get(response.respondentId)
    const values = rcResponses.map(_ => _.value)
    const totalContrib = sum(values)
    return {...response, value: response.value / totalContrib * 100}
  })
}

async function _getStatsQuestions(questions) {
  const stats = await statsByDescriptor()
  const getQ = descriptor => questions.filter(_ => _.statId === stats[descriptor].id)[0] || {}

  return {
    th: getQ(STAT_DESCRIPTORS.TECHNICAL_HEALTH),
    rc: getQ(STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION),
    hours: getQ(STAT_DESCRIPTORS.PROJECT_HOURS),
    challenge: getQ(STAT_DESCRIPTORS.CHALLENGE),
    cc: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION),
    cultureContributionStructure: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_STRUCTURE),
    cultureContributionSafety: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SAFETY),
    cultureContributionTruth: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_TRUTH),
    cultureContributionChallenge: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_CHALLENGE),
    cultureContributionSupport: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SUPPORT),
    cultureContributionEngagement: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENGAGEMENT),
    cultureContributionEnjoyment: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENJOYMENT),
    tp: getQ(STAT_DESCRIPTORS.TEAM_PLAY),
    receptiveness: getQ(STAT_DESCRIPTORS.RECEPTIVENESS),
    resultsFocus: getQ(STAT_DESCRIPTORS.RESULTS_FOCUS),
    flexibleLeadership: getQ(STAT_DESCRIPTORS.FLEXIBLE_LEADERSHIP),
    frictionReduction: getQ(STAT_DESCRIPTORS.FRICTION_REDUCTION),
  }
}

async function _getPlayersStatsConfig(playerIds) {
  const users = await getPlayerInfo(playerIds)
  const playerStatsConfigs = users.map(user => ({
    id: user.id,
    ignoreWhenComputingElo: userCan(user, 'beIgnoredWhenComputingElo'),
  }))

  return mapById(playerStatsConfigs)
}

function _playerResponsesForQuestionById(retroResponses, questionId, valueFor = _ => _) {
  const responses = retroResponses.filter(_ => _.questionId === questionId)

  return responses.reduce((result, response) => {
    result.set(response.respondentId, valueFor(response.value))
    return result
  }, new Map())
}

function _computeStatsClosure(project, teamPlayersById, retroResponses, statsQuestions, playerStatsConfigsById) {
  const teamPlayerHours = _playerResponsesForQuestionById(retroResponses, statsQuestions.hours.id, _ => parseInt(_, 10))
  const teamPlayerChallenges = _playerResponsesForQuestionById(retroResponses, statsQuestions.challenge.id)
  const teamHours = sum(Array.from(teamPlayerHours.values()))

  // create a stats-computation function based on a closure of the passed-in
  // parameters as well as some additional derived data
  return (responses, statsQuestions) => {
    const playerId = responses[0].subjectId
    const player = teamPlayersById.get(playerId)
    const scores = _extractPlayerScores(statsQuestions, responses, playerId)

    const expectedHours = project.expectedHours || 40

    const stats = {}
    stats.playerId = playerId // will be removed later
    stats.teamHours = teamHours
    stats.hours = Math.min(teamPlayerHours.get(playerId) || 0, expectedHours)
    stats.timeOnTask = (stats.hours === 0) ? 0 : stats.hours / expectedHours * 100
    stats.challenge = teamPlayerChallenges.get(playerId)
    stats.abc = aggregateBuildCycles(teamPlayersById.size)
    stats.th = technicalHealth(scores.th)
    stats.cc = cultureContribution(scores.cc)
    stats.cultureContributionStructure = cultureContributionStructure(scores.cultureContributionStructure)
    stats.cultureContributionSafety = cultureContributionSafety(scores.cultureContributionSafety)
    stats.cultureContributionTruth = cultureContributionTruth(scores.cultureContributionTruth)
    stats.cultureContributionChallenge = cultureContributionChallenge(scores.cultureContributionChallenge)
    stats.cultureContributionSupport = cultureContributionSupport(scores.cultureContributionSupport)
    stats.cultureContributionEngagement = cultureContributionEngagement(scores.cultureContributionEngagement)
    stats.cultureContributionEnjoyment = cultureContributionEnjoyment(scores.cultureContributionEnjoyment)
    stats.tp = teamPlay(scores.tp)
    stats.receptiveness = receptiveness(scores.receptiveness)
    stats.resultsFocus = resultsFocus(scores.resultsFocus)
    stats.flexibleLeadership = flexibleLeadership(scores.flexibleLeadership)
    stats.frictionReduction = frictionReduction(scores.frictionReduction)
    stats.rc = relativeContribution(scores.rc.all)
    stats.rcSelf = scores.rc.self || 0
    stats.rcOther = roundDecimal(avg(scores.rc.other)) || 0
    stats.rcPerHour = stats.hours && stats.rc ? roundDecimal(stats.rc / stats.hours) : 0
    stats.estimationBias = stats.rcSelf - stats.rcOther
    stats.estimationAccuracy = 100 - Math.abs(stats.estimationBias)
    stats.ec = expectedContribution(stats.hours, stats.teamHours)
    stats.ecd = expectedContributionDelta(stats.ec, stats.rc)
    stats.ecc = effectiveContributionCycles(stats.abc, stats.rc)
    stats.xp = experiencePoints(teamHours, stats.rc)
    if (!playerStatsConfigsById.get(playerId).ignoreWhenComputingElo) {
      stats.elo = (player.stats || {}).elo || {} // pull current overall Elo stats
    }

    return stats
  }
}

function _extractPlayerScores(statsQuestions, responses, playerId) {
  // extract values needed for each player's stats
  // from survey responses submitted about them
  const scores = {
    th: [],
    cc: [],
    cultureContributionStructure: [],
    cultureContributionSafety: [],
    cultureContributionTruth: [],
    cultureContributionChallenge: [],
    cultureContributionSupport: [],
    cultureContributionEngagement: [],
    cultureContributionEnjoyment: [],
    tp: [],
    receptiveness: [],
    flexibleLeadership: [],
    resultsFocus: [],
    frictionReduction: [],
    rc: {
      all: [],
      self: null,
      other: [],
    },
  }
  const appendScoreStats = Object.keys(scores).filter(_ => _ !== 'rc')

  responses.forEach(response => {
    const {
      questionId: responseQuestionId,
      value: responseValue,
    } = response

    switch (responseQuestionId) {
      case statsQuestions.rc.id:
        safePushInt(scores.rc.all, responseValue)
        if (response.respondentId === playerId) {
          scores.rc.self = parseInt(responseValue, 10)
        } else {
          safePushInt(scores.rc.other, responseValue)
        }
        break

      default:
        appendScoreStats.forEach(stat => {
          if (responseQuestionId === statsQuestions[stat].id) {
            safePushInt(scores[stat], responseValue)
          }
        })
        break
    }
  })

  return scores
}

function _mergeEloRatings(teamPlayersStats, playerStatsConfigsById) {
  const playersWithEloStats = teamPlayersStats
    .filter(({playerId}) => !playerStatsConfigsById.get(playerId).ignoreWhenComputingElo)
  const eloRatings = _computeEloRatings(playersWithEloStats)
  const teamPlayersStatsWithUpdatedEloRatings = teamPlayersStats.map(stats => {
    const updatedElo = eloRatings.get(stats.playerId)
    if (!updatedElo) {
      return stats
    }
    const {rating, matches, kFactor, score} = updatedElo
    return {...stats, elo: {rating, matches, kFactor, score}}
  })

  return teamPlayersStatsWithUpdatedEloRatings
}

function _computeEloRatings(playerStats) {
  const scoreboard = playerStats
    .reduce((result, {playerId, ...stats}) => {
      const {elo = {}} = stats
      result.set(playerId, {
        id: playerId,
        rating: elo.rating || INITIAL_RATINGS.DEFAULT,
        matches: elo.matches || 0,
        kFactor: _kFactor(elo.matches),
        score: stats.rcPerHour, // effectiveness
      })
      return result
    }, new Map())

  // sorted by elo (descending) solely for the sake of being deterministic
  const sortedPlayerIds = Array.from(scoreboard.values())
                            .sort((a, b) => a.rating - b.rating)
                            .map(item => item.id)

  // pair every team player up to run "matches"
  const matches = toPairs(sortedPlayerIds)

  // for each team player pair, update ratings based on relative effectiveness
  matches.forEach(([playerIdA, playerIdB]) => {
    const playerA = scoreboard.get(playerIdA)
    const playerB = scoreboard.get(playerIdB)
    const [playerRatingA, playerRatingB] = eloRatings([playerA, playerB])

    playerA.rating = playerRatingA
    playerA.matches++
    playerA.kFactor = _kFactor(playerA.matches)

    playerB.rating = playerRatingB
    playerB.matches++
    playerB.kFactor = _kFactor(playerB.matches)
  })

  return scoreboard
}

function _kFactor(numMatches) {
  return (numMatches || 0) < 20 ?
    K_FACTORS.BEGINNER :
    K_FACTORS.DEFAULT
}
<commit_msg>factor-in estimation accuracy when computing relative contribution<commit_after>/**
 * Extracts peer-review values from responses to retrospective survey questions
 * submitted by a project's team members. Uses these values to compute & update
 * each project member's project-specific and overall stats.
 */
import {getSurveyById} from 'src/server/db/survey'
import {findQuestionsByIds} from 'src/server/db/question'
import {findResponsesBySurveyId} from 'src/server/db/response'
import {savePlayerProjectStats, findPlayersByIds} from 'src/server/db/player'
import {statsByDescriptor} from 'src/server/db/stat'
import {avg, sum, mapById, safePushInt, toPairs} from 'src/server/util'
import {userCan, roundDecimal} from 'src/common/util'
import {
  aggregateBuildCycles,
  relativeContribution,
  expectedContribution,
  expectedContributionDelta,
  effectiveContributionCycles,
  eloRatings,
  experiencePoints,
  technicalHealth,
  cultureContribution,
  cultureContributionStructure,
  cultureContributionSafety,
  cultureContributionTruth,
  cultureContributionChallenge,
  cultureContributionSupport,
  cultureContributionEngagement,
  cultureContributionEnjoyment,
  teamPlay,
  receptiveness,
  flexibleLeadership,
  resultsFocus,
  frictionReduction,
} from 'src/server/util/stats'
import {STAT_DESCRIPTORS} from 'src/common/models/stat'
import {groupResponsesBySubject} from 'src/server/util/survey'
import getPlayerInfo from 'src/server/actions/getPlayerInfo'

const INITIAL_RATINGS = {
  DEFAULT: 1000,
}
const K_FACTORS = {
  BEGINNER: 100,
  DEFAULT: 20,
}

export default async function updatePlayerStatsForProject(project) {
  _assertValidProject(project)
  const {retrospectiveSurveyId} = project

  const retroSurvey = await getSurveyById(retrospectiveSurveyId)
  const retroResponses = await findResponsesBySurveyId(retrospectiveSurveyId)
  const retroQuestionIds = retroSurvey.questionRefs.map(qref => qref.questionId)
  const retroQuestions = await findQuestionsByIds(retroQuestionIds)

  let teamPlayersById = mapById(await findPlayersByIds(project.playerIds))
  const statsQuestions = await _getStatsQuestions(retroQuestions)

  // ensure that we're only looking at valid responses about players who
  // actually played on the team, and adjust relative contribution responses to
  // ensure that they total 100%
  const playerResponses = _getPlayerResponses(project, teamPlayersById, retroResponses, retroQuestions, statsQuestions)
  const adjustedPlayerResponses = _adjustRCResponsesTo100Percent(playerResponses, statsQuestions)
  const playerResponsesById = groupResponsesBySubject(adjustedPlayerResponses)
  const adjustedProject = {...project, playerIds: Array.from(playerResponsesById.keys())}
  teamPlayersById = mapById(Array.from(teamPlayersById.values())
    .filter(player => adjustedProject.playerIds.includes(player.id)))

  // compute all stats and initialize Elo rating
  const playerStatsConfigsById = await _getPlayersStatsConfig(adjustedProject.playerIds)
  const computeStats = _computeStatsClosure(project, teamPlayersById, retroResponses, statsQuestions, playerStatsConfigsById)
  const teamPlayersStats = Array.from(playerResponsesById.values())
    .map(responses => computeStats(responses, statsQuestions))

  // compute updated Elo ratings and merge them in
  const teamPlayersStatsWithUpdatedEloRatings = _mergeEloRatings(teamPlayersStats, playerStatsConfigsById)

  const playerStatsUpdates = teamPlayersStatsWithUpdatedEloRatings.map(({playerId, ...stats}) => {
    return savePlayerProjectStats(playerId, project.id, stats)
  })

  await Promise.all(playerStatsUpdates)
}

function _assertValidProject(project) {
  const {id, name, playerIds, retrospectiveSurveyId} = project
  if (!playerIds || playerIds.length === 0) {
    throw new Error(`No players found on team for project ${name} (${id})`)
  }
  if (!retrospectiveSurveyId) {
    throw new Error(`Retrospective survey ID not set for project ${name} (${id})`)
  }
}

function _getPlayerResponses(project, teamPlayersById, retroResponses, retroQuestions, statsQuestions) {
  const isZeroHoursResponse = response => (response.questionId === statsQuestions.hours.id && parseInt(response.value, 10) === 0)
  const inactivePlayerIds = retroResponses.filter(isZeroHoursResponse).map(_ => _.respondentId)

  const isNotFromOrAboutInactivePlayer = response => {
    return !inactivePlayerIds.includes(response.respondentId) &&
      !inactivePlayerIds.includes(response.subjectId)
  }
  const activeRetroResponses = retroResponses.filter(isNotFromOrAboutInactivePlayer)

  const retroQuestionsById = mapById(retroQuestions)
  const responseQuestionSubjectIsPlayerOrTeam = response => {
    const responseQuestion = retroQuestionsById.get(response.questionId)
    const {subjectType} = responseQuestion || {}
    return subjectType === 'player' || subjectType === 'team'
  }
  const playerResponses = activeRetroResponses.filter(responseQuestionSubjectIsPlayerOrTeam)

  const playerIsOnTeam = playerId => !teamPlayersById.has(playerId)
  const invalidPlayerIds = Array.from(playerResponses
    .map(_ => _.subjectId)
    .filter(playerIsOnTeam)
    .reduce((result, playerId) => {
      result.add(playerId)
      return result
    }, new Set()))
  if (invalidPlayerIds.length > 0) {
    console.warn(
      'Survey responses found for players who are not on project ' +
      `${project.name} (${project.id}): ${invalidPlayerIds.join(', ')}. ` +
      'Ignoring responses from these players.'
    )
    return playerResponses.filter(response => !invalidPlayerIds.includes(response.subjectId))
  }

  return playerResponses
}

function _adjustRCResponsesTo100Percent(playerResponses, statsQuestions) {
  // adjust relative contribution responses so that they always add-up to 100%
  // (especially important because inactive players may have been removed, but
  // we do it for all cases because it is actually "more correct")
  const rcResponsesByRespondentId = playerResponses
    .filter(response => response.questionId === statsQuestions.rc.id)
    .reduce((result, response) => {
      const rcResponsesForRespondent = result.get(response.respondentId) || []
      rcResponsesForRespondent.push(response)
      result.set(response.respondentId, rcResponsesForRespondent)
      return result
    }, new Map())
  return playerResponses.map(response => {
    if (response.questionId !== statsQuestions.rc.id) {
      return response
    }
    const rcResponses = rcResponsesByRespondentId.get(response.respondentId)
    const values = rcResponses.map(_ => _.value)
    const totalContrib = sum(values)
    return {...response, value: response.value / totalContrib * 100}
  })
}

async function _getStatsQuestions(questions) {
  const stats = await statsByDescriptor()
  const getQ = descriptor => questions.filter(_ => _.statId === stats[descriptor].id)[0] || {}

  return {
    th: getQ(STAT_DESCRIPTORS.TECHNICAL_HEALTH),
    rc: getQ(STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION),
    hours: getQ(STAT_DESCRIPTORS.PROJECT_HOURS),
    challenge: getQ(STAT_DESCRIPTORS.CHALLENGE),
    cc: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION),
    cultureContributionStructure: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_STRUCTURE),
    cultureContributionSafety: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SAFETY),
    cultureContributionTruth: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_TRUTH),
    cultureContributionChallenge: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_CHALLENGE),
    cultureContributionSupport: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SUPPORT),
    cultureContributionEngagement: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENGAGEMENT),
    cultureContributionEnjoyment: getQ(STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENJOYMENT),
    tp: getQ(STAT_DESCRIPTORS.TEAM_PLAY),
    receptiveness: getQ(STAT_DESCRIPTORS.RECEPTIVENESS),
    resultsFocus: getQ(STAT_DESCRIPTORS.RESULTS_FOCUS),
    flexibleLeadership: getQ(STAT_DESCRIPTORS.FLEXIBLE_LEADERSHIP),
    frictionReduction: getQ(STAT_DESCRIPTORS.FRICTION_REDUCTION),
  }
}

async function _getPlayersStatsConfig(playerIds) {
  const users = await getPlayerInfo(playerIds)
  const playerStatsConfigs = users.map(user => ({
    id: user.id,
    ignoreWhenComputingElo: userCan(user, 'beIgnoredWhenComputingElo'),
  }))

  return mapById(playerStatsConfigs)
}

function _playerResponsesForQuestionById(retroResponses, questionId, valueFor = _ => _) {
  const responses = retroResponses.filter(_ => _.questionId === questionId)

  return responses.reduce((result, response) => {
    result.set(response.respondentId, valueFor(response.value))
    return result
  }, new Map())
}

function _computeStatsClosure(project, teamPlayersById, retroResponses, statsQuestions, playerStatsConfigsById) {
  const teamPlayerHours = _playerResponsesForQuestionById(retroResponses, statsQuestions.hours.id, _ => parseInt(_, 10))
  const teamPlayerChallenges = _playerResponsesForQuestionById(retroResponses, statsQuestions.challenge.id)
  const teamHours = sum(Array.from(teamPlayerHours.values()))

  // create a stats-computation function based on a closure of the passed-in
  // parameters as well as some additional derived data
  return (responses, statsQuestions) => {
    const playerId = responses[0].subjectId
    const player = teamPlayersById.get(playerId)
    const scores = _extractPlayerScores(statsQuestions, responses, playerId)
    const playerEstimationAccuraciesById = new Map()
    for (const player of teamPlayersById.values()) {
      const accuracy = ((player.stats || {}).weightedAverages || {}).estimationAccuracy || 0
      playerEstimationAccuraciesById.set(player.id, accuracy)
    }

    const expectedHours = project.expectedHours || 40

    const stats = {}
    stats.playerId = playerId // will be removed later
    stats.teamHours = teamHours
    stats.hours = Math.min(teamPlayerHours.get(playerId) || 0, expectedHours)
    stats.timeOnTask = (stats.hours === 0) ? 0 : stats.hours / expectedHours * 100
    stats.challenge = teamPlayerChallenges.get(playerId)
    stats.abc = aggregateBuildCycles(teamPlayersById.size)
    stats.th = technicalHealth(scores.th)
    stats.cc = cultureContribution(scores.cc)
    stats.cultureContributionStructure = cultureContributionStructure(scores.cultureContributionStructure)
    stats.cultureContributionSafety = cultureContributionSafety(scores.cultureContributionSafety)
    stats.cultureContributionTruth = cultureContributionTruth(scores.cultureContributionTruth)
    stats.cultureContributionChallenge = cultureContributionChallenge(scores.cultureContributionChallenge)
    stats.cultureContributionSupport = cultureContributionSupport(scores.cultureContributionSupport)
    stats.cultureContributionEngagement = cultureContributionEngagement(scores.cultureContributionEngagement)
    stats.cultureContributionEnjoyment = cultureContributionEnjoyment(scores.cultureContributionEnjoyment)
    stats.tp = teamPlay(scores.tp)
    stats.receptiveness = receptiveness(scores.receptiveness)
    stats.resultsFocus = resultsFocus(scores.resultsFocus)
    stats.flexibleLeadership = flexibleLeadership(scores.flexibleLeadership)
    stats.frictionReduction = frictionReduction(scores.frictionReduction)
    stats.rc = relativeContribution(scores.playerRCScoresById, playerEstimationAccuraciesById)
    stats.rcSelf = scores.rc.self || 0
    stats.rcOther = roundDecimal(avg(scores.rc.other)) || 0
    stats.rcPerHour = stats.hours && stats.rc ? roundDecimal(stats.rc / stats.hours) : 0
    stats.estimationBias = stats.rcSelf - stats.rcOther
    stats.estimationAccuracy = 100 - Math.abs(stats.estimationBias)
    stats.ec = expectedContribution(stats.hours, stats.teamHours)
    stats.ecd = expectedContributionDelta(stats.ec, stats.rc)
    stats.ecc = effectiveContributionCycles(stats.abc, stats.rc)
    stats.xp = experiencePoints(teamHours, stats.rc)
    if (!playerStatsConfigsById.get(playerId).ignoreWhenComputingElo) {
      stats.elo = (player.stats || {}).elo || {} // pull current overall Elo stats
    }

    return stats
  }
}

function _extractPlayerScores(statsQuestions, responses, playerId) {
  // extract values needed for each player's stats
  // from survey responses submitted about them
  const scores = {
    th: [],
    cc: [],
    cultureContributionStructure: [],
    cultureContributionSafety: [],
    cultureContributionTruth: [],
    cultureContributionChallenge: [],
    cultureContributionSupport: [],
    cultureContributionEngagement: [],
    cultureContributionEnjoyment: [],
    tp: [],
    receptiveness: [],
    flexibleLeadership: [],
    resultsFocus: [],
    frictionReduction: [],
    rc: {
      all: [],
      self: null,
      other: [],
    },
  }
  const playerRCScoresById = new Map()
  const appendScoreStats = Object.keys(scores).filter(_ => _ !== 'rc')

  responses.forEach(response => {
    const {
      questionId: responseQuestionId,
      value: responseValue,
    } = response

    switch (responseQuestionId) {
      case statsQuestions.rc.id:
        safePushInt(scores.rc.all, responseValue)
        if (response.respondentId === playerId) {
          scores.rc.self = parseInt(responseValue, 10)
        } else {
          safePushInt(scores.rc.other, responseValue)
        }
        playerRCScoresById.set(response.respondentId, responseValue)
        break

      default:
        appendScoreStats.forEach(stat => {
          if (responseQuestionId === statsQuestions[stat].id) {
            safePushInt(scores[stat], responseValue)
          }
        })
        break
    }
  })

  return {...scores, playerRCScoresById}
}

function _mergeEloRatings(teamPlayersStats, playerStatsConfigsById) {
  const playersWithEloStats = teamPlayersStats
    .filter(({playerId}) => !playerStatsConfigsById.get(playerId).ignoreWhenComputingElo)
  const eloRatings = _computeEloRatings(playersWithEloStats)
  const teamPlayersStatsWithUpdatedEloRatings = teamPlayersStats.map(stats => {
    const updatedElo = eloRatings.get(stats.playerId)
    if (!updatedElo) {
      return stats
    }
    const {rating, matches, kFactor, score} = updatedElo
    return {...stats, elo: {rating, matches, kFactor, score}}
  })

  return teamPlayersStatsWithUpdatedEloRatings
}

function _computeEloRatings(playerStats) {
  const scoreboard = playerStats
    .reduce((result, {playerId, ...stats}) => {
      const {elo = {}} = stats
      result.set(playerId, {
        id: playerId,
        rating: elo.rating || INITIAL_RATINGS.DEFAULT,
        matches: elo.matches || 0,
        kFactor: _kFactor(elo.matches),
        score: stats.rcPerHour, // effectiveness
      })
      return result
    }, new Map())

  // sorted by elo (descending) solely for the sake of being deterministic
  const sortedPlayerIds = Array.from(scoreboard.values())
                            .sort((a, b) => a.rating - b.rating)
                            .map(item => item.id)

  // pair every team player up to run "matches"
  const matches = toPairs(sortedPlayerIds)

  // for each team player pair, update ratings based on relative effectiveness
  matches.forEach(([playerIdA, playerIdB]) => {
    const playerA = scoreboard.get(playerIdA)
    const playerB = scoreboard.get(playerIdB)
    const [playerRatingA, playerRatingB] = eloRatings([playerA, playerB])

    playerA.rating = playerRatingA
    playerA.matches++
    playerA.kFactor = _kFactor(playerA.matches)

    playerB.rating = playerRatingB
    playerB.matches++
    playerB.kFactor = _kFactor(playerB.matches)
  })

  return scoreboard
}

function _kFactor(numMatches) {
  return (numMatches || 0) < 20 ?
    K_FACTORS.BEGINNER :
    K_FACTORS.DEFAULT
}
<|endoftext|>


<commit_before>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs        = require('fs');

describe('Test package.json and io-package.json', function() {
    it('Test package files', function (done) {
        console.log();

        var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
        var ioPackage = JSON.parse(fileContentIOPackage);

        var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
        var npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
        expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;

        expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
            console.log();
        }

        expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
        expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;

        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
            console.log();
        }
        expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
        if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
            console.log('WARNING: titleLang is not existing in io-package.json. Please add');
            console.log();
        }
        if (
            ioPackage.common.title.indexOf('iobroker') !== -1 ||
            ioPackage.common.title.indexOf('ioBroker') !== -1 ||
            ioPackage.common.title.indexOf('adapter') !== -1 ||
            ioPackage.common.title.indexOf('Adapter') !== -1
        ) {
            console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
            console.log();
        }

        if (ioPackage.common.name.indexOf('vis-') !== 0) {
            if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
                console.log('WARNING: Admin3 support is missing! Please add it');
                console.log();
            }
            if (ioPackage.common.materialize) {
                expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
            }
        }

        var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
        var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
        if (fileContentReadme.indexOf('## Changelog') === -1) {
            console.log('Warning: The README.md should have a section ## Changelog');
            console.log();
        }
        expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
        if (!licenseFileExists) {
            console.log('Warning: The License should also exist as LICENSE file');
            console.log();
        }
        if (fileContentReadme.indexOf('## License') === -1) {
            console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
            console.log();
        }
        done();
    });
});
<commit_msg>update basic package-file testing<commit_after>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
'use strict';

const expect = require('chai').expect;
const fs     = require('fs');

describe('Test package.json and io-package.json', () => {
    it('Test package files', done => {
        console.log();

        const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
        const ioPackage = JSON.parse(fileContentIOPackage);

        const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
        const npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
        expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;

        expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
            console.log();
        }

        expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
        expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;

        expect(ioPackage.common.license, 'ERROR: License missing in io-package in common.license').to.exist;

        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
            console.log();
        }
        expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
        if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
            console.log('WARNING: titleLang is not existing in io-package.json. Please add');
            console.log();
        }
        if (
            ioPackage.common.title.indexOf('iobroker') !== -1 ||
            ioPackage.common.title.indexOf('ioBroker') !== -1 ||
            ioPackage.common.title.indexOf('adapter') !== -1 ||
            ioPackage.common.title.indexOf('Adapter') !== -1
        ) {
            console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
            console.log();
        }

        if (!ioPackage.common.controller && !ioPackage.common.onlyWWW && !ioPackage.common.noConfig) {
            if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
                console.log('WARNING: Admin3 support is missing! Please add it');
                console.log();
            }
            if (ioPackage.common.materialize) {
                expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
            }
        }

        const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
        const fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
        if (fileContentReadme.indexOf('## Changelog') === -1) {
            console.log('Warning: The README.md should have a section ## Changelog');
            console.log();
        }
        expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
        if (!licenseFileExists) {
            console.log('Warning: The License should also exist as LICENSE file');
            console.log();
        }
        if (fileContentReadme.indexOf('## License') === -1) {
            console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
            console.log();
        }
        done();
    });
});
<|endoftext|>


<commit_before>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
'use strict';

const expect = require('chai').expect;
const fs     = require('fs');

describe('Test package.json and io-package.json', () => {
    it('Test package files', done => {
        console.log();

        const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
        const ioPackage = JSON.parse(fileContentIOPackage);

        const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
        const npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
        expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;

        expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
            console.log();
        }

        expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
        expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;

        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
            console.log();
        }
        expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
        if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
            console.log('WARNING: titleLang is not existing in io-package.json. Please add');
            console.log();
        }
        if (
            ioPackage.common.title.indexOf('iobroker') !== -1 ||
            ioPackage.common.title.indexOf('ioBroker') !== -1 ||
            ioPackage.common.title.indexOf('adapter') !== -1 ||
            ioPackage.common.title.indexOf('Adapter') !== -1
        ) {
            console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
            console.log();
        }

        if (!ioPackage.common.controller && !ioPackage.common.onlyWWW && !ioPackage.common.noConfig) {
            if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
                console.log('WARNING: Admin3 support is missing! Please add it');
                console.log();
            }
            if (ioPackage.common.materialize) {
                expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
            }
        }

        const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
        const fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
        if (fileContentReadme.indexOf('## Changelog') === -1) {
            console.log('Warning: The README.md should have a section ## Changelog');
            console.log();
        }
        expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
        if (!licenseFileExists) {
            console.log('Warning: The License should also exist as LICENSE file');
            console.log();
        }
        if (fileContentReadme.indexOf('## License') === -1) {
            console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
            console.log();
        }
        done();
    });
});
<commit_msg>update basic package-file testing<commit_after>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
'use strict';

const expect = require('chai').expect;
const fs     = require('fs');

describe('Test package.json and io-package.json', () => {
    it('Test package files', done => {
        console.log();

        const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
        const ioPackage = JSON.parse(fileContentIOPackage);

        const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
        const npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
        expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;

        expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
            console.log();
        }

        expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
        expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;

        expect(ioPackage.common.license, 'ERROR: License missing in io-package in common.license').to.exist;

        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
            console.log();
        }
        expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
        if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
            console.log('WARNING: titleLang is not existing in io-package.json. Please add');
            console.log();
        }
        if (
            ioPackage.common.title.indexOf('iobroker') !== -1 ||
            ioPackage.common.title.indexOf('ioBroker') !== -1 ||
            ioPackage.common.title.indexOf('adapter') !== -1 ||
            ioPackage.common.title.indexOf('Adapter') !== -1
        ) {
            console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
            console.log();
        }

        if (!ioPackage.common.controller && !ioPackage.common.onlyWWW && !ioPackage.common.noConfig) {
            if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
                console.log('WARNING: Admin3 support is missing! Please add it');
                console.log();
            }
            if (ioPackage.common.materialize) {
                expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
            }
        }

        const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
        const fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
        if (fileContentReadme.indexOf('## Changelog') === -1) {
            console.log('Warning: The README.md should have a section ## Changelog');
            console.log();
        }
        expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
        if (!licenseFileExists) {
            console.log('Warning: The License should also exist as LICENSE file');
            console.log();
        }
        if (fileContentReadme.indexOf('## License') === -1) {
            console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
            console.log();
        }
        done();
    });
});
<|endoftext|>